diff options
Diffstat (limited to 'src/core/broadcast.c')
| -rw-r--r-- | src/core/broadcast.c | 982 |
1 files changed, 982 insertions, 0 deletions
diff --git a/src/core/broadcast.c b/src/core/broadcast.c new file mode 100644 index 0000000..6a7cf68 --- /dev/null +++ b/src/core/broadcast.c @@ -0,0 +1,982 @@ +#include "hybbx/broadcast.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/plugin.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit.h" +#include "hybbx/ax25.h" +#include "hybbx/texts.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +void hybbx_ax25_frequency_table_clear(hybbx_ax25_frequency_table_t *table) +{ + if (table == NULL) { + return; + } + + memset(table, 0, sizeof(*table)); +} + +int hybbx_ax25_frequency_match(double a_mhz, double b_mhz) +{ + double diff; + + if (a_mhz <= 0.0 || b_mhz <= 0.0) { + return 0; + } + + diff = a_mhz - b_mhz; + if (diff < 0.0) { + diff = -diff; + } + + return diff < 0.001; +} + +void hybbx_ax25_frequency_apply(hybbx_ax25_frequency_table_t *table, + const hybbx_config_t *config) +{ + unsigned count; + unsigned i; + unsigned loaded = 0; + char key[24]; + const char *value; + + if (table == NULL || config == NULL) { + return; + } + + hybbx_ax25_frequency_table_clear(table); + + count = hybbx_config_get_uint(config, "ax25", "frequency_count", + 0u, 0u, HYBBX_AX25_FREQUENCY_MAX); + + for (i = 1; i <= HYBBX_AX25_FREQUENCY_MAX; i++) { + snprintf(key, sizeof(key), "frequency%u", i); + value = hybbx_config_get(config, "ax25", key, NULL); + if (value == NULL || value[0] == '\0') { + if (count > 0 && i > count) { + break; + } + continue; + } + + { + double mhz = strtod(value, NULL); + + if (mhz <= 0.0) { + continue; + } + if (loaded >= HYBBX_AX25_FREQUENCY_MAX) { + break; + } + + table->mhz[loaded] = mhz; + + snprintf(key, sizeof(key), "frequency%u_label", i); + value = hybbx_config_get(config, "ax25", key, NULL); + if (value == NULL || value[0] == '\0') { + snprintf(key, sizeof(key), "frequency%u_name", i); + value = hybbx_config_get(config, "ax25", key, NULL); + } + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(table->labels[loaded], value, + sizeof(table->labels[loaded])); + } else { + snprintf(table->labels[loaded], sizeof(table->labels[loaded]), + "%.3f MHz", mhz); + } + loaded++; + } + + if (count > 0 && i >= count) { + break; + } + } + + table->count = loaded; + + if (table->count > 0) { + hybbx_log_info("[ax25] %u configured frequencies (%.3f … %.3f MHz)", + table->count, table->mhz[0], table->mhz[table->count - 1]); + } else { + hybbx_log_info("[ax25] no frequencies in INI (set frequency1 … in [ax25])"); + } +} + +void hybbx_broadcast_config_defaults(hybbx_broadcast_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->ax25_enabled = 1; + cfg->ax25_auto = 1; + cfg->ax25_auto_interval_sec = HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC; + cfg->ax25_auto_stagger_sec = 0; + hybbx_strlcpy(cfg->ax25_mycall, "HYBBX", sizeof(cfg->ax25_mycall)); + hybbx_strlcpy(cfg->ax25_dest, "QST", sizeof(cfg->ax25_dest)); + hybbx_strlcpy(cfg->ax25_auto_message, HYBBX_BROADCAST_AUTO_MESSAGE_DEFAULT, + sizeof(cfg->ax25_auto_message)); + hybbx_ax25_frequency_table_clear(&cfg->frequencies); +} + +void hybbx_broadcast_config_apply(hybbx_broadcast_config_t *cfg, + const hybbx_config_t *config) +{ + const char *mycall; + const char *dest; + const char *auto_msg; + unsigned interval; + + if (cfg == NULL || config == NULL) { + return; + } + + hybbx_broadcast_config_defaults(cfg); + + cfg->enabled = hybbx_config_get_bool(config, "broadcast", "enabled", 1); + cfg->ax25_enabled = hybbx_config_get_bool(config, "broadcast", "ax25", 1); + cfg->ax25_auto = hybbx_config_get_bool(config, "broadcast", "ax25_auto", 1); + + interval = hybbx_config_get_uint(config, "broadcast", "ax25_auto_interval", + HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + 86400u); + cfg->ax25_auto_interval_sec = interval; + cfg->ax25_auto_stagger_sec = 0; + + mycall = hybbx_config_get(config, "broadcast", "ax25_mycall", NULL); + dest = hybbx_config_get(config, "broadcast", "ax25_dest", NULL); + auto_msg = hybbx_config_get(config, "broadcast", "ax25_auto_message", NULL); + if (mycall != NULL && mycall[0] != '\0') { + hybbx_ax25_address_t addr; + + if (hybbx_ax25_address_parse(mycall, &addr) == HYBBX_OK) { + hybbx_strlcpy(cfg->ax25_mycall, mycall, sizeof(cfg->ax25_mycall)); + } else { + hybbx_log_warn("[broadcast] ax25_mycall=%s invalid — using HYBBX", + mycall); + } + } + if (dest != NULL && dest[0] != '\0') { + hybbx_ax25_address_t addr; + + if (hybbx_ax25_address_parse(dest, &addr) == HYBBX_OK) { + hybbx_strlcpy(cfg->ax25_dest, dest, sizeof(cfg->ax25_dest)); + } else { + hybbx_log_warn("[broadcast] ax25_dest=%s invalid — using QST", dest); + hybbx_strlcpy(cfg->ax25_dest, "QST", sizeof(cfg->ax25_dest)); + } + } + if (auto_msg != NULL && auto_msg[0] != '\0') { + size_t msg_len = strlen(auto_msg); + + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + hybbx_log_warn("[broadcast] ax25_auto_message truncated (%zu > %u)", + msg_len, (unsigned)HYBBX_BROADCAST_AX25_MESSAGE_MAX); + } + hybbx_strlcpy(cfg->ax25_auto_message, auto_msg, + sizeof(cfg->ax25_auto_message)); + } + + hybbx_ax25_frequency_apply(&cfg->frequencies, config); + + hybbx_log_info("[broadcast] announce=%s ax25_auto=%s interval=%us " + "(min %us, band idle %us, link gap %us, per-link min %us)", + cfg->enabled ? "yes" : "no", + cfg->ax25_auto ? "yes" : "no", + cfg->ax25_auto_interval_sec, + (unsigned)HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + (unsigned)HYBBX_BROADCAST_AX25_BAND_IDLE_SEC, + (unsigned)HYBBX_BROADCAST_AX25_LINK_GAP_SEC, + (unsigned)HYBBX_BROADCAST_AX25_LINK_MIN_SEC); +} + +static hybbx_result_t broadcast_build_path(const hybbx_broadcast_config_t *cfg, + hybbx_ax25_path_t *path) +{ + if (cfg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(path, 0, sizeof(*path)); + + if (cfg->ax25_dest[0] == '\0' || strcmp(cfg->ax25_dest, "*") == 0) { + hybbx_log_warn("[broadcast] ax25_dest=%s invalid — using QST", + cfg->ax25_dest[0] != '\0' ? cfg->ax25_dest : "(empty)"); + if (hybbx_ax25_address_parse("QST", &path->dest) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + } else if (hybbx_ax25_address_parse(cfg->ax25_dest, &path->dest) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + if (hybbx_ax25_address_parse(cfg->ax25_mycall, &path->source) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static time_t g_ax25_last_sent; +static time_t g_ax25_link_last_sent[HYBBX_CIRCUIT_MAX_LINKS]; +static unsigned g_ax25_auto_tick; +static unsigned g_ax25_defer_log_sec; + +typedef struct broadcast_ax25_seq { + int active; + hybbx_service_t *service; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned next_index; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + time_t resume_at; +} broadcast_ax25_seq_t; + +static broadcast_ax25_seq_t g_ax25_seq; + +static double broadcast_link_target_mhz(const hybbx_broadcast_config_t *cfg, + const hybbx_circuit_broadcast_link_t *link, + unsigned link_index) +{ + (void)cfg; + (void)link_index; + + if (link == NULL) { + return 0.0; + } + + return link->frequency_mhz; +} + +static void broadcast_ax25_log_deferred(double frequency_mhz, const char *reason) +{ + if (reason == NULL) { + return; + } + + g_ax25_defer_log_sec++; + if (g_ax25_defer_log_sec < 60u) { + return; + } + + g_ax25_defer_log_sec = 0; + if (frequency_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 %.3f MHz deferred (%s)", + frequency_mhz, reason); + } else { + hybbx_log_stats("[broadcast] ax25 auto deferred (%s)", reason); + } +} + +static void broadcast_ax25_seq_defer_until(time_t resume_at, double target_mhz, + const char *reason) +{ + time_t now = time(NULL); + + broadcast_ax25_log_deferred(target_mhz, reason); + if (now != (time_t)-1 && (resume_at == (time_t)-1 || resume_at <= now)) { + resume_at = now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC; + } + g_ax25_seq.resume_at = resume_at; +} + +static int broadcast_hub_link_band_idle(hybbx_circuit_hub_t *hub, + unsigned slot_index) +{ + if (hub == NULL) { + return 0; + } + + return hybbx_circuit_hub_link_band_idle(hub, slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); +} + +static int broadcast_ax25_rate_ok(unsigned interval_sec) +{ + time_t now; + time_t elapsed; + + if (g_ax25_last_sent == 0) { + return 1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return 0; + } + + elapsed = now - g_ax25_last_sent; + if (elapsed < 0) { + return 1; + } + + return (unsigned)elapsed >= interval_sec; +} + +static int broadcast_ax25_link_rate_ok(unsigned slot_index, + unsigned interval_sec) +{ + time_t now; + time_t elapsed; + + if (slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return 0; + } + + if (g_ax25_link_last_sent[slot_index] == 0) { + return 1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return 0; + } + + elapsed = now - g_ax25_link_last_sent[slot_index]; + if (elapsed < 0) { + return 1; + } + + return (unsigned)elapsed >= interval_sec; +} + +static void broadcast_ax25_mark_sent(void) +{ + time_t now = time(NULL); + + if (now != (time_t)-1) { + g_ax25_last_sent = now; + } +} + +static void broadcast_ax25_mark_link_sent(unsigned slot_index) +{ + time_t now = time(NULL); + + if (slot_index >= HYBBX_CIRCUIT_MAX_LINKS || now == (time_t)-1) { + return; + } + + g_ax25_link_last_sent[slot_index] = now; + g_ax25_last_sent = now; +} + +static size_t broadcast_expand_service_token(char *out, size_t out_len, + const char *tmpl, + const char *service_name) +{ + size_t pos = 0; + size_t i = 0; + + if (out == NULL || out_len == 0 || tmpl == NULL) { + return 0; + } + + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + + out[0] = '\0'; + + while (tmpl[i] != '\0' && pos + 1 < out_len) { + if (strncmp(tmpl + i, HYBBX_BANNER_TOKEN_SERVICE, + strlen(HYBBX_BANNER_TOKEN_SERVICE)) == 0) { + size_t n = strlen(service_name); + + if (pos + n >= out_len) { + n = out_len - pos - 1; + } + memcpy(out + pos, service_name, n); + pos += n; + i += strlen(HYBBX_BANNER_TOKEN_SERVICE); + continue; + } + + out[pos++] = tmpl[i++]; + } + + out[pos] = '\0'; + return pos; +} + +static hybbx_result_t broadcast_ax25_send_slot(hybbx_service_t *service, + unsigned slot_index, + const char *message, + int enforce_rate_limit) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_ax25_path_t path; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t msg_len; + hybbx_result_t rc; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + const char *link_id = ""; + double target_mhz = 0.0; + + if (service == NULL || message == NULL || message[0] == '\0' || + slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].slot_index == slot_index) { + link_id = links[i].link_id; + target_mhz = links[i].frequency_mhz; + break; + } + } + if (link_id[0] == '\0') { + return HYBBX_ERR_DENIED; + } + + if (!hybbx_circuit_hub_link_band_idle(hub, slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC)) { + return HYBBX_ERR_BUSY; + } + + if (enforce_rate_limit && + !broadcast_ax25_link_rate_ok(slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + return HYBBX_ERR_BUSY; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_circuit_hub_link_broadcast_qos(hub)) { + return HYBBX_ERR_BUSY; + } + + rc = broadcast_build_path(cfg, &path); + if (rc != HYBBX_OK) { + return rc; + } + + { + uint8_t ax25_frame[HYBBX_AX25_FRAME_MAX]; + size_t ax25_len = hybbx_ax25_build_ui(&path, + (const uint8_t *)message, msg_len, + ax25_frame, sizeof(ax25_frame)); + + if (ax25_len == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[broadcast] ax25 path %s>%s payload=%zu slot=%u", + path.source.call, path.dest.call, msg_len, slot_index); + + frame_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + HYBBX_CIRCUIT_FLAG_TX, + ax25_frame, ax25_len, + frame, sizeof(frame)); + } + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + rc = hybbx_circuit_hub_send_hbx_slot(hub, slot_index, frame, frame_len, 1); + if (rc != HYBBX_OK) { + return rc; + } + + broadcast_ax25_mark_link_sent(slot_index); + + if (target_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 link=%s %.3f MHz (slot %u): %s", + link_id, target_mhz, slot_index, message); + } else { + hybbx_log_stats("[broadcast] ax25 link=%s (slot %u): %s", + link_id, slot_index, message); + } + + return HYBBX_OK; +} + +static hybbx_result_t broadcast_ax25_send(hybbx_service_t *service, + double frequency_mhz, + const char *message, + int enforce_rate_limit) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_ax25_path_t path; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t msg_len; + hybbx_result_t rc; + unsigned sent_links; + int per_link = 0; + + if (service == NULL || message == NULL || message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + if (frequency_mhz > 0.0) { + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + int any_rate_ok = 0; + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].frequency_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, + links[i].frequency_mhz)) { + continue; + } + if (!enforce_rate_limit || + broadcast_ax25_link_rate_ok(links[i].slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + any_rate_ok = 1; + break; + } + } + if (!any_rate_ok) { + return HYBBX_ERR_BUSY; + } + per_link = 1; + } else if (enforce_rate_limit && + !broadcast_ax25_rate_ok(cfg->ax25_auto_interval_sec)) { + return HYBBX_ERR_BUSY; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_circuit_hub_link_broadcast_qos(hub)) { + return HYBBX_ERR_BUSY; + } + + rc = broadcast_build_path(cfg, &path); + if (rc != HYBBX_OK) { + return rc; + } + + { + uint8_t ax25_frame[HYBBX_AX25_FRAME_MAX]; + size_t ax25_len = hybbx_ax25_build_ui(&path, + (const uint8_t *)message, msg_len, + ax25_frame, sizeof(ax25_frame)); + + if (ax25_len == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[broadcast] ax25 path %s>%s payload=%zu", + path.source.call, path.dest.call, msg_len); + + frame_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + HYBBX_CIRCUIT_FLAG_TX, + ax25_frame, ax25_len, + frame, sizeof(frame)); + } + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + rc = hybbx_circuit_hub_multicast_hbx(hub, frame, frame_len, + frequency_mhz, 1, &sent_links); + if (rc != HYBBX_OK) { + return rc; + } + + if (per_link && frequency_mhz > 0.0) { + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].frequency_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, + links[i].frequency_mhz)) { + continue; + } + broadcast_ax25_mark_link_sent(links[i].slot_index); + } + } else { + broadcast_ax25_mark_sent(); + } + + if (frequency_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 %.3f MHz (%u hub): %s", + frequency_mhz, sent_links, message); + } else { + hybbx_log_stats("[broadcast] ax25 all qualifying links (%u hub): %s", + sent_links, message); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_broadcast_ax25(hybbx_service_t *service, + double frequency_mhz, + const char *message) +{ + return broadcast_ax25_send(service, frequency_mhz, message, 1); +} + +static int broadcast_ax25_seq_start(hybbx_service_t *service, + hybbx_circuit_hub_t *hub, + const hybbx_circuit_broadcast_link_t *links, + unsigned link_count, + const char *message) +{ + if (g_ax25_seq.active || service == NULL || hub == NULL || + links == NULL || link_count == 0 || message == NULL || + message[0] == '\0' || link_count > HYBBX_CIRCUIT_MAX_LINKS) { + return 0; + } + + memset(&g_ax25_seq, 0, sizeof(g_ax25_seq)); + g_ax25_seq.active = 1; + g_ax25_seq.service = service; + g_ax25_seq.hub = hub; + memcpy(g_ax25_seq.links, links, + link_count * sizeof(g_ax25_seq.links[0])); + g_ax25_seq.link_count = link_count; + hybbx_strlcpy(g_ax25_seq.message, message, sizeof(g_ax25_seq.message)); + return 1; +} + +static void broadcast_ax25_seq_advance(void) +{ + const hybbx_broadcast_config_t *cfg; + time_t now; + unsigned idx; + double target_mhz; + hybbx_result_t rc; + int advance = 0; + + if (!g_ax25_seq.active) { + return; + } + + if (g_ax25_seq.hub == NULL || + !hybbx_circuit_hub_running(g_ax25_seq.hub)) { + g_ax25_seq.active = 0; + return; + } + + now = time(NULL); + if (now == (time_t)-1) { + return; + } + + if (g_ax25_seq.resume_at != 0 && now < g_ax25_seq.resume_at) { + return; + } + + if (g_ax25_seq.next_index >= g_ax25_seq.link_count) { + g_ax25_seq.active = 0; + return; + } + + cfg = hybbx_service_get_broadcast(g_ax25_seq.service); + if (cfg == NULL) { + g_ax25_seq.active = 0; + return; + } + + idx = g_ax25_seq.next_index; + target_mhz = broadcast_link_target_mhz(cfg, &g_ax25_seq.links[idx], idx); + + if (target_mhz <= 0.0) { + hybbx_log_stats("[broadcast] ax25 skip link=%s (no MHz)", + g_ax25_seq.links[idx].link_id); + advance = 1; + } else if (!broadcast_hub_link_band_idle(g_ax25_seq.hub, + g_ax25_seq.links[idx].slot_index)) { + time_t ready = hybbx_circuit_hub_link_band_ready_at( + g_ax25_seq.hub, g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); + + broadcast_ax25_seq_defer_until(ready, target_mhz, "band busy"); + return; + } else if (!broadcast_ax25_link_rate_ok(g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + broadcast_ax25_seq_defer_until( + now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC, + target_mhz, "link rate"); + return; + } else { + rc = broadcast_ax25_send_slot(g_ax25_seq.service, + g_ax25_seq.links[idx].slot_index, + g_ax25_seq.message, 0); + if (rc == HYBBX_OK) { + g_ax25_defer_log_sec = 0; + advance = 1; + } else if (rc == HYBBX_ERR_BUSY) { + if (!hybbx_circuit_hub_running(g_ax25_seq.hub)) { + g_ax25_seq.active = 0; + return; + } + { + time_t ready = hybbx_circuit_hub_link_band_ready_at( + g_ax25_seq.hub, g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); + + broadcast_ax25_seq_defer_until(ready, target_mhz, + "band busy"); + } + return; + } else if (rc == HYBBX_ERR_DENIED) { + broadcast_ax25_seq_defer_until( + now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC, + target_mhz, "no qualifying link"); + return; + } else { + hybbx_log_warn("[broadcast] ax25 %.3f MHz failed (%d)", + target_mhz, (int)rc); + advance = 1; + } + } + + if (!advance) { + return; + } + + g_ax25_seq.next_index++; + if (g_ax25_seq.next_index < g_ax25_seq.link_count) { + g_ax25_seq.resume_at = now + (time_t)HYBBX_BROADCAST_AX25_LINK_GAP_SEC; + } else { + g_ax25_seq.active = 0; + g_ax25_seq.resume_at = 0; + } +} + +void hybbx_broadcast_ax25_seq_cancel(void) +{ + g_ax25_seq.active = 0; + g_ax25_seq.resume_at = 0; +} + +hybbx_result_t hybbx_broadcast_ax25_manual(hybbx_service_t *service) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + unsigned link_count; + + if (service == NULL) { + return HYBBX_ERR_INVALID; + } + + if (g_ax25_seq.active) { + return HYBBX_ERR_BUSY; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + if (link_count == 0) { + return HYBBX_ERR_DENIED; + } + + broadcast_expand_service_token(message, sizeof(message), + cfg->ax25_auto_message, + hybbx_service_get_name(service)); + if (message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_log_info("[broadcast] ax25 manual sequential (%u link(s)): %s", + link_count, message); + + if (!broadcast_ax25_seq_start(service, hub, links, link_count, message)) { + return HYBBX_ERR_BUSY; + } + + broadcast_ax25_seq_advance(); + return HYBBX_OK; +} + +void hybbx_broadcast_ax25_tick(hybbx_service_t *service) +{ + const hybbx_broadcast_config_t *cfg; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned interval; + + if (service == NULL) { + return; + } + + if (g_ax25_seq.active) { + broadcast_ax25_seq_advance(); + return; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled || !cfg->ax25_auto) { + return; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return; + } + + interval = cfg->ax25_auto_interval_sec; + if (interval == 0) { + return; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + if (link_count == 0) { + return; + } + + g_ax25_auto_tick++; + + broadcast_expand_service_token(message, sizeof(message), + cfg->ax25_auto_message, + hybbx_service_get_name(service)); + if (message[0] == '\0') { + return; + } + + if (g_ax25_auto_tick < interval) { + return; + } + if (!broadcast_ax25_rate_ok(interval)) { + return; + } + + hybbx_log_info("[broadcast] ax25 auto sequential (%u link(s)): %s", + link_count, message); + + if (!broadcast_ax25_seq_start(service, hub, links, link_count, message)) { + return; + } + + g_ax25_auto_tick = 0; + broadcast_ax25_seq_advance(); +} + +typedef struct broadcast_announce_ctx { + hybbx_session_t *from; + const char *from_name; + const char *message; +} broadcast_announce_ctx_t; + +static void broadcast_announce_visitor(hybbx_session_t *session, void *userdata) +{ + broadcast_announce_ctx_t *ctx = (broadcast_announce_ctx_t *)userdata; + char line[HYBBX_LINE_MAX]; + + if (session == NULL || ctx == NULL || ctx->message == NULL) { + return; + } + + /* + * Local /broadcast is for online human users only. Circuit link adapter + * sessions (HBX bridges to TNCs) must not receive terminal fan-out — + * it floods low-bandwidth circuit queues and triggers load-balance + * pause/break on packet-radio links. + */ + if (!hybbx_session_is_interactive_user(session)) { + return; + } + if (!hybbx_session_logged_in(session)) { + return; + } + + if (hybbx_msg_format_sysop(line, sizeof(line), + ctx->from_name != NULL ? ctx->from_name : "Announce", + ctx->message) != HYBBX_OK) { + return; + } + if (ctx->from == NULL || session != ctx->from) { + (void)hybbx_session_command_gap(session); + } + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_broadcast_announce(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + broadcast_announce_ctx_t ctx; + const hybbx_broadcast_config_t *cfg; + size_t msg_len; + + if (service == NULL || message == NULL || message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.from_name = (from != NULL) ? hybbx_session_display_name(from) : "Announce"; + ctx.message = message; + hybbx_service_visit_sessions(service, broadcast_announce_visitor, &ctx); + + hybbx_log_stats("[broadcast] announce to local Main (%zu bytes): %s", + msg_len, message); + return HYBBX_OK; +} |
