From fa05a5f8238e9e1417235711656e5062ccc843a7 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Fri, 7 Aug 2026 18:23:28 +0000 Subject: Initial push --- stacks/max25-bcpr/src/bcpr_config.c | 397 +++++++++++++++++++ stacks/max25-bcpr/src/bcpr_crc.c | 55 +++ stacks/max25-bcpr/src/bcpr_daemon.c | 289 ++++++++++++++ stacks/max25-bcpr/src/bcpr_engine.c | 652 +++++++++++++++++++++++++++++++ stacks/max25-bcpr/src/bcpr_hdlc.c | 293 ++++++++++++++ stacks/max25-bcpr/src/bcpr_kiss.c | 127 ++++++ stacks/max25-bcpr/src/bcpr_lock.c | 202 ++++++++++ stacks/max25-bcpr/src/bcpr_runas.c | 81 ++++ stacks/max25-bcpr/src/bcpr_ser12.c | 246 ++++++++++++ stacks/max25-bcpr/src/bcpr_uart.c | 251 ++++++++++++ stacks/max25-bcpr/src/max25-bcprd-init.c | 255 ++++++++++++ stacks/max25-bcpr/src/max25-bcprd.c | 205 ++++++++++ 12 files changed, 3053 insertions(+) create mode 100644 stacks/max25-bcpr/src/bcpr_config.c create mode 100644 stacks/max25-bcpr/src/bcpr_crc.c create mode 100644 stacks/max25-bcpr/src/bcpr_daemon.c create mode 100644 stacks/max25-bcpr/src/bcpr_engine.c create mode 100644 stacks/max25-bcpr/src/bcpr_hdlc.c create mode 100644 stacks/max25-bcpr/src/bcpr_kiss.c create mode 100644 stacks/max25-bcpr/src/bcpr_lock.c create mode 100644 stacks/max25-bcpr/src/bcpr_runas.c create mode 100644 stacks/max25-bcpr/src/bcpr_ser12.c create mode 100644 stacks/max25-bcpr/src/bcpr_uart.c create mode 100644 stacks/max25-bcpr/src/max25-bcprd-init.c create mode 100644 stacks/max25-bcpr/src/max25-bcprd.c (limited to 'stacks/max25-bcpr/src') diff --git a/stacks/max25-bcpr/src/bcpr_config.c b/stacks/max25-bcpr/src/bcpr_config.c new file mode 100644 index 0000000..3707891 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_config.c @@ -0,0 +1,397 @@ +/* + * Simple INI loader for max25-bcpr (max 2 devices: [bc0] / [bc1]). + */ +#include "bcpr/bcpr_config.h" + +#include +#include +#include +#include + +void bcpr_config_defaults(bcpr_config_t *cfg) +{ + int i; + memset(cfg, 0, sizeof(*cfg)); + snprintf(cfg->state_dir, sizeof(cfg->state_dir), "%s", "/tmp/max25-bcpr"); + cfg->dry_run = 0; + cfg->n_dev = 0; + /* FlexNet SER12.doc: 14.5 s keyed / 500 ms unkey. */ + cfg->ptt_wd = 1; + cfg->ptt_wd_key_ms = 14500; + cfg->ptt_wd_pause_ms = 500; + cfg->txd_bias = BCPR_TXD_PULSE; + cfg->tot_enabled = 1; + cfg->tot_max_key_ms = 25000; + cfg->tot_min_gap_ms = 1500; + cfg->tot_max_consecutive = 3; + cfg->tot_max_bursts = 8; + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + bcpr_dev_config_t *d = &cfg->dev[i]; + d->enabled = 0; + d->baud = 1200; + snprintf(d->mode, sizeof(d->mode), "%s", "ser12*"); + d->tx_delay = 35; + d->tx_tail = 2; + d->slottime = 10; + d->ppersist = 40; + d->fulldup = 0; + d->ptt_wd = 1; + d->ptt_wd_key_ms = 14500; + d->ptt_wd_pause_ms = 500; + d->txd_bias = BCPR_TXD_PULSE; + d->tot_enabled = 1; + d->tot_max_key_ms = 25000; + d->tot_min_gap_ms = 1500; + d->tot_max_consecutive = 3; + d->tot_max_bursts = 8; + } +} + +static void trim(char *s) +{ + char *e; + while (*s && isspace((unsigned char)*s)) { + memmove(s, s + 1, strlen(s)); + } + e = s + strlen(s); + while (e > s && isspace((unsigned char)e[-1])) { + *--e = '\0'; + } +} + +static int parse_u(const char *v, unsigned *out) +{ + char *end = NULL; + unsigned long x = strtoul(v, &end, 0); + if (!v[0] || (end && *end)) { + return -1; + } + *out = (unsigned)x; + return 0; +} + +static int parse_i(const char *v, int *out) +{ + char *end = NULL; + long x = strtol(v, &end, 0); + if (!v[0] || (end && *end)) { + return -1; + } + *out = (int)x; + return 0; +} + +static int truthy(const char *v) +{ + return (strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0 || + strcasecmp(v, "on") == 0 || strcmp(v, "1") == 0); +} + +static int parse_txd_bias(const char *v, int *out) +{ + if (strcasecmp(v, "pulse") == 0 || strcasecmp(v, "sailer") == 0) { + *out = BCPR_TXD_PULSE; + return 0; + } + if (strcasecmp(v, "steady") == 0 || strcasecmp(v, "tfpcx") == 0 || + strcasecmp(v, "break") == 0) { + *out = BCPR_TXD_STEADY; + return 0; + } + return -1; +} + +static int apply_tot(int *enabled, int *max_key_ms, int *min_gap_ms, + int *max_consecutive, int *max_bursts, + const char *key, const char *val) +{ + if (strcmp(key, "tot") == 0 || strcmp(key, "tot_enabled") == 0) { + *enabled = truthy(val) ? 1 : 0; + return 0; + } + if (strcmp(key, "tot_max_key_sec") == 0 || strcmp(key, "tot_max_key_ms") == 0) { + if (strcmp(key, "tot_max_key_sec") == 0) { + int sec; + if (parse_i(val, &sec) != 0 || sec < 1) { + return -1; + } + *max_key_ms = sec * 1000; + return 0; + } + return parse_i(val, max_key_ms); + } + if (strcmp(key, "tot_min_gap_sec") == 0 || strcmp(key, "tot_min_gap_ms") == 0) { + if (strcmp(key, "tot_min_gap_sec") == 0) { + int sec; + if (parse_i(val, &sec) != 0 || sec < 0) { + return -1; + } + *min_gap_ms = sec * 1000; + return 0; + } + return parse_i(val, min_gap_ms); + } + if (strcmp(key, "tot_max_consecutive") == 0) { + return parse_i(val, max_consecutive); + } + if (strcmp(key, "tot_max_bursts") == 0) { + return parse_i(val, max_bursts); + } + return 1; +} + +static int apply_wd_txd(int *ptt_wd, int *key_ms, int *pause_ms, int *txd_bias, + const char *key, const char *val) +{ + if (strcmp(key, "ptt_wd") == 0 || strcmp(key, "ptt_watchdog") == 0) { + *ptt_wd = truthy(val) ? 1 : 0; + return 0; + } + if (strcmp(key, "ptt_wd_key_ms") == 0) { + return parse_i(val, key_ms); + } + if (strcmp(key, "ptt_wd_pause_ms") == 0) { + return parse_i(val, pause_ms); + } + if (strcmp(key, "txd_bias") == 0) { + return parse_txd_bias(val, txd_bias); + } + return 1; /* not handled */ +} + +static void normalize_legacy_path(char *buf, size_t len) +{ + static const struct { + const char *from; + const char *to; + } map[] = { + {"/tmp/bcpr/", "/tmp/max25-bcpr/"}, + {"/tmp/bcpr", "/tmp/max25-bcpr"}, + {"/var/run/bcpr/", "/tmp/max25-bcpr/"}, + {"/var/run/bcpr", "/tmp/max25-bcpr"}, + }; + size_t i; + size_t flen; + if (!buf || !len || !buf[0]) { + return; + } + for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) { + flen = strlen(map[i].from); + if (strncmp(buf, map[i].from, flen) != 0) { + continue; + } + if (buf[flen] != '\0' && buf[flen] != '/') { + continue; + } + { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "%s%s", map[i].to, buf + flen); + snprintf(buf, len, "%s", tmp); + } + return; + } +} + +static int apply_kv(bcpr_config_t *cfg, int section, const char *key, + const char *val) +{ + /* section: -1 = [max25-bcpr]/[bcpr] global, 0 = bc0, 1 = bc1 */ + if (section < 0) { + int rc; + if (strcmp(key, "dry_run") == 0) { + cfg->dry_run = truthy(val); + return 0; + } + if (strcmp(key, "state_dir") == 0) { + snprintf(cfg->state_dir, sizeof(cfg->state_dir), "%s", val); + return 0; + } + if (strcmp(key, "user") == 0) { + snprintf(cfg->run_user, sizeof(cfg->run_user), "%s", val); + return 0; + } + if (strcmp(key, "group") == 0) { + snprintf(cfg->run_group, sizeof(cfg->run_group), "%s", val); + return 0; + } + rc = apply_wd_txd(&cfg->ptt_wd, &cfg->ptt_wd_key_ms, + &cfg->ptt_wd_pause_ms, &cfg->txd_bias, key, val); + if (rc <= 0) { + /* Propagate global WD/TXD defaults onto both devices. */ + int i; + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + cfg->dev[i].ptt_wd = cfg->ptt_wd; + cfg->dev[i].ptt_wd_key_ms = cfg->ptt_wd_key_ms; + cfg->dev[i].ptt_wd_pause_ms = cfg->ptt_wd_pause_ms; + cfg->dev[i].txd_bias = cfg->txd_bias; + } + return rc; + } + rc = apply_tot(&cfg->tot_enabled, &cfg->tot_max_key_ms, &cfg->tot_min_gap_ms, + &cfg->tot_max_consecutive, &cfg->tot_max_bursts, key, val); + if (rc <= 0) { + int i; + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + cfg->dev[i].tot_enabled = cfg->tot_enabled; + cfg->dev[i].tot_max_key_ms = cfg->tot_max_key_ms; + cfg->dev[i].tot_min_gap_ms = cfg->tot_min_gap_ms; + cfg->dev[i].tot_max_consecutive = cfg->tot_max_consecutive; + cfg->dev[i].tot_max_bursts = cfg->tot_max_bursts; + } + return rc; + } + return 0; + } + if (section >= BCPR_MAX_DEVICES) { + return -1; + } + { + bcpr_dev_config_t *d = &cfg->dev[section]; + int rc; + if (strcmp(key, "enabled") == 0) { + d->enabled = truthy(val); + } else if (strcmp(key, "serial") == 0 || strcmp(key, "tty") == 0) { + snprintf(d->serial, sizeof(d->serial), "%s", val); + d->enabled = 1; + } else if (strcmp(key, "iobase") == 0) { + return parse_u(val, &d->iobase); + } else if (strcmp(key, "irq") == 0) { + return parse_u(val, &d->irq); + } else if (strcmp(key, "baud") == 0) { + return parse_u(val, &d->baud); + } else if (strcmp(key, "mode") == 0) { + snprintf(d->mode, sizeof(d->mode), "%s", val); + } else if (strcmp(key, "kiss_link") == 0) { + snprintf(d->kiss_link, sizeof(d->kiss_link), "%s", val); + } else if (strcmp(key, "tx_delay") == 0 || strcmp(key, "txdelay") == 0) { + return parse_i(val, &d->tx_delay); + } else if (strcmp(key, "tx_tail") == 0 || strcmp(key, "txtail") == 0) { + return parse_i(val, &d->tx_tail); + } else if (strcmp(key, "slottime") == 0) { + return parse_i(val, &d->slottime); + } else if (strcmp(key, "ppersist") == 0) { + return parse_i(val, &d->ppersist); + } else if (strcmp(key, "fulldup") == 0) { + d->fulldup = truthy(val); + } else { + rc = apply_wd_txd(&d->ptt_wd, &d->ptt_wd_key_ms, &d->ptt_wd_pause_ms, + &d->txd_bias, key, val); + if (rc < 0) { + return -1; + } + if (rc == 0) { + return 0; + } + rc = apply_tot(&d->tot_enabled, &d->tot_max_key_ms, &d->tot_min_gap_ms, + &d->tot_max_consecutive, &d->tot_max_bursts, key, val); + if (rc < 0) { + return -1; + } + if (rc == 0) { + return 0; + } + } + } + return 0; +} + +int bcpr_config_load(bcpr_config_t *cfg, const char *path) +{ + FILE *f; + char line[512]; + int section = -1; + int i; + + if (!cfg || !path) { + return -1; + } + bcpr_config_defaults(cfg); + f = fopen(path, "r"); + if (!f) { + return -1; + } + while (fgets(line, sizeof(line), f)) { + char *eq; + char *p = line; + trim(p); + if (p[0] == '\0' || p[0] == '#' || p[0] == ';') { + continue; + } + if (p[0] == '[') { + char *end = strchr(p, ']'); + if (!end) { + continue; + } + *end = '\0'; + p++; + if (strcasecmp(p, "max25-bcpr") == 0 || strcasecmp(p, "bcpr") == 0 || + strcasecmp(p, "global") == 0) { + section = -1; + } else if (strcasecmp(p, "bc0") == 0 || + strcasecmp(p, "device.bc0") == 0) { + section = 0; + } else if (strcasecmp(p, "bc1") == 0 || + strcasecmp(p, "device.bc1") == 0) { + section = 1; + } else { + section = -2; /* ignore unknown */ + } + continue; + } + if (section == -2) { + continue; + } + eq = strchr(p, '='); + if (!eq) { + continue; + } + *eq = '\0'; + trim(p); + trim(eq + 1); + (void)apply_kv(cfg, section, p, eq + 1); + } + fclose(f); + + normalize_legacy_path(cfg->state_dir, sizeof(cfg->state_dir)); + + cfg->n_dev = 0; + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + if (cfg->dev[i].enabled && cfg->dev[i].serial[0]) { + if (!cfg->dev[i].kiss_link[0]) { + snprintf(cfg->dev[i].kiss_link, sizeof(cfg->dev[i].kiss_link), + "%s/kiss-bc%d", cfg->state_dir, i); + } + normalize_legacy_path(cfg->dev[i].kiss_link, + sizeof(cfg->dev[i].kiss_link)); + /* Clamp WD timings. */ + if (cfg->dev[i].ptt_wd_key_ms < 1000) { + cfg->dev[i].ptt_wd_key_ms = 1000; + } + if (cfg->dev[i].ptt_wd_pause_ms < 50) { + cfg->dev[i].ptt_wd_pause_ms = 50; + } + if (cfg->dev[i].txd_bias != BCPR_TXD_STEADY) { + cfg->dev[i].txd_bias = BCPR_TXD_PULSE; + } + if (cfg->dev[i].tot_max_key_ms < 1000) { + cfg->dev[i].tot_max_key_ms = 1000; + } + if (cfg->dev[i].tot_min_gap_ms < 0) { + cfg->dev[i].tot_min_gap_ms = 0; + } + if (cfg->dev[i].tot_max_consecutive < 1) { + cfg->dev[i].tot_max_consecutive = 1; + } + if (cfg->dev[i].tot_max_bursts < 1) { + cfg->dev[i].tot_max_bursts = 1; + } + cfg->n_dev++; + } else { + cfg->dev[i].enabled = 0; + } + } + if (cfg->n_dev > BCPR_MAX_DEVICES) { + return -1; + } + return 0; +} diff --git a/stacks/max25-bcpr/src/bcpr_crc.c b/stacks/max25-bcpr/src/bcpr_crc.c new file mode 100644 index 0000000..6aabc49 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_crc.c @@ -0,0 +1,55 @@ +#include "bcpr/bcpr_crc.h" + +/* CRC-CCITT poly 0x8408 reflected (hdlcdrv / WAMPES style via crc_ccitt). */ +static const uint16_t bcpr_crc_table[256] = { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, + 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, + 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, + 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, + 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, + 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, + 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, + 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, + 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, + 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, + 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, + 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, + 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, + 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, + 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, + 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, + 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, + 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, + 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, + 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, + 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, + 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, + 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, + 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 +}; + +uint16_t bcpr_crc_ccitt(uint16_t crc, const uint8_t *buf, size_t len) +{ + while (len--) { + crc = (crc >> 8) ^ bcpr_crc_table[(crc ^ *buf++) & 0xff]; + } + return crc; +} + +void bcpr_append_crc_ccitt(uint8_t *buffer, int len) +{ + unsigned int crc = bcpr_crc_ccitt(0xffff, buffer, (size_t)len) ^ 0xffff; + buffer += len; + *buffer++ = (uint8_t)(crc & 0xff); + *buffer++ = (uint8_t)(crc >> 8); +} + +int bcpr_check_crc_ccitt(const uint8_t *buf, int cnt) +{ + return (bcpr_crc_ccitt(0xffff, buf, (size_t)cnt) & 0xffff) == 0xf0b8; +} diff --git a/stacks/max25-bcpr/src/bcpr_daemon.c b/stacks/max25-bcpr/src/bcpr_daemon.c new file mode 100644 index 0000000..9019159 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_daemon.c @@ -0,0 +1,289 @@ +/* + * Shared max25-bcprd run loop (KISS thread + SER12 engine). + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_daemon.h" +#include "bcpr/bcpr_ser12.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t g_stop; + +static void on_sig(int sig) +{ + (void)sig; + g_stop = 1; +} + +static void kiss_write_frame(int fd, const uint8_t *kiss, int len) +{ + uint8_t out[BCPR_MAXFLEN * 2 + 4]; + int o = 0; + int i; + + if (fd < 0 || !kiss || len <= 0) { + return; + } + out[o++] = 0xC0; + for (i = 0; i < len && o < (int)sizeof(out) - 2; i++) { + if (kiss[i] == 0xC0) { + out[o++] = 0xDB; + out[o++] = 0xDC; + } else if (kiss[i] == 0xDB) { + out[o++] = 0xDB; + out[o++] = 0xDD; + } else { + out[o++] = kiss[i]; + } + } + out[o++] = 0xC0; + (void)write(fd, out, (size_t)o); +} + +static bcpr_kiss_pty_t *g_pty; +static int g_npty; + +static void on_rx(int dev_idx, const uint8_t *kiss, int len, void *ud) +{ + int i; + (void)ud; + for (i = 0; i < g_npty; i++) { + if (g_pty[i].idx == dev_idx) { + kiss_write_frame(g_pty[i].master_fd, kiss, len); + return; + } + } +} + +static void kiss_feed(bcpr_engine_t *e, bcpr_kiss_pty_t *kp) +{ + static uint8_t acc[BCPR_MAX_DEVICES][BCPR_MAXFLEN + 8]; + static int alen[BCPR_MAX_DEVICES]; + static int esc[BCPR_MAX_DEVICES]; + static int in_frame[BCPR_MAX_DEVICES]; + uint8_t buf[512]; + ssize_t n; + int i; + int di = kp->idx; + + if (di < 0 || di >= BCPR_MAX_DEVICES) { + return; + } + n = read(kp->master_fd, buf, sizeof(buf)); + if (n <= 0) { + return; + } + for (i = 0; i < (int)n; i++) { + uint8_t b = buf[i]; + if (!in_frame[di]) { + if (b == 0xC0) { + in_frame[di] = 1; + alen[di] = 0; + esc[di] = 0; + } + continue; + } + if (esc[di]) { + esc[di] = 0; + if (b == 0xDC) { + b = 0xC0; + } else if (b == 0xDD) { + b = 0xDB; + } + if (alen[di] < (int)sizeof(acc[di])) { + acc[di][alen[di]++] = b; + } + continue; + } + if (b == 0xDB) { + esc[di] = 1; + continue; + } + if (b == 0xC0) { + if (alen[di] >= 2) { + int q; + int w; + for (w = 0; w < 70; w++) { + q = bcpr_engine_queue_kiss(e, di, acc[di], alen[di]); + if (q == 0) { + break; + } + usleep(50000); + } + if (q != 0) { + fprintf(stderr, + "max25-bcprd: queue_kiss drop bc%d len=%d (busy)\n", + di, alen[di]); + } + } + in_frame[di] = 0; + alen[di] = 0; + continue; + } + if (alen[di] < (int)sizeof(acc[di])) { + acc[di][alen[di]++] = b; + } + } +} + +static void *kiss_thread(void *arg) +{ + bcpr_engine_t *e = (bcpr_engine_t *)arg; + + while (!g_stop && !e->stop) { + struct pollfd pf[BCPR_MAX_DEVICES]; + int nf = 0; + int map[BCPR_MAX_DEVICES]; + int i; + int ret; + int backoff = 0; + + for (i = 0; i < g_npty; i++) { + if (g_pty[i].master_fd < 0) { + continue; + } + pf[nf].fd = g_pty[i].master_fd; + pf[nf].events = POLLIN | POLLHUP | POLLERR; + map[nf] = i; + nf++; + } + if (nf == 0) { + usleep(50000); + continue; + } + ret = poll(pf, (nfds_t)nf, 50); + if (ret <= 0) { + continue; + } + for (i = 0; i < nf; i++) { + short re = pf[i].revents; + if (re & POLLIN) { + kiss_feed(e, &g_pty[map[i]]); + } + if (re & (POLLHUP | POLLERR | POLLNVAL)) { + backoff = 1; + } + } + if (backoff) { + usleep(50000); + } + } + return NULL; +} + +static int ensure_dir(const char *path) +{ + char tmp[256]; + char *p; + size_t len; + + if (!path || !path[0]) { + return -1; + } + snprintf(tmp, sizeof(tmp), "%s", path); + len = strlen(tmp); + if (len == 0) { + return -1; + } + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + (void)mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755) == 0 || errno == EEXIST ? 0 : -1; +} + +int bcpr_daemon_run(bcpr_config_t *cfg, bcpr_kiss_pty_t *ptys, int npty, + bcpr_engine_t *engine, const bcpr_daemon_opts_t *opts) +{ + pthread_t thr; + int thr_ok = 0; + int i; + bcpr_engine_t local_engine; + bcpr_engine_t *e = engine; + int cal_mode; + int seconds; + + if (!cfg || !opts) { + return 1; + } + + g_stop = 0; + g_pty = ptys; + g_npty = npty; + cal_mode = opts->cal_mode; + seconds = opts->seconds; + + signal(SIGINT, on_sig); + signal(SIGTERM, on_sig); + + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + if (opts->cli_txd_bias >= 0) { + cfg->dev[i].txd_bias = opts->cli_txd_bias; + } + if (opts->cli_ptt_wd >= 0) { + cfg->dev[i].ptt_wd = opts->cli_ptt_wd; + } + if (opts->cli_ptt_wd_key_ms > 0) { + cfg->dev[i].ptt_wd_key_ms = opts->cli_ptt_wd_key_ms; + } + if (opts->cli_ptt_wd_pause_ms > 0) { + cfg->dev[i].ptt_wd_pause_ms = opts->cli_ptt_wd_pause_ms; + } + } + + (void)ensure_dir(cfg->state_dir); + + if (!opts->engine_preopened) { + e = &local_engine; + if (bcpr_engine_open(e, cfg) != 0) { + for (i = 0; i < npty; i++) { + bcpr_kiss_pty_close(&ptys[i]); + } + return 1; + } + } + + e->run_seconds = seconds; + if (cal_mode != BCPR_CAL_OFF) { + bcpr_engine_set_cal(e, cal_mode); + } + bcpr_engine_set_rx(e, on_rx, NULL); + + if (npty > 0 && cal_mode == BCPR_CAL_OFF && + pthread_create(&thr, NULL, kiss_thread, e) == 0) { + thr_ok = 1; + } + + if (cal_mode != BCPR_CAL_OFF && !cfg->dry_run) { + fprintf(stderr, + "bcprd: *** WATCH NOW *** cal PTT+tone for %d s — then unkey\n", + seconds); + } + + (void)bcpr_engine_run(e); + + g_stop = 1; + e->stop = 1; + if (thr_ok) { + pthread_join(thr, NULL); + } + bcpr_engine_close(e); + for (i = 0; i < npty; i++) { + bcpr_kiss_pty_close(&ptys[i]); + } + fprintf(stderr, "max25-bcprd: stopped\n"); + return 0; +} diff --git a/stacks/max25-bcpr/src/bcpr_engine.c b/stacks/max25-bcpr/src/bcpr_engine.c new file mode 100644 index 0000000..2367817 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_engine.c @@ -0,0 +1,652 @@ +/* + * Bit-clock engine: RT timer loop drives SER12 + HDLC for up to 2 devices. + * Userspace timing (not kernel hard-IRQ). See NOTICE.md. + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_engine.h" +#include "bcpr/bcpr_uart.h" +#include "bcpr/bcpr_hdlc.h" +#include "bcpr/bcpr_ser12.h" + +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#endif + +typedef struct { + bcpr_engine_t *e; + int idx; +} rx_ctx_t; + +static unsigned now_us(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + /* Full monotonic µs — not nsec-within-second (gap spikes were misread). */ + return (unsigned)(ts.tv_sec * 1000000ull + (unsigned long long)ts.tv_nsec / 1000ull); +} + +/* Idle longer than this resets consecutive-burst counting (new session). */ +#define TOT_SESSION_RESET_US 120000000u + +static void tot_write_trip_file(const bcpr_engine_t *e, const bcpr_device_t *d, + const char *reason) +{ + char path[192]; + FILE *f; + + if (!e || !d || e->cfg.dry_run || e->cfg.state_dir[0] == '\0') { + return; + } + snprintf(path, sizeof(path), "%s/tot-trip-bc%d", e->cfg.state_dir, d->index); + f = fopen(path, "w"); + if (!f) { + return; + } + fprintf(f, + "reason=%s\nburst_total=%d\nconsecutive=%d\ntripped=1\n" + "max_key_ms=%d\nmax_bursts=%d\nmax_consecutive=%d\n", + reason ? reason : "unknown", d->tot_burst_total, d->tot_consecutive, + d->cfg.tot_max_key_ms, d->cfg.tot_max_bursts, + d->cfg.tot_max_consecutive); + fclose(f); +} + +static void tot_trip(bcpr_engine_t *e, bcpr_device_t *d, const char *reason) +{ + if (!d || d->tot_tripped) { + return; + } + d->tot_tripped = 1; + bcpr_ser12_force_unkey(&d->ser12); + bcpr_hdlc_abort_tx(&d->hdlc); + fprintf(stderr, + "bcpr: bc%d TOT TRIP reason=%s bursts=%d consecutive=%d\n", + d->index, reason ? reason : "unknown", d->tot_burst_total, + d->tot_consecutive); + tot_write_trip_file(e, d, reason); +} + +static void tot_on_ptt_rise(bcpr_engine_t *e, bcpr_device_t *d, unsigned now_us) +{ + unsigned gap_us; + + if (!d->cfg.tot_enabled || d->tot_tripped) { + return; + } + d->tot_key_start_us = now_us; + if (d->tot_last_off_us == 0u) { + d->tot_consecutive = 1; + return; + } + gap_us = now_us - d->tot_last_off_us; + if (gap_us >= (unsigned)d->cfg.tot_min_gap_ms * 1000u) { + if (gap_us < TOT_SESSION_RESET_US) { + d->tot_consecutive++; + } else { + d->tot_consecutive = 1; + d->tot_burst_total = 0; + } + } + if (d->tot_consecutive > d->cfg.tot_max_consecutive) { + tot_trip(e, d, "max_consecutive"); + } +} + +static void tot_on_ptt_fall(bcpr_engine_t *e, bcpr_device_t *d, unsigned now_us) +{ + if (!d->cfg.tot_enabled || d->tot_tripped) { + return; + } + d->tot_last_off_us = now_us; + d->tot_burst_total++; + if (d->tot_burst_total >= d->cfg.tot_max_bursts) { + tot_trip(e, d, "max_bursts"); + } +} + +static void tot_check_key_duration(bcpr_engine_t *e, bcpr_device_t *d, + unsigned now_us) +{ + unsigned elapsed_us; + unsigned max_us; + + if (!d->cfg.tot_enabled || d->tot_tripped || !d->ptt_was) { + return; + } + if (d->tot_key_start_us == 0u) { + d->tot_key_start_us = now_us; + return; + } + max_us = (unsigned)d->cfg.tot_max_key_ms * 1000u; + elapsed_us = now_us - d->tot_key_start_us; + if (elapsed_us >= max_us) { + bcpr_ser12_force_unkey(&d->ser12); + bcpr_hdlc_abort_tx(&d->hdlc); + tot_on_ptt_fall(e, d, now_us); + tot_trip(e, d, "max_key"); + } +} + +static void on_frame_ctx(const uint8_t *kiss, int len, void *ud) +{ + rx_ctx_t *ctx = (rx_ctx_t *)ud; + if (ctx && ctx->e && ctx->e->on_rx) { + ctx->e->on_rx(ctx->idx, kiss, len, ctx->e->on_rx_ud); + } +} + +void bcpr_engine_set_rx(bcpr_engine_t *e, bcpr_rx_fn fn, void *ud) +{ + if (!e) { + return; + } + e->on_rx = fn; + e->on_rx_ud = ud; +} + +int bcpr_engine_queue_kiss(bcpr_engine_t *e, int dev_idx, const uint8_t *kiss, + int len) +{ + int i; + static unsigned last_tx_us; + unsigned now; + unsigned elapsed; + + if (!e || !kiss) { + return -1; + } + now = now_us(); + if (last_tx_us != 0u) { + elapsed = now - last_tx_us; + if (elapsed < 1500000u) { + usleep(1500000u - elapsed); + } + } + last_tx_us = now_us(); + for (i = 0; i < e->n; i++) { + if (e->dev[i].index == dev_idx) { + if (e->dev[i].tot_tripped) { + fprintf(stderr, "bcpr: bc%d TOT drop queue_kiss (tripped)\n", + dev_idx); + return -1; + } + return bcpr_hdlc_queue_kiss(&e->dev[i].hdlc, kiss, len); + } + } + return -1; +} + +int bcpr_engine_open(bcpr_engine_t *e, const bcpr_config_t *cfg) +{ + int i; + int n = 0; + + if (!e || !cfg) { + return -1; + } + memset(e, 0, sizeof(*e)); + e->cfg = *cfg; + e->stop = 0; + e->run_seconds = 0; + bcpr_uart_set_dry_run(cfg->dry_run); + + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + bcpr_device_t *d; + bcpr_channel_t ch; + unsigned baud = 1200; + int opt_dcd = 0; + + if (!cfg->dev[i].enabled) { + continue; + } + d = &e->dev[n]; + memset(d, 0, sizeof(*d)); + d->cfg = cfg->dev[i]; + d->index = i; + d->running = 0; + + if (bcpr_lock_acquire(&d->lock, &d->cfg, cfg->dry_run) != 0) { + fprintf(stderr, "bcpr: lock failed for max25e0:bc%d\n", i); + bcpr_engine_close(e); + return -1; + } + + bcpr_ser12_set_mode(&d->ser12, d->cfg.mode, &baud); + if (d->cfg.baud) { + baud = d->cfg.baud; + } + opt_dcd = d->ser12.opt_dcd; + bcpr_ser12_init(&d->ser12, baud, opt_dcd); + bcpr_ser12_set_ptt_wd(&d->ser12, d->cfg.ptt_wd, d->cfg.ptt_wd_key_ms, + d->cfg.ptt_wd_pause_ms); + + ch.tx_delay = d->cfg.tx_delay; + ch.tx_tail = d->cfg.tx_tail; + ch.slottime = d->cfg.slottime; + ch.ppersist = d->cfg.ppersist; + ch.fulldup = d->cfg.fulldup; + bcpr_hdlc_init(&d->hdlc, (int)baud, &ch); + + if (!cfg->dry_run) { + if (bcpr_uart_ioperm(d->cfg.iobase, 1) != 0) { + fprintf(stderr, "bcpr: ioperm failed 0x%x\n", d->cfg.iobase); + bcpr_engine_close(e); + return -1; + } + bcpr_uart_set_divisor(d->cfg.iobase, 115200u / 100u / 8u); + bcpr_uart_open_ser12(d->cfg.iobase); + /* + * txd_bias=steady: assert UART break after open (LCR.SB). + * THR framing cannot hold DC-steady TXD; break ≈ TFPCX +12 V. + * Default remains pulse (Sailer THR 0x00). MCR unchanged. + */ + if (d->cfg.txd_bias == BCPR_TXD_STEADY) { + bcpr_uart_set_break(d->cfg.iobase, 1); + d->break_set = 1; + } + } + d->running = 1; + n++; + } + e->n = n; + if (n == 0) { + fprintf(stderr, "bcpr: no enabled devices in config\n"); + return -1; + } + fprintf(stderr, "bcpr: open max25e0 (%d device%s)%s\n", n, + n == 1 ? "" : "s", cfg->dry_run ? " [dry-run]" : ""); + for (i = 0; i < n; i++) { + const bcpr_device_t *d = &e->dev[i]; + fprintf(stderr, + "bcpr: bc%d ptt_wd=%s key_ms=%d pause_ms=%d txd_bias=%s tot=%s " + "max_key_ms=%d max_consecutive=%d max_bursts=%d\n", + d->index, d->cfg.ptt_wd ? "on" : "off", d->cfg.ptt_wd_key_ms, + d->cfg.ptt_wd_pause_ms, + d->cfg.txd_bias == BCPR_TXD_STEADY ? "steady" : "pulse", + d->cfg.tot_enabled ? "on" : "off", d->cfg.tot_max_key_ms, + d->cfg.tot_max_consecutive, d->cfg.tot_max_bursts); + } + return 0; +} + +void bcpr_engine_close(bcpr_engine_t *e) +{ + int i; + if (!e) { + return; + } + e->stop = 1; + for (i = 0; i < e->n; i++) { + bcpr_device_t *d = &e->dev[i]; + if (d->running && !e->cfg.dry_run) { + if (d->break_set) { + bcpr_uart_set_break(d->cfg.iobase, 0); + d->break_set = 0; + } + bcpr_uart_close_ser12(d->cfg.iobase); + (void)bcpr_uart_ioperm(d->cfg.iobase, 0); + } + bcpr_lock_release(&d->lock); + d->running = 0; + } + e->n = 0; +} + +static void try_rt(void) +{ +#if defined(__linux__) + struct sched_param sp; + cpu_set_t set; + int rc; + memset(&sp, 0, sizeof(sp)); + /* Pin pages — fault during PTT = multi-ms TXD gap → pump collapse. */ + if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) { + fprintf(stderr, "bcpr: mlockall failed errno=%d (page faults risk gaps)\n", + errno); + } + /* Prefer one CPU — migration mid-PTT causes multi-ms gaps. */ + CPU_ZERO(&set); + CPU_SET(0, &set); + if (sched_setaffinity(0, sizeof(set), &set) != 0) { + fprintf(stderr, "bcpr: sched_setaffinity(0) errno=%d\n", errno); + } + /* + * High FIFO while bit-clocking — charge-pump cannot tolerate ms preemption. + * Needs root or CAP_SYS_NICE; log hard if denied (S1++ gaps often follow). + */ + sp.sched_priority = 80; + rc = sched_setscheduler(0, SCHED_FIFO, &sp); + if (rc != 0) { + sp.sched_priority = 50; + rc = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp); + } + if (rc != 0) { + sp.sched_priority = 10; + rc = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp); + } + if (rc != 0) { + fprintf(stderr, + "bcpr: SCHED_FIFO failed errno=%d — expect max_gap multi-ms " + "(need root/CAP_SYS_NICE for bcprd)\n", + errno); + } else { + fprintf(stderr, "bcpr: SCHED_FIFO ok prio=%d cpu0\n", sp.sched_priority); + } +#endif +} + +void bcpr_engine_set_cal(bcpr_engine_t *e, int cal_mode) +{ + int i; + if (!e) { + return; + } + if (cal_mode < BCPR_CAL_OFF || cal_mode > BCPR_CAL_ALT) { + cal_mode = BCPR_CAL_OFF; + } + e->cal_mode = cal_mode; + for (i = 0; i < e->n; i++) { + bcpr_ser12_set_cal(&e->dev[i].ser12, cal_mode); + } +} + +static int64_t now_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; +} + +static void emit_tx_telemetry(const bcpr_engine_t *e, bcpr_device_t *d, + int64_t ptt_off_ns) +{ + char path[192]; + FILE *f; + int64_t dur_ns; + unsigned mean_gap = 0; + double thr_rate = 0.0; + double ptt_ms; + + if (!d || !e) { + return; + } + dur_ns = ptt_off_ns - d->ptt_on_ns; + if (dur_ns < 0) { + dur_ns = 0; + } + ptt_ms = (double)dur_ns / 1.0e6; + if (d->tick_count > 0) { + mean_gap = (unsigned)(d->gap_sum_us / d->tick_count); + } + if (ptt_ms > 0.5) { + thr_rate = (double)d->thr_writes * 1000.0 / ptt_ms; + } + fprintf(stderr, + "bcpr: tx-telemetry bc%d ptt_ms=%.1f thr_writes=%u thr_rate=%.0f " + "max_tick_gap_us=%u mean_gap_us=%u gaps_gt_2x=%u baud_us=%u\n", + d->index, ptt_ms, d->thr_writes, thr_rate, d->max_tick_gap_us, + mean_gap, d->gaps_gt_2x, d->ser12.baud_us); + if (e->cfg.dry_run || e->cfg.state_dir[0] == '\0') { + return; + } + snprintf(path, sizeof(path), "%s/tx-last-bc%d", e->cfg.state_dir, d->index); + f = fopen(path, "w"); + if (!f) { + return; + } + fprintf(f, + "ptt_on_ns=%lld\nptt_off_ns=%lld\nptt_ms=%.1f\nthr_writes=%u\n" + "thr_rate=%.0f\nmax_tick_gap_us=%u\nmean_gap_us=%u\n" + "gaps_gt_2x=%u\nbaud_us=%u\ntick_count=%u\n", + (long long)d->ptt_on_ns, (long long)ptt_off_ns, ptt_ms, + d->thr_writes, thr_rate, d->max_tick_gap_us, mean_gap, + d->gaps_gt_2x, d->ser12.baud_us, d->tick_count); + fclose(f); +} + +static unsigned tx_baud_div(const bcpr_device_t *d) +{ + unsigned baud = d->ser12.baud ? d->ser12.baud : 1200u; + unsigned div = (115200u / 8u) / baud; + return div ? div : 1u; +} + +static void tick_device(bcpr_engine_t *e, bcpr_device_t *d, rx_ctx_t *ctx) +{ + int cts = 0; + int mcr = 0x0d; + int do_thr = 0; + unsigned t = now_us(); + int ptt; + int keyed = d->ptt_was; /* already in TX — keep path minimal for TXD pump */ + + if (!e->cfg.dry_run && !keyed) { + unsigned char msr = bcpr_uart_msr(d->cfg.iobase); + cts = (msr & 0x10) ? 1 : 0; + if (d->ser12.opt_dcd > 0) { + d->hdlc.dcd = (msr & 0x80) ? 1 : 0; + } else if (d->ser12.opt_dcd < 0) { + d->hdlc.dcd = (msr & 0x80) ? 0 : 1; + } + } + + /* S0: tick-gap while PTT keyed (full monotonic µs; unsigned wrap OK). */ + if (d->ptt_was && d->last_tick_us) { + unsigned gap = t - d->last_tick_us; + unsigned lim2; + if (gap > d->max_tick_gap_us) { + d->max_tick_gap_us = gap; + } + d->gap_sum_us += gap; + d->tick_count++; + lim2 = d->ser12.baud_us * 2u; + if (lim2 < 2u) { + lim2 = 2u; + } + if (gap > lim2) { + d->gaps_gt_2x++; + } + } + d->last_tick_us = t; + + bcpr_ser12_tick(&d->ser12, &d->hdlc, cts, &mcr, &do_thr, t); + ptt = d->ser12.ptt_hw ? 1 : 0; + + if (d->tot_tripped) { + bcpr_ser12_force_unkey(&d->ser12); + bcpr_hdlc_abort_tx(&d->hdlc); + ptt = 0; + mcr = 0x0d; + } else { + tot_check_key_duration(e, d, t); + ptt = d->ser12.ptt_hw ? 1 : 0; + } + + if (ptt && !d->ptt_was) { + tot_on_ptt_rise(e, d, t); + d->ptt_on_ns = now_ns(); + d->thr_writes = 0; + d->max_tick_gap_us = 0; + d->gap_sum_us = 0; + d->tick_count = 0; + d->gaps_gt_2x = 0; + d->last_tick_us = t; + d->tx_div_set = 0; + } else if (!ptt && d->ptt_was) { + if (!d->tot_tripped) { + tot_on_ptt_fall(e, d, t); + } + if (!e->cfg.dry_run) { + /* Match baycom_ser_fdx: idle divisor only on PTT fall. */ + bcpr_uart_set_divisor(d->cfg.iobase, 115200u / 100u / 8u); + d->tx_div_set = 0; + } + emit_tx_telemetry(e, d, now_ns()); + } + d->ptt_was = ptt; + + if (!e->cfg.dry_run) { + /* + * S1++: set baud_uartdiv once on PTT rise (kernel ser12_fdx). + * Re-writing divisor every bit toggles DLAB mid-shift → intermittent + * TXD charge-pump starve while MCR RTS still keys (MCR PASS / no RF). + */ + if (ptt && !d->tx_div_set) { + bcpr_uart_set_divisor(d->cfg.iobase, tx_baud_div(d)); + d->tx_div_set = 1; + } + /* Kernel order: THR 0x00 first (charge-pump), then MCR bit+PTT. + * txd_bias=steady: skip THR — break already holds TXD SPACE; + * pulse is Sailer default (framing edges feed BayCom pump). */ + if (d->cfg.txd_bias == BCPR_TXD_STEADY) { + if (!d->break_set) { + bcpr_uart_set_break(d->cfg.iobase, 1); + d->break_set = 1; + } + if (ptt) { + d->thr_writes++; /* count pump-equivalent ticks for telem */ + } + } else { + if (d->break_set) { + bcpr_uart_set_break(d->cfg.iobase, 0); + d->break_set = 0; + } + if (do_thr) { + bcpr_uart_thr00(d->cfg.iobase); + if (ptt) { + d->thr_writes++; + } + } + } + bcpr_uart_mcr(d->cfg.iobase, (unsigned char)mcr); + } + + /* Defer HDLC RX drain while keyed — keeps bit deadline tight (S1+). */ + if (!ptt) { + ctx->e = e; + ctx->idx = d->index; + bcpr_hdlc_receiver(&d->hdlc, on_frame_ctx, ctx); + } +} + +/* Publish Soft-/hard-DCD for RX-before-TX gates (state_dir/dcd-bcN). + * Also refresh rx-activity-bcN whenever Soft-DCD is asserted so smoke/L3 + * can delete-and-rewait without a permanent false miss when dcd flickers. */ +static void publish_dcd_status(const bcpr_engine_t *e) +{ + int i; + char path[192]; + FILE *f; + + if (!e || e->cfg.dry_run || e->cfg.state_dir[0] == '\0') { + return; + } + for (i = 0; i < e->n; i++) { + const bcpr_device_t *d = &e->dev[i]; + int dcd = d->hdlc.dcd ? 1 : 0; + snprintf(path, sizeof(path), "%s/dcd-bc%d", e->cfg.state_dir, d->index); + f = fopen(path, "w"); + if (f) { + fprintf(f, "dcd=%d\n", dcd); + fclose(f); + } + snprintf(path, sizeof(path), "%s/rx-activity-bc%d", e->cfg.state_dir, + d->index); + f = fopen(path, "w"); + if (f) { + /* Latch: dcd=1 → activity; dcd=0 clears so L3 needs live Soft-DCD. */ + fprintf(f, "rx_activity=%d\n", dcd ? 1 : 0); + fclose(f); + } + } +} + +int bcpr_engine_run(bcpr_engine_t *e) +{ + struct timespec next; + rx_ctx_t ctx; + time_t t0; + unsigned period_ns; + unsigned tick = 0; + int64_t bit_deadline_ns = 0; + + if (!e || e->n <= 0) { + return -1; + } + try_rt(); + t0 = time(NULL); + period_ns = e->dev[0].ser12.baud_us * 1000u; + if (period_ns < 100000u) { + period_ns = 833000u; + } + + clock_gettime(CLOCK_MONOTONIC, &next); + while (!e->stop) { + int i; + int any_ptt = 0; + unsigned baud_us = e->dev[0].ser12.baud_us; + + for (i = 0; i < e->n; i++) { + tick_device(e, &e->dev[i], &ctx); + if (e->dev[i].ser12.ptt_hw || e->dev[i].ptt_was) { + any_ptt = 1; + } + } + /* ~100 ms at 1200 baud — skip file I/O while PTT (stretches TXD gaps). */ + if ((++tick % 120u) == 0u && !any_ptt) { + publish_dcd_status(e); + } + + /* + * S1/S1+: while PTT, absolute bit deadline + busy-spin (not + * nanosleep). THRE wait alone stacks with tick work → ~2× baud gaps. + * Idle RX keeps absolute nanosleep schedule. + */ + if (any_ptt && !e->cfg.dry_run) { + int64_t now; + + if (baud_us < 200u) { + baud_us = 200u; + } + now = now_ns(); + if (bit_deadline_ns == 0 || + now > bit_deadline_ns + (int64_t)baud_us * 1000LL * 4) { + /* PTT edge / large slip — resync. */ + bit_deadline_ns = now + (int64_t)baud_us * 1000LL; + } else { + bit_deadline_ns += (int64_t)baud_us * 1000LL; + } + /* + * Pure busy-spin to absolute bit deadline — no nanosleep, no + * wait_thre syscalls (those stacked gaps and starved the pump). + * Target: thr_writes ≈ baud for whole PTT; max_gap < ~2× baud_us. + */ + while (now_ns() < bit_deadline_ns) { + } + clock_gettime(CLOCK_MONOTONIC, &next); + } else { + bit_deadline_ns = 0; + next.tv_nsec += (long)period_ns; + while (next.tv_nsec >= 1000000000L) { + next.tv_nsec -= 1000000000L; + next.tv_sec++; + } + while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, + NULL) == EINTR) { + } + } + if (e->run_seconds > 0 && (time(NULL) - t0) >= e->run_seconds) { + break; + } + } + return 0; +} diff --git a/stacks/max25-bcpr/src/bcpr_hdlc.c b/stacks/max25-bcpr/src/bcpr_hdlc.c new file mode 100644 index 0000000..09c8d32 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_hdlc.c @@ -0,0 +1,293 @@ +#include "bcpr/bcpr_hdlc.h" +#include "bcpr/bcpr_crc.h" + +#include +#include + +static int hbuf_empty(const bcpr_hbuf_t *hb) +{ + return hb->rd == hb->wr; +} + +static int hbuf_full(const bcpr_hbuf_t *hb) +{ + return !((BCPR_HDLC_BUF - 1 + hb->rd - hb->wr) % BCPR_HDLC_BUF); +} + +static void hbuf_put(bcpr_hbuf_t *hb, uint16_t val) +{ + unsigned newp = (hb->wr + 1) % BCPR_HDLC_BUF; + if (newp != hb->rd) { + hb->buf[hb->wr] = val; + hb->wr = newp; + } +} + +static uint16_t hbuf_get(bcpr_hbuf_t *hb) +{ + uint16_t val; + if (hb->rd == hb->wr) { + return 0; + } + val = hb->buf[hb->rd]; + hb->rd = (hb->rd + 1) % BCPR_HDLC_BUF; + return val; +} + +#define tenms_to_2flags(h, tenms) (((tenms) * (h)->bitrate) / 100 / 16) + +void bcpr_hdlc_init(bcpr_hdlc_t *h, int bitrate, const bcpr_channel_t *ch) +{ + memset(h, 0, sizeof(*h)); + h->bitrate = bitrate > 0 ? bitrate : 1200; + if (ch) { + h->ch = *ch; + } else { + h->ch.tx_delay = 35; + h->ch.tx_tail = 2; + h->ch.slottime = 10; + h->ch.ppersist = 40; + h->ch.fulldup = 0; + } + h->slotcnt = 1; + h->rx_bp = h->rx_buffer; + h->tx_bp = h->tx_buffer; +} + +void bcpr_hdlc_putbits(bcpr_hdlc_t *h, unsigned bits) +{ + hbuf_put(&h->rx_hbuf, (uint16_t)(bits & 0xffff)); +} + +unsigned bcpr_hdlc_getbits(bcpr_hdlc_t *h) +{ + if (hbuf_empty(&h->tx_hbuf)) { + h->ptt = 0; + return 0; + } + return hbuf_get(&h->tx_hbuf); +} + +int bcpr_hdlc_ptt(const bcpr_hdlc_t *h) +{ + return h->ptt; +} + +void bcpr_hdlc_abort_tx(bcpr_hdlc_t *h) +{ + if (!h) { + return; + } + h->ptt = 0; + h->tx_state = 0; + h->numflags = 0; + h->tx_bitstream = 0; + h->tx_bitbuf = 0; + h->tx_numbits = 0; + h->tx_len = 0; + h->tx_bp = h->tx_buffer; + h->have_pending = 0; + h->pending_len = 0; + h->tx_hbuf.rd = h->tx_hbuf.wr; +} + +int bcpr_hdlc_queue_kiss(bcpr_hdlc_t *h, const uint8_t *kiss, int len) +{ + if (!kiss || len < 2 || len - 1 > BCPR_MAXFLEN) { + return -1; + } + if (h->have_pending || h->ptt) { + return -1; + } + /* strip KISS command byte */ + memcpy(h->pending, kiss + 1, (size_t)(len - 1)); + h->pending_len = len - 1; + h->have_pending = 1; + return 0; +} + +static int hdlc_rx_add_bytes(bcpr_hdlc_t *h, unsigned bits, int num) +{ + int added = 0; + while (h->rx_state && num >= 8) { + if (h->rx_len >= (int)sizeof(h->rx_buffer)) { + h->rx_state = 0; + return 0; + } + *h->rx_bp++ = (uint8_t)(bits >> (32 - num)); + h->rx_len++; + num -= 8; + added += 8; + } + return added; +} + +static void hdlc_rx_flag(bcpr_hdlc_t *h, + void (*on_frame)(const uint8_t *kiss, int len, void *ud), + void *ud) +{ + uint8_t kiss[BCPR_MAXFLEN + 3]; + int pkt_len; + if (h->rx_len < 4) { + return; + } + if (!bcpr_check_crc_ccitt(h->rx_buffer, h->rx_len)) { + return; + } + pkt_len = h->rx_len - 2 + 1; + kiss[0] = 0; + memcpy(kiss + 1, h->rx_buffer, (size_t)(pkt_len - 1)); + if (on_frame) { + on_frame(kiss, pkt_len, ud); + } +} + +void bcpr_hdlc_receiver(bcpr_hdlc_t *h, + void (*on_frame)(const uint8_t *kiss, int len, void *ud), + void *ud) +{ + int i; + unsigned mask1, mask2, mask3, mask4, mask5, mask6, word; + + while (!hbuf_empty(&h->rx_hbuf)) { + word = hbuf_get(&h->rx_hbuf); + h->bitstream >>= 16; + h->bitstream |= word << 16; + h->bitbuf >>= 16; + h->bitbuf |= word << 16; + h->numbits += 16; + for (i = 15, mask1 = 0x1fc00, mask2 = 0x1fe00, mask3 = 0x0fc00, + mask4 = 0x1f800, mask5 = 0xf800, mask6 = 0xffff; + i >= 0; i--, mask1 <<= 1, mask2 <<= 1, mask3 <<= 1, mask4 <<= 1, + mask5 <<= 1, mask6 = (mask6 << 1) | 1) { + if ((h->bitstream & mask1) == mask1) { + h->rx_state = 0; + } else if ((h->bitstream & mask2) == mask3) { + if (h->rx_state) { + hdlc_rx_add_bytes(h, h->bitbuf << (8 + i), + h->numbits - 8 - i); + hdlc_rx_flag(h, on_frame, ud); + } + h->rx_len = 0; + h->rx_bp = h->rx_buffer; + h->rx_state = 1; + h->numbits = i; + } else if ((h->bitstream & mask4) == mask5) { + h->numbits--; + h->bitbuf = (h->bitbuf & (~mask6)) | + ((h->bitbuf & mask6) << 1); + } + } + h->numbits -= hdlc_rx_add_bytes(h, h->bitbuf, h->numbits); + } +} + +void bcpr_hdlc_transmitter(bcpr_hdlc_t *h) +{ + unsigned mask1, mask2, mask3; + int i; + + for (;;) { + if (h->tx_numbits >= 16) { + if (hbuf_full(&h->tx_hbuf)) { + return; + } + hbuf_put(&h->tx_hbuf, (uint16_t)(h->tx_bitbuf & 0xffff)); + h->tx_bitbuf >>= 16; + h->tx_numbits -= 16; + } + switch (h->tx_state) { + default: + return; + case 0: + case 1: + if (h->numflags) { + h->numflags--; + h->tx_bitbuf |= 0x7e7e << h->tx_numbits; + h->tx_numbits += 16; + break; + } + if (h->tx_state == 1) { + return; + } + if (!h->have_pending) { + int flgs = tenms_to_2flags(h, h->ch.tx_tail); + if (flgs < 2) { + flgs = 2; + } + h->tx_state = 1; + h->numflags = flgs; + break; + } + /* pending[] holds up to BCPR_MAXFLEN; reject only oversize / empty. */ + if (h->pending_len > BCPR_MAXFLEN || h->pending_len < 2) { + h->have_pending = 0; + h->tx_state = 0; + h->numflags = 1; + break; + } + memcpy(h->tx_buffer, h->pending, (size_t)h->pending_len); + h->have_pending = 0; + h->tx_bp = h->tx_buffer; + bcpr_append_crc_ccitt(h->tx_buffer, h->pending_len); + h->tx_len = h->pending_len + 2; + h->tx_state = 2; + h->tx_bitstream = 0; + break; + case 2: + if (!h->tx_len) { + h->tx_state = 0; + h->numflags = 1; + break; + } + h->tx_len--; + h->tx_bitbuf |= *h->tx_bp << h->tx_numbits; + h->tx_bitstream >>= 8; + h->tx_bitstream |= (*h->tx_bp++) << 16; + mask1 = 0x1f000; + mask2 = 0x10000; + mask3 = 0xffffffffu >> (31 - h->tx_numbits); + h->tx_numbits += 8; + for (i = 0; i < 8; + i++, mask1 <<= 1, mask2 <<= 1, mask3 = (mask3 << 1) | 1) { + if ((h->tx_bitstream & mask1) != mask1) { + continue; + } + h->tx_bitstream &= ~mask2; + h->tx_bitbuf = (h->tx_bitbuf & mask3) | + ((h->tx_bitbuf & (~mask3)) << 1); + h->tx_numbits++; + mask3 = (mask3 << 1) | 1; + } + break; + } + } +} + +void bcpr_hdlc_arbitrate(bcpr_hdlc_t *h) +{ + if (h->ptt || !h->have_pending) { + return; + } + if (h->ch.fulldup) { + h->tx_state = 0; + h->numflags = tenms_to_2flags(h, h->ch.tx_delay); + h->tx_bitbuf = h->tx_bitstream = 0; + h->tx_numbits = 0; + bcpr_hdlc_transmitter(h); + h->ptt = 1; + return; + } + if (!h->dcd && (--h->slotcnt <= 0)) { + h->slotcnt = h->ch.slottime; + if ((rand() % 256) > h->ch.ppersist) { + return; + } + h->tx_state = 0; + h->numflags = tenms_to_2flags(h, h->ch.tx_delay); + h->tx_bitbuf = h->tx_bitstream = 0; + h->tx_numbits = 0; + bcpr_hdlc_transmitter(h); + h->ptt = 1; + } +} diff --git a/stacks/max25-bcpr/src/bcpr_kiss.c b/stacks/max25-bcpr/src/bcpr_kiss.c new file mode 100644 index 0000000..495f4d1 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_kiss.c @@ -0,0 +1,127 @@ +/* + * KISS PTY bridge for max25-bcpr (HyBBX attach via kiss_link symlink). + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_kiss.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#elif defined(__FreeBSD__) +#include +#else +#include +#endif + +static int ensure_dir(const char *path) +{ + char tmp[256]; + char *p; + size_t len; + + if (!path || !path[0]) { + return -1; + } + snprintf(tmp, sizeof(tmp), "%s", path); + len = strlen(tmp); + if (len == 0) { + return -1; + } + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + (void)mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755) == 0 || errno == EEXIST ? 0 : -1; +} + +int bcpr_kiss_pty_open(bcpr_kiss_pty_t *kp, int idx, const char *link_path, + const char *state_dir) +{ + int master = -1, slave = -1; + char slave_name[128]; + char dir[128]; + const char *slash; + + memset(kp, 0, sizeof(*kp)); + kp->idx = idx; + kp->master_fd = -1; + kp->slave_fd = -1; + snprintf(kp->link_path, sizeof(kp->link_path), "%s", link_path); + + slash = strrchr(link_path, '/'); + if (slash && slash > link_path) { + size_t n = (size_t)(slash - link_path); + if (n >= sizeof(dir)) { + n = sizeof(dir) - 1; + } + memcpy(dir, link_path, n); + dir[n] = '\0'; + (void)ensure_dir(dir); + } else { + (void)ensure_dir(state_dir); + } + + if (openpty(&master, &slave, slave_name, NULL, NULL) != 0) { + perror("max25-bcpr openpty"); + return -1; + } + if (fchmod(slave, 0660) != 0) { + perror("max25-bcpr fchmod kiss slave"); + } else { + struct group *gr = getgrnam("dialout"); + if (gr == NULL) { + gr = getgrnam("uucp"); + } + if (gr == NULL) { + gr = getgrnam("tty"); + } + if (gr != NULL && fchown(slave, (uid_t)-1, gr->gr_gid) != 0) { + perror("max25-bcpr fchown kiss slave"); + } + } + unlink(link_path); + if (symlink(slave_name, link_path) != 0) { + perror("max25-bcpr symlink kiss_link"); + close(slave); + close(master); + return -1; + } + fcntl(master, F_SETFL, O_NONBLOCK); + kp->master_fd = master; + close(slave); + kp->slave_fd = -1; + fprintf(stderr, "max25-bcpr: max25e0:bc%d KISS → %s -> %s\n", idx, link_path, + slave_name); + return 0; +} + +void bcpr_kiss_pty_close(bcpr_kiss_pty_t *kp) +{ + if (!kp) { + return; + } + if (kp->slave_fd >= 0) { + close(kp->slave_fd); + kp->slave_fd = -1; + } + if (kp->master_fd >= 0) { + close(kp->master_fd); + kp->master_fd = -1; + } + if (kp->link_path[0]) { + unlink(kp->link_path); + } +} diff --git a/stacks/max25-bcpr/src/bcpr_lock.c b/stacks/max25-bcpr/src/bcpr_lock.c new file mode 100644 index 0000000..b179323 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_lock.c @@ -0,0 +1,202 @@ +/* + * Exclusive lock on owned COM ports only (flock + optional setserial uart none). + * Pass real IRQ — refuse mismatch. See vault IRQ/lock contract. + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_lock.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void tty_basename(const char *serial, char *out, size_t out_sz) +{ + const char *p = strrchr(serial, '/'); + p = p ? p + 1 : serial; + snprintf(out, out_sz, "%s", p); +} + +int bcpr_lock_verify_irq(const char *serial, unsigned expect_irq, + unsigned *got_irq, unsigned *got_io) +{ + char name[64]; + char path[128]; + char buf[64]; + FILE *f; + unsigned irq = 0; + unsigned io = 0; + + if (!serial || !serial[0]) { + return -1; + } + tty_basename(serial, name, sizeof(name)); + snprintf(path, sizeof(path), "/sys/class/tty/%s/irq", name); + f = fopen(path, "r"); + if (f) { + if (fgets(buf, sizeof(buf), f)) { + irq = (unsigned)strtoul(buf, NULL, 0); + } + fclose(f); + } else { + /* Fallback: setserial -g (best-effort). */ + char cmd[256]; + FILE *p; + snprintf(cmd, sizeof(cmd), "setserial -g %s 2>/dev/null", serial); + p = popen(cmd, "r"); + if (!p) { + return -1; + } + if (fgets(buf, sizeof(buf), p)) { + char *ip = strstr(buf, "IRQ: "); + char *pp = strstr(buf, "Port: "); + if (ip) { + irq = (unsigned)strtoul(ip + 5, NULL, 0); + } + if (pp) { + io = (unsigned)strtoul(pp + 6, NULL, 0); + } + } + pclose(p); + } + + snprintf(path, sizeof(path), "/sys/class/tty/%s/iomem_base", name); + f = fopen(path, "r"); + if (f) { + if (fgets(buf, sizeof(buf), f)) { + io = (unsigned)strtoul(buf, NULL, 0); + } + fclose(f); + } + + if (got_irq) { + *got_irq = irq; + } + if (got_io) { + *got_io = io; + } + if (expect_irq == 0) { + return -1; /* never invent; refuse zero when required */ + } + if (irq == 0 || irq != expect_irq) { + return -1; + } + return 0; +} + +static int make_lock_path(const bcpr_dev_config_t *dev, char *out, size_t sz) +{ + char name[64]; + const char *dir = "/var/run/max25-bcpr"; + tty_basename(dev->serial, name, sizeof(name)); + if (name[0] == '\0') { + return -1; + } + snprintf(out, sz, "%s/lock-%s", dir, name); + return 0; +} + +int bcpr_lock_acquire(bcpr_port_lock_t *lk, const bcpr_dev_config_t *dev, + int dry_run) +{ + char lockpath[192]; + unsigned got_irq = 0, got_io = 0; + + if (!lk || !dev) { + return -1; + } + memset(lk, 0, sizeof(*lk)); + snprintf(lk->serial, sizeof(lk->serial), "%s", dev->serial); + lk->iobase = dev->iobase; + lk->irq = dev->irq; + lk->lock_fd = -1; + lk->dry_run = dry_run ? 1 : 0; + + if (dry_run) { + return 0; + } + if (!dev->serial[0] || dev->irq == 0 || dev->iobase == 0) { + return -1; + } + if (bcpr_lock_verify_irq(dev->serial, dev->irq, &got_irq, &got_io) != 0) { +#if defined(__FreeBSD__) + /* + * No Linux sysfs/setserial — operator INI is SSoT for iobase/irq + * (see vault ExSys AM1 map). Skip hard fail when probe unavailable. + */ + got_irq = dev->irq; + got_io = dev->iobase; +#else + fprintf(stderr, + "bcpr: IRQ mismatch or unreadable for %s (expect %u got %u)\n", + dev->serial, dev->irq, got_irq); + return -1; +#endif + } + if (got_io && got_io != dev->iobase) { + fprintf(stderr, + "bcpr: iobase mismatch for %s (expect 0x%x got 0x%x)\n", + dev->serial, dev->iobase, got_io); + return -1; + } + + if (make_lock_path(dev, lockpath, sizeof(lockpath)) != 0) { + return -1; + } + (void)mkdir("/var/run/max25-bcpr", 0755); + lk->lock_fd = open(lockpath, O_RDWR | O_CREAT | O_CLOEXEC, 0644); + if (lk->lock_fd < 0) { + perror("bcpr lock open"); + return -1; + } + if (flock(lk->lock_fd, LOCK_EX | LOCK_NB) != 0) { + fprintf(stderr, "bcpr: port %s already locked\n", dev->serial); + close(lk->lock_fd); + lk->lock_fd = -1; + return -1; + } + + /* Release 8250 claim on this COM only so userspace can bit-bang. */ + { + char cmd[256]; + snprintf(cmd, sizeof(cmd), "setserial %s uart none 2>/dev/null", + dev->serial); + if (system(cmd) == 0) { + lk->uart_released = 1; + } + } + return 0; +} + +void bcpr_lock_release(bcpr_port_lock_t *lk) +{ + if (!lk) { + return; + } + if (lk->dry_run) { + memset(lk, 0, sizeof(*lk)); + lk->lock_fd = -1; + return; + } + if (lk->uart_released && lk->serial[0]) { + char cmd[256]; + /* Restore 16550A claim — best effort. */ + snprintf(cmd, sizeof(cmd), + "setserial %s uart 16550A port 0x%x irq %u 2>/dev/null", + lk->serial, lk->iobase, lk->irq); + (void)system(cmd); + lk->uart_released = 0; + } + if (lk->lock_fd >= 0) { + flock(lk->lock_fd, LOCK_UN); + close(lk->lock_fd); + lk->lock_fd = -1; + } + memset(lk, 0, sizeof(*lk)); + lk->lock_fd = -1; +} diff --git a/stacks/max25-bcpr/src/bcpr_runas.c b/stacks/max25-bcpr/src/bcpr_runas.c new file mode 100644 index 0000000..8c930bf --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_runas.c @@ -0,0 +1,81 @@ +/* + * Process identity: max25-bcprd runs unprivileged; init drops from root. + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_runas.h" + +#include +#include +#include +#include +#include +#include + +int bcpr_runas_refuse_root(int dry_run) +{ + if (geteuid() == 0 && !dry_run) { + fprintf(stderr, + "max25-bcprd: refusing to run as root — use max25-bcprd-init " + "(setuid) for live SER12\n"); + return -1; + } + return 0; +} + +int bcpr_runas_drop(const bcpr_config_t *cfg) +{ + struct passwd *pw; + struct group *gr = NULL; + gid_t gid; + uid_t uid; + const char *name; + + if (!cfg || !cfg->run_user[0]) { + fprintf(stderr, "max25-bcpr: [max25-bcpr] user= required for daemon\n"); + return -1; + } + if (geteuid() != 0) { + fprintf(stderr, "max25-bcpr: privilege drop requires euid=0\n"); + return -1; + } + + pw = getpwnam(cfg->run_user); + if (!pw) { + fprintf(stderr, "max25-bcpr: unknown user=%s\n", cfg->run_user); + return -1; + } + uid = pw->pw_uid; + gid = pw->pw_gid; + name = pw->pw_name; + + if (cfg->run_group[0]) { + gr = getgrnam(cfg->run_group); + if (!gr) { + fprintf(stderr, "max25-bcpr: unknown group=%s\n", cfg->run_group); + return -1; + } + gid = gr->gr_gid; + } + + if (initgroups(name, gid) != 0) { + fprintf(stderr, "max25-bcpr: initgroups(%s) failed: %s\n", name, + strerror(errno)); + return -1; + } + if (setgid(gid) != 0) { + fprintf(stderr, "max25-bcpr: setgid failed: %s\n", strerror(errno)); + return -1; + } + if (setuid(uid) != 0) { + fprintf(stderr, "max25-bcpr: setuid failed: %s\n", strerror(errno)); + return -1; + } + if (setuid(0) == 0) { + fprintf(stderr, "max25-bcpr: privilege drop incomplete\n"); + return -1; + } + + fprintf(stderr, "max25-bcpr: running as uid=%u gid=%u (%s)\n", + (unsigned)uid, (unsigned)gid, name); + return 0; +} diff --git a/stacks/max25-bcpr/src/bcpr_ser12.c b/stacks/max25-bcpr/src/bcpr_ser12.c new file mode 100644 index 0000000..32091e0 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_ser12.c @@ -0,0 +1,246 @@ +#include "bcpr/bcpr_ser12.h" + +#include +#include + +void bcpr_ser12_set_mode(bcpr_ser12_t *s, const char *mode, unsigned *baud_out) +{ + unsigned baud = 1200; + if (mode && strncmp(mode, "ser", 3) == 0) { + unsigned n = (unsigned)strtoul(mode + 3, NULL, 10); + if (n >= 3 && n <= 48) { + baud = n * 100u; + } + } + if (baud_out) { + *baud_out = baud; + } + s->baud = baud; + s->baud_us = 1000000u / baud; + if (mode && strchr(mode, '*')) { + s->opt_dcd = 0; + } else if (mode && strchr(mode, '+')) { + s->opt_dcd = -1; + } else { + s->opt_dcd = 1; + } +} + +void bcpr_ser12_init(bcpr_ser12_t *s, unsigned baud, int opt_dcd) +{ + memset(s, 0, sizeof(*s)); + s->baud = baud ? baud : 1200; + s->baud_us = 1000000u / s->baud; + s->opt_dcd = opt_dcd; + s->shreg = 0x10000; + s->dcd_sum0 = 2; + s->dcd_time = 120; + s->cal_mode = BCPR_CAL_OFF; + /* FlexNet SER12.doc defaults until bcpr_ser12_set_ptt_wd(). */ + s->ptt_wd = 1; + s->ptt_wd_key_us = 14500u * 1000u; + s->ptt_wd_pause_us = 500u * 1000u; +} + +void bcpr_ser12_set_ptt_wd(bcpr_ser12_t *s, int enable, int key_ms, int pause_ms) +{ + if (!s) { + return; + } + s->ptt_wd = enable ? 1 : 0; + if (key_ms < 1000) { + key_ms = 1000; + } + if (pause_ms < 50) { + pause_ms = 50; + } + s->ptt_wd_key_us = (unsigned)key_ms * 1000u; + s->ptt_wd_pause_us = (unsigned)pause_ms * 1000u; + s->ptt_wd_pausing = 0; + s->ptt_wd_phase_start_us = 0; +} + +void bcpr_ser12_force_unkey(bcpr_ser12_t *s) +{ + if (!s) { + return; + } + s->ptt_hw = 0; + s->ptt_wd_pausing = 0; + s->ptt_wd_phase_start_us = 0; + s->txshreg = 0; +} + +void bcpr_ser12_set_cal(bcpr_ser12_t *s, int cal_mode) +{ + if (!s) { + return; + } + if (cal_mode < BCPR_CAL_OFF || cal_mode > BCPR_CAL_ALT) { + cal_mode = BCPR_CAL_OFF; + } + s->cal_mode = cal_mode; + if (cal_mode != BCPR_CAL_OFF) { + s->ptt_hw = 1; + if (cal_mode == BCPR_CAL_HIGH) { + s->tx_bit = 1; + } else if (cal_mode == BCPR_CAL_LOW) { + s->tx_bit = 0; + } else { + s->tx_bit = 0; /* alt starts low, toggles each tick */ + } + s->txshreg = 1; + s->ptt_wd_pausing = 0; + s->ptt_wd_phase_start_us = 0; + } +} + +/* + * FlexNet SER12.doc / PAR96: during long calibrate, drop PTT every ~14.5 s for + * ~500 ms so the radio PTT watchdog can discharge. Mirror for --cal and any + * sustained key. Logical ptt_hw stays 1 (keep baud divisor / telemetry); only + * MCR RTS is cleared for the pause window. + */ +static void ser12_apply_ptt_wd(bcpr_ser12_t *s, int *mcr_out, unsigned now_us) +{ + unsigned elapsed; + + if (!s->ptt_wd || !s->ptt_hw) { + s->ptt_wd_pausing = 0; + s->ptt_wd_phase_start_us = 0; + return; + } + if (s->ptt_wd_phase_start_us == 0) { + s->ptt_wd_phase_start_us = now_us ? now_us : 1u; + s->ptt_wd_pausing = 0; + return; + } + elapsed = now_us - s->ptt_wd_phase_start_us; + if (!s->ptt_wd_pausing) { + if (elapsed >= s->ptt_wd_key_us) { + s->ptt_wd_pausing = 1; + s->ptt_wd_phase_start_us = now_us ? now_us : 1u; + *mcr_out = 0x0d; /* RTS clear — PTT off */ + } + return; + } + /* In pause: force idle MCR regardless of cal/HDLC bit. */ + *mcr_out = 0x0d; + if (elapsed >= s->ptt_wd_pause_us) { + s->ptt_wd_pausing = 0; + s->ptt_wd_phase_start_us = now_us ? now_us : 1u; + /* Next tick restores keyed MCR from cal/HDLC path. */ + } +} + +static void ser12_rx(bcpr_ser12_t *s, bcpr_hdlc_t *h, unsigned curs, + unsigned now_us) +{ + int timediff; + int bdus8 = (int)(s->baud_us >> 3); + int bdus4 = (int)(s->baud_us >> 2); + int bdus2 = (int)(s->baud_us >> 1); + + timediff = (int)(now_us - s->pll_time); + /* wrap handling for us modulo ~1s window */ + while (timediff >= 500000) { + timediff -= 1000000; + } + while (timediff <= -500000) { + timediff += 1000000; + } + while (timediff >= bdus2) { + timediff -= (int)s->baud_us; + s->pll_time += s->baud_us; + s->dcd_time--; + if (s->shreg & 1) { + bcpr_hdlc_putbits(h, (s->shreg >> 1) ^ 0xffffu); + s->shreg = 0x10000; + } + s->shreg >>= 1; + } + if (s->dcd_time <= 0) { + if (!s->opt_dcd) { + h->dcd = (s->dcd_sum0 + s->dcd_sum1 + s->dcd_sum2) < 0; + } + s->dcd_sum2 = s->dcd_sum1; + s->dcd_sum1 = s->dcd_sum0; + s->dcd_sum0 = 2; + s->dcd_time += 120; + } + if (s->last_rxbit != (unsigned char)curs) { + s->last_rxbit = (unsigned char)curs; + s->shreg |= 0x10000; + if (timediff > 0) { + s->pll_time += (unsigned)bdus8; + } else { + s->pll_time += 1000000u - (unsigned)bdus8; + } + if (abs(timediff) > bdus4) { + s->dcd_sum0 += 4; + } else { + s->dcd_sum0--; + } + } + while (s->pll_time >= 1000000u) { + s->pll_time -= 1000000u; + } +} + +void bcpr_ser12_tick(bcpr_ser12_t *s, bcpr_hdlc_t *h, int cts, int *mcr_out, + int *do_thr00, unsigned now_us) +{ + *do_thr00 = 1; + + /* DOS cal.exe: continuous PTT + sticky/toggle DTR; no HDLC. */ + if (s->cal_mode != BCPR_CAL_OFF) { + s->ptt_hw = 1; + if (s->cal_mode == BCPR_CAL_HIGH) { + s->tx_bit = 1; + } else if (s->cal_mode == BCPR_CAL_LOW) { + s->tx_bit = 0; + } else { + s->tx_bit = (unsigned char)!s->tx_bit; + } + *mcr_out = 0x0e | (!!s->tx_bit); + ser12_apply_ptt_wd(s, mcr_out, now_us); + return; + } + + /* + * While PTT: skip Soft-DCD RX PLL — PC-COM charge-pump needs unbroken + * THR 0x00 @ baud; RX work causes multi-ms TXD gaps → underpowered AFSK. + */ + if (!s->ptt_hw) { + ser12_rx(s, h, cts ? 1u : 0u, now_us); + } + + if (s->ptt_hw) { + *mcr_out = 0x0e | (!!s->tx_bit); + } else { + *mcr_out = 0x0d; + } + + if (s->ptt_hw) { + if (s->txshreg <= 1) { + s->txshreg = 0x10000u | bcpr_hdlc_getbits(h); + if (!bcpr_hdlc_ptt(h)) { + s->ptt_hw = 0; + *mcr_out = 0x0d; + ser12_apply_ptt_wd(s, mcr_out, now_us); + return; + } + } + s->tx_bit = (unsigned char)(!(s->tx_bit ^ (s->txshreg & 1))); + s->txshreg >>= 1; + } else { + bcpr_hdlc_arbitrate(h); + if (bcpr_hdlc_ptt(h)) { + s->txshreg = 1; + s->ptt_hw = 1; + } + } + bcpr_hdlc_transmitter(h); + ser12_apply_ptt_wd(s, mcr_out, now_us); + /* Receiver drain is owned by bcpr_engine (RX callback). */ +} diff --git a/stacks/max25-bcpr/src/bcpr_uart.c b/stacks/max25-bcpr/src/bcpr_uart.c new file mode 100644 index 0000000..22ccba3 --- /dev/null +++ b/stacks/max25-bcpr/src/bcpr_uart.c @@ -0,0 +1,251 @@ +/* + * Userspace 8250/16550 register access for SER12 bit-bang. + * Prefer ioperm(2); fall back to /dev/port. Dry-run: no-ops. + * Algorithms match Linux baycom_ser_fdx (see NOTICE.md). + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_uart.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#elif defined(__FreeBSD__) +#include +#include +#endif + +#if defined(__FreeBSD__) +static int bcpr_freebsd_set_ioperm(unsigned start, unsigned len, int enable) +{ + struct { + unsigned int start; + unsigned int length; + int enable; + } args; + + args.start = start; + args.length = len; + args.enable = enable; + return sysarch(I386_SET_IOPERM, &args); +} +#endif + +static int g_dry; +static int g_port_fd = -1; +static int g_ioperm_ok; + +void bcpr_uart_set_dry_run(int on) +{ + g_dry = on ? 1 : 0; +} + +int bcpr_uart_ioperm(unsigned iobase, int on) +{ + if (g_dry) { + return 0; + } +#if defined(__linux__) + if (ioperm((unsigned long)iobase, 8, on ? 1 : 0) == 0) { + g_ioperm_ok = on ? 1 : 0; + return 0; + } +#elif defined(__FreeBSD__) + /* + * Prefer /dev/io (process-wide inb/outb). sysarch range is fallback. + * ExSys port: disable matching uart(4) unit (e.g. uart2/cuau2) in + * loader.conf — kernel uart + userspace inb on same iobase → SIGBUS. + */ + if (on) { + if (g_port_fd < 0) { + g_port_fd = open("/dev/io", O_RDWR | O_CLOEXEC); + } + if (g_port_fd >= 0) { + g_ioperm_ok = 1; + return 0; + } + } + if (bcpr_freebsd_set_ioperm((unsigned)iobase, 8, on ? 1 : 0) == 0) { + g_ioperm_ok = on ? 1 : 0; + return 0; + } +#endif + /* Fallback: /dev/io (FreeBSD) or /dev/port (Linux) — needs root. */ + if (on) { + if (g_port_fd < 0) { +#if defined(__FreeBSD__) + g_port_fd = open("/dev/io", O_RDWR | O_CLOEXEC); +#else + g_port_fd = open("/dev/port", O_RDWR | O_CLOEXEC); +#endif + if (g_port_fd >= 0) { + g_ioperm_ok = 1; + return 0; + } + } + if (g_port_fd >= 0) { + return 0; + } + return -1; + } + if (g_port_fd >= 0) { + close(g_port_fd); + g_port_fd = -1; + } + g_ioperm_ok = 0; + return 0; +} + +static int port_rw(unsigned port, int do_write, unsigned char *val) +{ + off_t off = (off_t)port; + if (g_port_fd < 0) { + return -1; + } + if (lseek(g_port_fd, off, SEEK_SET) != off) { + return -1; + } + if (do_write) { + return (write(g_port_fd, val, 1) == 1) ? 0 : -1; + } + return (read(g_port_fd, val, 1) == 1) ? 0 : -1; +} + +void bcpr_uart_outb(unsigned char val, unsigned port) +{ + if (g_dry) { + return; + } + if (g_ioperm_ok) { + outb(val, port); + return; + } + (void)port_rw(port, 1, &val); +} + +unsigned char bcpr_uart_inb(unsigned port) +{ + unsigned char v = 0; + if (g_dry) { + return 0; + } + if (g_ioperm_ok) { + return inb(port); + } + (void)port_rw(port, 0, &v); + return v; +} + +void bcpr_uart_set_divisor(unsigned iobase, unsigned divisor) +{ + unsigned char lcr; + if (g_dry) { + return; + } + lcr = bcpr_uart_inb(iobase + 3); + bcpr_uart_outb((unsigned char)(lcr | 0x80), iobase + 3); /* DLAB */ + bcpr_uart_outb((unsigned char)(divisor & 0xff), iobase + 0); + bcpr_uart_outb((unsigned char)((divisor >> 8) & 0xff), iobase + 1); + bcpr_uart_outb((unsigned char)(lcr & 0x7f), iobase + 3); +} + +void bcpr_uart_open_ser12(unsigned iobase) +{ + if (g_dry) { + return; + } + /* Match baycom_ser_fdx open: FIFO off, 6-bit word, IER THRE+MSR. */ + bcpr_uart_outb(0x00, iobase + 2); /* FCR */ + bcpr_uart_outb(0x01, iobase + 3); /* LCR 6N1 */ + bcpr_uart_outb(0x0a, iobase + 1); /* IER */ + bcpr_uart_outb(0x0d, iobase + 4); /* MCR idle */ + bcpr_uart_thr00(iobase); +} + +void bcpr_uart_close_ser12(unsigned iobase) +{ + if (g_dry) { + return; + } + bcpr_uart_outb(0x00, iobase + 1); /* IER off */ + bcpr_uart_outb(0x01, iobase + 4); /* MCR close */ +} + +unsigned char bcpr_uart_msr(unsigned iobase) +{ + if (g_dry) { + return 0; + } + return bcpr_uart_inb(iobase + 6); +} + +void bcpr_uart_mcr(unsigned iobase, unsigned char v) +{ + if (g_dry) { + return; + } + bcpr_uart_outb(v, iobase + 4); +} + +void bcpr_uart_thr00(unsigned iobase) +{ + if (g_dry) { + return; + } + bcpr_uart_outb(0x00, iobase + 0); +} + +void bcpr_uart_set_break(unsigned iobase, int on) +{ + unsigned char lcr; + if (g_dry) { + return; + } + /* + * 8250 LCR bit6 (Set Break): force TXD to continuous SPACE (RS-232 +V). + * Honest limit: THR writes alone cannot hold DC-steady TXD — each byte is + * framed (start/data/stop). Break is the practical TFPCX-class approximation + * (docs: static ≈ +12 V modem supply). Does not change MCR/PTT. + */ + lcr = bcpr_uart_inb(iobase + 3); + if (on) { + bcpr_uart_outb((unsigned char)(lcr | 0x40), iobase + 3); + } else { + bcpr_uart_outb((unsigned char)(lcr & (unsigned char)~0x40), iobase + 3); + } +} + +int bcpr_uart_wait_thre(unsigned iobase, unsigned timeout_us) +{ + struct timespec t0, now; + unsigned char lsr; + + if (g_dry) { + return 1; + } + if (clock_gettime(CLOCK_MONOTONIC, &t0) != 0) { + return 0; + } + for (;;) { + lsr = bcpr_uart_inb(iobase + 5); + if (lsr & 0x20) { /* THRE */ + return 1; + } + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + return 0; + } + { + long elapsed = (long)(now.tv_sec - t0.tv_sec) * 1000000L + + (now.tv_nsec - t0.tv_nsec) / 1000L; + if (elapsed >= (long)timeout_us) { + return 0; + } + } + } +} diff --git a/stacks/max25-bcpr/src/max25-bcprd-init.c b/stacks/max25-bcpr/src/max25-bcprd-init.c new file mode 100644 index 0000000..6a02240 --- /dev/null +++ b/stacks/max25-bcpr/src/max25-bcprd-init.c @@ -0,0 +1,255 @@ +/* + * max25-bcprd-init — privileged SER12 setup (setuid root), then unprivileged daemon. + * Fork keeps ioperm in child; parent writes pidfile and exits. + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_config.h" +#include "bcpr/bcpr_daemon.h" +#include "bcpr/bcpr_engine.h" +#include "bcpr/bcpr_kiss.h" +#include "bcpr/bcpr_runas.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void usage(const char *argv0) +{ + fprintf(stderr, + "Usage: %s -c [--once] [--seconds N]\n" + " [--cal high|low|alt] [--txd-bias pulse|steady]\n" + " [--ptt-wd|--no-ptt-wd] [--ptt-wd-key-ms N]\n" + " [--ptt-wd-pause-ms N] [--version]\n" + " Privileged init for live SER12 — drops to [max25-bcpr] user= after setup.\n" + " Dry-run: use max25-bcprd --dry-run (unprivileged).\n", + argv0); +} + +static int ensure_dir(const char *path) +{ + char tmp[256]; + char *p; + size_t len; + + if (!path || !path[0]) { + return -1; + } + snprintf(tmp, sizeof(tmp), "%s", path); + len = strlen(tmp); + if (len == 0) { + return -1; + } + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + (void)mkdir(tmp, 0755); + *p = '/'; + } + } + return mkdir(tmp, 0755) == 0 || errno == EEXIST ? 0 : -1; +} + +static int write_pidfile(const char *path, pid_t pid) +{ + FILE *f; + + f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "max25-bcprd-init: cannot write pidfile %s\n", path); + return -1; + } + fprintf(f, "%d\n", (int)pid); + fclose(f); + return 0; +} + +int main(int argc, char **argv) +{ + const char *cfg_path = NULL; + int seconds = 0; + int cal_mode = BCPR_CAL_OFF; + int cli_txd_bias = -1; + int cli_ptt_wd = -1; + int cli_ptt_wd_key_ms = -1; + int cli_ptt_wd_pause_ms = -1; + int i; + bcpr_config_t cfg; + bcpr_engine_t engine; + bcpr_kiss_pty_t ptys[BCPR_MAX_DEVICES]; + int npty = 0; + char pidpath[256]; + pid_t child; + bcpr_daemon_opts_t opts; + + for (i = 1; i < argc; i++) { + if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) && + i + 1 < argc) { + cfg_path = argv[++i]; + } else if (strcmp(argv[i], "--once") == 0) { + if (seconds <= 0) { + seconds = 1; + } + } else if (strcmp(argv[i], "--seconds") == 0 && i + 1 < argc) { + seconds = atoi(argv[++i]); + } else if (strcmp(argv[i], "--cal") == 0 && i + 1 < argc) { + const char *m = argv[++i]; + if (strcmp(m, "high") == 0 || strcmp(m, "1") == 0) { + cal_mode = BCPR_CAL_HIGH; + } else if (strcmp(m, "low") == 0 || strcmp(m, "2") == 0) { + cal_mode = BCPR_CAL_LOW; + } else if (strcmp(m, "alt") == 0 || strcmp(m, "3") == 0) { + cal_mode = BCPR_CAL_ALT; + } else { + fprintf(stderr, "max25-bcprd-init: --cal needs high|low|alt\n"); + return 2; + } + } else if (strcmp(argv[i], "--txd-bias") == 0 && i + 1 < argc) { + const char *m = argv[++i]; + if (strcasecmp(m, "pulse") == 0 || strcasecmp(m, "sailer") == 0) { + cli_txd_bias = BCPR_TXD_PULSE; + } else if (strcasecmp(m, "steady") == 0 || + strcasecmp(m, "tfpcx") == 0 || + strcasecmp(m, "break") == 0) { + cli_txd_bias = BCPR_TXD_STEADY; + } else { + fprintf(stderr, "max25-bcprd-init: --txd-bias needs pulse|steady\n"); + return 2; + } + } else if (strcmp(argv[i], "--ptt-wd") == 0) { + cli_ptt_wd = 1; + } else if (strcmp(argv[i], "--no-ptt-wd") == 0) { + cli_ptt_wd = 0; + } else if (strcmp(argv[i], "--ptt-wd-key-ms") == 0 && i + 1 < argc) { + cli_ptt_wd_key_ms = atoi(argv[++i]); + } else if (strcmp(argv[i], "--ptt-wd-pause-ms") == 0 && + i + 1 < argc) { + cli_ptt_wd_pause_ms = atoi(argv[++i]); + } else if (strcmp(argv[i], "--version") == 0 || + strcmp(argv[i], "-V") == 0) { + printf("bcprd-init 0.1.0\n"); + return 0; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + return 0; + } else { + usage(argv[0]); + return 2; + } + } + if (!cfg_path) { + usage(argv[0]); + return 2; + } + if (geteuid() != 0) { + fprintf(stderr, + "max25-bcprd-init: requires root (setuid) for SER12 lock/ioperm/KISS\n"); + return 1; + } + if (cal_mode != BCPR_CAL_OFF && seconds <= 0) { + seconds = 5; + } + + if (bcpr_config_load(&cfg, cfg_path) != 0) { + fprintf(stderr, "max25-bcprd-init: cannot load %s\n", cfg_path); + return 1; + } + if (cfg.dry_run) { + fprintf(stderr, + "max25-bcprd-init: dry_run=yes — use max25-bcprd --dry-run instead\n"); + return 1; + } + if (!cfg.run_user[0]) { + fprintf(stderr, + "max25-bcprd-init: [max25-bcpr] user= required for privilege drop\n"); + return 1; + } + + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + if (cli_txd_bias >= 0) { + cfg.dev[i].txd_bias = cli_txd_bias; + } + if (cli_ptt_wd >= 0) { + cfg.dev[i].ptt_wd = cli_ptt_wd; + } + if (cli_ptt_wd_key_ms > 0) { + cfg.dev[i].ptt_wd_key_ms = cli_ptt_wd_key_ms; + } + if (cli_ptt_wd_pause_ms > 0) { + cfg.dev[i].ptt_wd_pause_ms = cli_ptt_wd_pause_ms; + } + } + + (void)ensure_dir(cfg.state_dir); + snprintf(pidpath, sizeof(pidpath), "%s/max25-bcprd.pid", cfg.state_dir); + + if (cal_mode == BCPR_CAL_OFF) { + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + if (!cfg.dev[i].enabled) { + continue; + } + if (bcpr_kiss_pty_open(&ptys[npty], i, cfg.dev[i].kiss_link, + cfg.state_dir) != 0) { + while (npty > 0) { + npty--; + bcpr_kiss_pty_close(&ptys[npty]); + } + return 1; + } + npty++; + } + } + + if (bcpr_engine_open(&engine, &cfg) != 0) { + for (i = 0; i < npty; i++) { + bcpr_kiss_pty_close(&ptys[i]); + } + return 1; + } + + child = fork(); + if (child < 0) { + perror("max25-bcprd-init fork"); + bcpr_engine_close(&engine); + for (i = 0; i < npty; i++) { + bcpr_kiss_pty_close(&ptys[i]); + } + return 1; + } + if (child > 0) { + if (write_pidfile(pidpath, child) != 0) { + kill(child, SIGTERM); + waitpid(child, NULL, 0); + return 1; + } + _exit(0); + } + + if (setsid() < 0) { + perror("max25-bcprd-init setsid"); + } + + if (bcpr_runas_drop(&cfg) != 0) { + _exit(1); + } + + memset(&opts, 0, sizeof(opts)); + opts.seconds = seconds; + opts.cal_mode = cal_mode; + opts.cli_txd_bias = cli_txd_bias; + opts.cli_ptt_wd = cli_ptt_wd; + opts.cli_ptt_wd_key_ms = cli_ptt_wd_key_ms; + opts.cli_ptt_wd_pause_ms = cli_ptt_wd_pause_ms; + opts.engine_preopened = 1; + + return bcpr_daemon_run(&cfg, ptys, npty, &engine, &opts); +} diff --git a/stacks/max25-bcpr/src/max25-bcprd.c b/stacks/max25-bcpr/src/max25-bcprd.c new file mode 100644 index 0000000..084c5df --- /dev/null +++ b/stacks/max25-bcpr/src/max25-bcprd.c @@ -0,0 +1,205 @@ +/* + * max25-bcprd — BayCom/based SER12 userspace daemon (unprivileged live path). + * Live SER12: max25-bcprd-init (setuid). Dry-run / offline: this binary. + */ +#define _GNU_SOURCE +#include "bcpr/bcpr_config.h" +#include "bcpr/bcpr_daemon.h" +#include "bcpr/bcpr_kiss.h" +#include "bcpr/bcpr_runas.h" + +#include +#include +#include +#include +#include + +static void usage(const char *argv0) +{ + fprintf(stderr, + "Usage: %s -c [--dry-run] [--once] [--seconds N]\n" + " [--cal high|low|alt] [--txd-bias pulse|steady]\n" + " [--ptt-wd|--no-ptt-wd] [--ptt-wd-key-ms N]\n" + " [--ptt-wd-pause-ms N] [--version]\n" + " Host face: max25e0:bc0 / bc1 (KISS PTY via kiss_link)\n" + " Live SER12: max25-bcprd-init (setuid) — this binary refuses root.\n" + " --cal: continuous SER12 tone+PTT (use init for live hardware)\n", + argv0); +} + +int main(int argc, char **argv) +{ + const char *cfg_path = NULL; + int dry = 0; + int seconds = 0; + int cal_mode = BCPR_CAL_OFF; + int cli_txd_bias = -1; + int cli_ptt_wd = -1; + int cli_ptt_wd_key_ms = -1; + int cli_ptt_wd_pause_ms = -1; + int i; + bcpr_config_t cfg; + bcpr_kiss_pty_t ptys[BCPR_MAX_DEVICES]; + int npty = 0; + bcpr_daemon_opts_t opts; + char ver[32] = "0.1.0"; + + for (i = 1; i < argc; i++) { + if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) && + i + 1 < argc) { + cfg_path = argv[++i]; + } else if (strcmp(argv[i], "--dry-run") == 0) { + dry = 1; + } else if (strcmp(argv[i], "--once") == 0) { + if (seconds <= 0) { + seconds = 1; + } + } else if (strcmp(argv[i], "--seconds") == 0 && i + 1 < argc) { + seconds = atoi(argv[++i]); + } else if (strcmp(argv[i], "--cal") == 0 && i + 1 < argc) { + const char *m = argv[++i]; + if (strcmp(m, "high") == 0 || strcmp(m, "1") == 0) { + cal_mode = BCPR_CAL_HIGH; + } else if (strcmp(m, "low") == 0 || strcmp(m, "2") == 0) { + cal_mode = BCPR_CAL_LOW; + } else if (strcmp(m, "alt") == 0 || strcmp(m, "3") == 0) { + cal_mode = BCPR_CAL_ALT; + } else { + fprintf(stderr, "max25-bcprd: --cal needs high|low|alt\n"); + return 2; + } + } else if (strcmp(argv[i], "--txd-bias") == 0 && i + 1 < argc) { + const char *m = argv[++i]; + if (strcasecmp(m, "pulse") == 0 || strcasecmp(m, "sailer") == 0) { + cli_txd_bias = BCPR_TXD_PULSE; + } else if (strcasecmp(m, "steady") == 0 || + strcasecmp(m, "tfpcx") == 0 || + strcasecmp(m, "break") == 0) { + cli_txd_bias = BCPR_TXD_STEADY; + } else { + fprintf(stderr, "max25-bcprd: --txd-bias needs pulse|steady\n"); + return 2; + } + } else if (strcmp(argv[i], "--ptt-wd") == 0) { + cli_ptt_wd = 1; + } else if (strcmp(argv[i], "--no-ptt-wd") == 0) { + cli_ptt_wd = 0; + } else if (strcmp(argv[i], "--ptt-wd-key-ms") == 0 && i + 1 < argc) { + cli_ptt_wd_key_ms = atoi(argv[++i]); + } else if (strcmp(argv[i], "--ptt-wd-pause-ms") == 0 && + i + 1 < argc) { + cli_ptt_wd_pause_ms = atoi(argv[++i]); + } else if (strcmp(argv[i], "--version") == 0 || + strcmp(argv[i], "-V") == 0) { + FILE *vf = fopen("/usr/local/share/max25/max25-bcpr/VERSION", "r"); + if (!vf) { + vf = fopen("stacks/max25-bcpr/VERSION", "r"); + } + if (vf) { + if (fgets(ver, sizeof(ver), vf)) { + ver[strcspn(ver, "\r\n")] = '\0'; + } + fclose(vf); + } + printf("bcprd %s\n", ver); + return 0; + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + usage(argv[0]); + return 0; + } else { + usage(argv[0]); + return 2; + } + } + if (!cfg_path) { + usage(argv[0]); + return 2; + } + if (cal_mode != BCPR_CAL_OFF && seconds <= 0) { + seconds = 5; + } + + if (bcpr_config_load(&cfg, cfg_path) != 0) { + fprintf(stderr, "max25-bcprd: cannot load %s\n", cfg_path); + return 1; + } + if (dry) { + cfg.dry_run = 1; + } + + if (bcpr_runas_refuse_root(cfg.dry_run) != 0) { + return 1; + } + if (!cfg.dry_run && cal_mode == BCPR_CAL_OFF) { + fprintf(stderr, + "max25-bcprd: live SER12 requires max25-bcprd-init — " + "not this binary alone\n"); + return 1; + } + + { + char self[512]; + ssize_t n = readlink("/proc/self/exe", self, sizeof(self) - 1); + FILE *vf = fopen("/usr/local/share/bcpr/VERSION", "r"); + if (!vf) { + vf = fopen("stacks/max25-bcpr/VERSION", "r"); + } + ver[0] = '\0'; + if (vf) { + if (fgets(ver, sizeof(ver), vf)) { + ver[strcspn(ver, "\r\n")] = '\0'; + } + fclose(vf); + } + if (n > 0) { + self[n] = '\0'; + fprintf(stderr, "max25-bcprd: path=%s version=%s\n", self, + ver[0] ? ver : "0.1.0"); + } else { + fprintf(stderr, "max25-bcprd: version=%s\n", ver[0] ? ver : "0.1.0"); + } + } + + if (cfg.dry_run) { + fprintf(stderr, "max25-bcprd: dry-run (no KISS PTY, no UART I/O)\n"); + } else if (cal_mode == BCPR_CAL_OFF) { + for (i = 0; i < BCPR_MAX_DEVICES; i++) { + if (!cfg.dev[i].enabled) { + continue; + } + if (bcpr_kiss_pty_open(&ptys[npty], i, cfg.dev[i].kiss_link, + cfg.state_dir) != 0) { + while (npty > 0) { + npty--; + bcpr_kiss_pty_close(&ptys[npty]); + } + return 1; + } + npty++; + } + } + if (cal_mode != BCPR_CAL_OFF) { + static const char *cal_names[] = {"off", "high", "low", "alt"}; + fprintf(stderr, + "max25-bcprd: CAL mode=%s seconds=%d (no KISS; SER12 tone+PTT)%s\n", + cal_names[cal_mode], seconds, + cfg.dry_run ? " [dry-run]" : ""); + if (!cfg.dry_run) { + fprintf(stderr, + "max25-bcprd: live cal needs max25-bcprd-init for hardware access\n"); + return 1; + } + } + + memset(&opts, 0, sizeof(opts)); + opts.dry_run = cfg.dry_run; + opts.seconds = seconds; + opts.cal_mode = cal_mode; + opts.cli_txd_bias = cli_txd_bias; + opts.cli_ptt_wd = cli_ptt_wd; + opts.cli_ptt_wd_key_ms = cli_ptt_wd_key_ms; + opts.cli_ptt_wd_pause_ms = cli_ptt_wd_pause_ms; + opts.engine_preopened = 0; + + return bcpr_daemon_run(&cfg, ptys, npty, NULL, &opts); +} -- cgit v1.3.1