diff options
Diffstat (limited to 'firmware/c1224/src')
| -rw-r--r-- | firmware/c1224/src/common/afsk_bitbang.c | 116 | ||||
| -rw-r--r-- | firmware/c1224/src/common/banner.c | 82 | ||||
| -rw-r--r-- | firmware/c1224/src/common/baud_config.c | 14 | ||||
| -rw-r--r-- | firmware/c1224/src/common/board_pins.c | 45 | ||||
| -rw-r--r-- | firmware/c1224/src/common/config.c | 422 | ||||
| -rw-r--r-- | firmware/c1224/src/common/crystal_steer.c | 20 | ||||
| -rw-r--r-- | firmware/c1224/src/common/display.c | 187 | ||||
| -rw-r--r-- | firmware/c1224/src/common/hdlc.c | 192 | ||||
| -rw-r--r-- | firmware/c1224/src/common/kiss.c | 98 | ||||
| -rw-r--r-- | firmware/c1224/src/common/main.c | 61 | ||||
| -rw-r--r-- | firmware/c1224/src/common/modem_probe.c | 38 | ||||
| -rw-r--r-- | firmware/c1224/src/common/ptt.c | 31 | ||||
| -rw-r--r-- | firmware/c1224/src/common/sense.c | 99 | ||||
| -rw-r--r-- | firmware/c1224/src/common/status.c | 78 | ||||
| -rw-r--r-- | firmware/c1224/src/common/usb_cmd.c | 268 |
15 files changed, 1751 insertions, 0 deletions
diff --git a/firmware/c1224/src/common/afsk_bitbang.c b/firmware/c1224/src/common/afsk_bitbang.c new file mode 100644 index 0000000..7019100 --- /dev/null +++ b/firmware/c1224/src/common/afsk_bitbang.c @@ -0,0 +1,116 @@ +/* + * Timed NRZI bitbang to TCM3105 TXD/RXD — v1.0-prototype best effort. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "afsk_bitbang.h" +#include "hdlc.h" +#include "board_pins.h" +#include "ptt.h" +#include "status.h" +#include "hardware/gpio.h" +#include "pico/stdlib.h" +#include <string.h> + +#define BIT_US (1000000u / (unsigned)C1224_BITRATE) +#define TX_BITS_MAX ((HDLC_MAX_INFO + 4) * 10 + 64) +#define RX_IDLE_MARK true + +static uint8_t g_tx_bits[TX_BITS_MAX]; +static volatile bool g_tx_busy; +static bool g_nrzi_level; +static hdlc_rx_t g_rx; +static bool g_rx_nrzi; +static absolute_time_t g_next_sample; +static afsk_rx_frame_fn g_rx_cb; +static uint8_t g_rx_out[HDLC_MAX_INFO]; + +void afsk_bitbang_init(void) +{ + gpio_put(PIN_MODEM_TXD, 1); + g_tx_busy = false; + g_nrzi_level = true; + g_rx_nrzi = RX_IDLE_MARK; + hdlc_rx_init(&g_rx); + g_rx_cb = NULL; + g_next_sample = get_absolute_time(); +} + +unsigned afsk_bitrate(void) +{ + return (unsigned)C1224_BITRATE; +} + +bool afsk_tx_busy(void) +{ + return g_tx_busy; +} + +void afsk_rx_set_callback(afsk_rx_frame_fn fn) +{ + g_rx_cb = fn; +} + +static void nrzi_emit(bool data_bit) +{ + /* AX.25 NRZI: 0 = transition, 1 = no transition */ + if (!data_bit) { + g_nrzi_level = !g_nrzi_level; + } + gpio_put(PIN_MODEM_TXD, g_nrzi_level ? 1 : 0); +} + +bool afsk_tx_frame(const uint8_t *info, size_t info_len) +{ + if (g_tx_busy || !info || info_len == 0 || info_len > HDLC_MAX_INFO) { + return false; + } + size_t nbits = hdlc_encode_bits(info, info_len, g_tx_bits, TX_BITS_MAX, 2); + if (nbits == 0) { + return false; + } + + g_tx_busy = true; + status_set(C1224_STATE_SENDING); + status_publish(); + ptt_set(true); + sleep_ms(40); /* key-up settle — prototype */ + + g_nrzi_level = true; + gpio_put(PIN_MODEM_TXD, 1); + + for (size_t i = 0; i < nbits; i++) { + nrzi_emit(g_tx_bits[i] != 0); + busy_wait_us_32(BIT_US); + } + + gpio_put(PIN_MODEM_TXD, 1); + sleep_ms(20); + ptt_set(false); + status_set(C1224_STATE_READY); + status_publish(); + g_tx_busy = false; + return true; +} + +void afsk_rx_poll(void) +{ + if (g_tx_busy) { + return; + } + absolute_time_t now = get_absolute_time(); + if (absolute_time_diff_us(g_next_sample, now) < 0) { + return; + } + g_next_sample = delayed_by_us(now, BIT_US); + + bool level = gpio_get(PIN_MODEM_RXD); + bool data_bit = (level == g_rx_nrzi); /* 1 = no transition */ + if (level != g_rx_nrzi) { + g_rx_nrzi = level; + } + + size_t out_len = 0; + if (hdlc_rx_bit(&g_rx, data_bit, g_rx_out, &out_len) && g_rx_cb && out_len > 0) { + g_rx_cb(g_rx_out, out_len); + } +} diff --git a/firmware/c1224/src/common/banner.c b/firmware/c1224/src/common/banner.c new file mode 100644 index 0000000..59eb5dd --- /dev/null +++ b/firmware/c1224/src/common/banner.c @@ -0,0 +1,82 @@ +/* + * Fixed boot banner — edit source only; not INI / CDC. + * Always printed once after USB CDC ready (main); quiet never suppresses it. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "banner.h" +#include "version.h" +#include "afsk_bitbang.h" +#include <stdio.h> +#include <string.h> + +/* Fixed CDC column width for centered serial look (40–48). */ +#define BANNER_WIDTH 44 + +/** Display columns (UTF-8 code points ≈ 1 col; ASCII-safe). */ +static size_t banner_cols(const char *s) +{ + size_t cols = 0; + while (*s) { + unsigned char c = (unsigned char)*s++; + if ((c & 0x80u) == 0u) { + cols++; + } else if ((c & 0xE0u) == 0xC0u) { + if (*s) { + s++; + } + cols++; + } else if ((c & 0xF0u) == 0xE0u) { + if (*s) { + s++; + } + if (*s) { + s++; + } + cols++; + } else if ((c & 0xF8u) == 0xF0u) { + if (*s) { + s++; + } + if (*s) { + s++; + } + if (*s) { + s++; + } + cols++; + } else { + cols++; + } + } + return cols; +} + +static void banner_line(const char *text) +{ + size_t cols = banner_cols(text); + size_t pad = 0; + if (cols < BANNER_WIDTH) { + pad = (BANNER_WIDTH - cols) / 2u; + } + for (size_t i = 0; i < pad; i++) { + putchar(' '); + } + fputs(text, stdout); + fputs("\r\n", stdout); +} + +void banner_print(void) +{ + /* Fixed boot banner — edit source only */ + char line[BANNER_WIDTH + 8]; + + snprintf(line, sizeof(line), "T-Modem-c1224 %s", C1224_FW_VERSION); + banner_line(line); + + snprintf(line, sizeof(line), "USB KISS Half-TNC · AFSK %u", + (unsigned)afsk_bitrate()); + banner_line(line); + + banner_line("Non-Profit · GNU GPLv3 · AS-IS"); + banner_line("https://github.com/ngteq"); +} diff --git a/firmware/c1224/src/common/baud_config.c b/firmware/c1224/src/common/baud_config.c new file mode 100644 index 0000000..7b7a37f --- /dev/null +++ b/firmware/c1224/src/common/baud_config.c @@ -0,0 +1,14 @@ +/* + * Informational label only — physical rate = stuffed crystal. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "baud_config.h" + +static const baud_crystal_config_t cfg = { + .label = "c1224", +}; + +const baud_crystal_config_t *baud_config_get(void) +{ + return &cfg; +} diff --git a/firmware/c1224/src/common/board_pins.c b/firmware/c1224/src/common/board_pins.c new file mode 100644 index 0000000..a199946 --- /dev/null +++ b/firmware/c1224/src/common/board_pins.c @@ -0,0 +1,45 @@ +/* + * Board GPIO setup — T-Modem-c1224 service Pico. + * Copyright (C) 2026 T-Modem contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "board_pins.h" +#include "hardware/gpio.h" +#include "hardware/i2c.h" + +void board_pins_init(void) +{ + /* Modem digital */ + gpio_init(PIN_MODEM_TXD); + gpio_set_dir(PIN_MODEM_TXD, GPIO_OUT); + gpio_put(PIN_MODEM_TXD, 0); + + gpio_init(PIN_MODEM_RXD); + gpio_set_dir(PIN_MODEM_RXD, GPIO_IN); + gpio_pull_up(PIN_MODEM_RXD); + + gpio_init(PIN_MODEM_CDT); + gpio_set_dir(PIN_MODEM_CDT, GPIO_IN); + /* CDT polarity is chip-dependent; no pull forced beyond weak up for floating unpopulated */ + gpio_pull_up(PIN_MODEM_CDT); + + /* GP14/GP15 mux placeholders — leave unused (single-XTAL v1.0) */ + + /* LEDs */ + gpio_init(PIN_LED_PTT); + gpio_set_dir(PIN_LED_PTT, GPIO_OUT); + gpio_put(PIN_LED_PTT, 0); + gpio_init(PIN_LED_DCD); + gpio_set_dir(PIN_LED_DCD, GPIO_OUT); + gpio_put(PIN_LED_DCD, 0); + gpio_init(PIN_LED_TXRX); + gpio_set_dir(PIN_LED_TXRX, GPIO_OUT); + gpio_put(PIN_LED_TXRX, 0); + + /* I2C for LCD backpack (may be absent — display layer tolerates NACK) */ + i2c_init(i2c0, I2C_LCD_BAUDRATE); + gpio_set_function(PIN_I2C_SDA, GPIO_FUNC_I2C); + gpio_set_function(PIN_I2C_SCL, GPIO_FUNC_I2C); + gpio_pull_up(PIN_I2C_SDA); + gpio_pull_up(PIN_I2C_SCL); +} diff --git a/firmware/c1224/src/common/config.c b/firmware/c1224/src/common/config.c new file mode 100644 index 0000000..70f2420 --- /dev/null +++ b/firmware/c1224/src/common/config.c @@ -0,0 +1,422 @@ +/* + * INI defaults + flash overlay (last 4 KiB of Pico flash). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "config.h" +#include "hardware/flash.h" +#include "hardware/sync.h" +#include "pico/stdlib.h" +#include <stdio.h> +#include <string.h> +#include <ctype.h> +#include <stdlib.h> + +#define CFG_MAGIC0 'C' +#define CFG_MAGIC1 '1' +#define CFG_MAGIC2 '2' +#define CFG_MAGIC3 '4' +#define CFG_VERSION 1 +#define CFG_SECTOR_SIZE 4096 +/* Store config in the last flash sector */ +#define CFG_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - CFG_SECTOR_SIZE) + +typedef struct __attribute__((packed)) { + uint8_t magic[4]; + uint8_t version; + uint8_t reserved; + uint16_t length; + uint16_t crc; + /* uint8_t data[length] follows */ +} cfg_hdr_t; + +static c1224_config_t g_cfg; + +static uint16_t crc16_ccitt(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= (uint16_t)data[i] << 8; + for (int b = 0; b < 8; b++) { + if (crc & 0x8000) { + crc = (uint16_t)((crc << 1) ^ 0x1021); + } else { + crc <<= 1; + } + } + } + return crc; +} + +static void copy_text(char *dst, const char *src) +{ + size_t n = strlen(src); + if (n >= C1224_TEXT_MAX) { + n = C1224_TEXT_MAX - 1; + } + memcpy(dst, src, n); + dst[n] = '\0'; +} + +void config_load_defaults(void) +{ + copy_text(g_cfg.starting, "c1224 is starting"); + copy_text(g_cfg.ready, "c1224 is ready"); + copy_text(g_cfg.fault, "c1224 is fault"); + copy_text(g_cfg.sending, "c1224 is sending"); + copy_text(g_cfg.signal, "c1224 signal"); + copy_text(g_cfg.packet, "c1224 packet"); + copy_text(g_cfg.power, "c1224 power"); + copy_text(g_cfg.idle, "c1224 idle"); + copy_text(g_cfg.callsign, ""); + copy_text(g_cfg.device_id, "c1224"); + g_cfg.quiet = true; + g_cfg.cdt_active_high = false; + g_cfg.packet_hold_ms = 500; + g_cfg.packet_edge_min = 4; + g_cfg.packet_window_ms = 50; +} + +static char *trim(char *s) +{ + while (*s && isspace((unsigned char)*s)) { + s++; + } + char *e = s + strlen(s); + while (e > s && isspace((unsigned char)e[-1])) { + *--e = '\0'; + } + return s; +} + +static bool parse_bool(const char *v, bool *out) +{ + if (!v || !*v) { + return false; + } + if (v[0] == '1' || v[0] == 'y' || v[0] == 'Y' || v[0] == 't' || v[0] == 'T') { + *out = true; + return true; + } + if (v[0] == '0' || v[0] == 'n' || v[0] == 'N' || v[0] == 'f' || v[0] == 'F') { + *out = false; + return true; + } + return false; +} + +bool config_set_kv(const char *key, const char *value) +{ + if (!key || !value) { + return false; + } + char kbuf[32]; + size_t n = strlen(key); + if (n >= sizeof(kbuf)) { + return false; + } + for (size_t i = 0; i <= n; i++) { + kbuf[i] = (char)tolower((unsigned char)key[i]); + } + + if (strcmp(kbuf, "starting") == 0) { + copy_text(g_cfg.starting, value); + return true; + } + if (strcmp(kbuf, "ready") == 0) { + copy_text(g_cfg.ready, value); + return true; + } + if (strcmp(kbuf, "fault") == 0) { + copy_text(g_cfg.fault, value); + return true; + } + if (strcmp(kbuf, "sending") == 0) { + copy_text(g_cfg.sending, value); + return true; + } + if (strcmp(kbuf, "signal") == 0) { + copy_text(g_cfg.signal, value); + return true; + } + if (strcmp(kbuf, "packet") == 0) { + copy_text(g_cfg.packet, value); + return true; + } + if (strcmp(kbuf, "power") == 0) { + copy_text(g_cfg.power, value); + return true; + } + if (strcmp(kbuf, "idle") == 0) { + copy_text(g_cfg.idle, value); + return true; + } + if (strcmp(kbuf, "callsign") == 0) { + copy_text(g_cfg.callsign, value); + return true; + } + if (strcmp(kbuf, "device_id") == 0) { + copy_text(g_cfg.device_id, value); + return true; + } + if (strcmp(kbuf, "quiet") == 0) { + bool b; + if (!parse_bool(value, &b)) { + return false; + } + g_cfg.quiet = b; + return true; + } + if (strcmp(kbuf, "cdt_active_high") == 0) { + bool b; + if (!parse_bool(value, &b)) { + return false; + } + g_cfg.cdt_active_high = b; + return true; + } + if (strcmp(kbuf, "packet_hold_ms") == 0) { + g_cfg.packet_hold_ms = (uint16_t)atoi(value); + return true; + } + if (strcmp(kbuf, "packet_edge_min") == 0) { + g_cfg.packet_edge_min = (uint16_t)atoi(value); + return true; + } + if (strcmp(kbuf, "packet_window_ms") == 0) { + g_cfg.packet_window_ms = (uint16_t)atoi(value); + return true; + } + return false; +} + +bool config_get_kv(const char *key, char *buf, size_t buflen) +{ + if (!key || !buf || buflen == 0) { + return false; + } + char kbuf[32]; + size_t n = strlen(key); + if (n >= sizeof(kbuf)) { + return false; + } + for (size_t i = 0; i <= n; i++) { + kbuf[i] = (char)tolower((unsigned char)key[i]); + } + + const char *src = NULL; + char num[16]; + if (strcmp(kbuf, "starting") == 0) { + src = g_cfg.starting; + } else if (strcmp(kbuf, "ready") == 0) { + src = g_cfg.ready; + } else if (strcmp(kbuf, "fault") == 0) { + src = g_cfg.fault; + } else if (strcmp(kbuf, "sending") == 0) { + src = g_cfg.sending; + } else if (strcmp(kbuf, "signal") == 0) { + src = g_cfg.signal; + } else if (strcmp(kbuf, "packet") == 0) { + src = g_cfg.packet; + } else if (strcmp(kbuf, "power") == 0) { + src = g_cfg.power; + } else if (strcmp(kbuf, "idle") == 0) { + src = g_cfg.idle; + } else if (strcmp(kbuf, "callsign") == 0) { + src = g_cfg.callsign; + } else if (strcmp(kbuf, "device_id") == 0) { + src = g_cfg.device_id; + } else if (strcmp(kbuf, "quiet") == 0) { + snprintf(num, sizeof(num), "%d", (int)g_cfg.quiet); + src = num; + } else if (strcmp(kbuf, "cdt_active_high") == 0) { + snprintf(num, sizeof(num), "%d", (int)g_cfg.cdt_active_high); + src = num; + } else if (strcmp(kbuf, "packet_hold_ms") == 0) { + snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_hold_ms); + src = num; + } else if (strcmp(kbuf, "packet_edge_min") == 0) { + snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_edge_min); + src = num; + } else if (strcmp(kbuf, "packet_window_ms") == 0) { + snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_window_ms); + src = num; + } else { + return false; + } + snprintf(buf, buflen, "%s", src); + return true; +} + +static void parse_ini_text(char *text) +{ + char *save = NULL; + for (char *line = strtok_r(text, "\r\n", &save); line; + line = strtok_r(NULL, "\r\n", &save)) { + line = trim(line); + if (line[0] == '\0' || line[0] == '#' || line[0] == ';') { + continue; + } + if (line[0] == '[') { + continue; /* section headers ignored — flat keys */ + } + char *eq = strchr(line, '='); + if (!eq) { + continue; + } + *eq = '\0'; + char *k = trim(line); + char *v = trim(eq + 1); + (void)config_set_kv(k, v); + } +} + +bool config_load_flash(void) +{ + const uint8_t *base = (const uint8_t *)(XIP_BASE + CFG_FLASH_OFFSET); + const cfg_hdr_t *hdr = (const cfg_hdr_t *)base; + if (hdr->magic[0] != CFG_MAGIC0 || hdr->magic[1] != CFG_MAGIC1 || + hdr->magic[2] != CFG_MAGIC2 || hdr->magic[3] != CFG_MAGIC3) { + return false; + } + if (hdr->version != CFG_VERSION) { + return false; + } + if (hdr->length == 0 || hdr->length > (CFG_SECTOR_SIZE - sizeof(cfg_hdr_t) - 1)) { + return false; + } + const uint8_t *data = base + sizeof(cfg_hdr_t); + if (crc16_ccitt(data, hdr->length) != hdr->crc) { + return false; + } + static char tmp[CFG_SECTOR_SIZE]; + if (hdr->length + 1 > sizeof(tmp)) { + return false; + } + memcpy(tmp, data, hdr->length); + tmp[hdr->length] = '\0'; + parse_ini_text(tmp); + return true; +} + +static size_t build_ini(char *out, size_t outsz) +{ + int n = snprintf(out, outsz, + "# c1224.ini — operator texts + terminal + sense\n" + "[texts]\n" + "starting=%s\n" + "ready=%s\n" + "fault=%s\n" + "sending=%s\n" + "signal=%s\n" + "packet=%s\n" + "power=%s\n" + "idle=%s\n" + "[terminal]\n" + "callsign=%s\n" + "device_id=%s\n" + "quiet=%d\n" + "[sense]\n" + "cdt_active_high=%d\n" + "packet_hold_ms=%u\n" + "packet_edge_min=%u\n" + "packet_window_ms=%u\n", + g_cfg.starting, g_cfg.ready, g_cfg.fault, g_cfg.sending, + g_cfg.signal, g_cfg.packet, g_cfg.power, g_cfg.idle, + g_cfg.callsign, g_cfg.device_id, + (int)g_cfg.quiet, + (int)g_cfg.cdt_active_high, + (unsigned)g_cfg.packet_hold_ms, + (unsigned)g_cfg.packet_edge_min, + (unsigned)g_cfg.packet_window_ms); + if (n < 0) { + return 0; + } + if ((size_t)n >= outsz) { + return outsz - 1; + } + return (size_t)n; +} + +bool config_save_flash(void) +{ + char body[CFG_SECTOR_SIZE - sizeof(cfg_hdr_t)]; + size_t len = build_ini(body, sizeof(body)); + if (len == 0) { + return false; + } + uint8_t sector[CFG_SECTOR_SIZE]; + memset(sector, 0xFF, sizeof(sector)); + cfg_hdr_t *hdr = (cfg_hdr_t *)sector; + hdr->magic[0] = CFG_MAGIC0; + hdr->magic[1] = CFG_MAGIC1; + hdr->magic[2] = CFG_MAGIC2; + hdr->magic[3] = CFG_MAGIC3; + hdr->version = CFG_VERSION; + hdr->reserved = 0; + hdr->length = (uint16_t)len; + hdr->crc = crc16_ccitt((const uint8_t *)body, len); + memcpy(sector + sizeof(cfg_hdr_t), body, len); + + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(CFG_FLASH_OFFSET, CFG_SECTOR_SIZE); + flash_range_program(CFG_FLASH_OFFSET, sector, CFG_SECTOR_SIZE); + restore_interrupts(ints); + return true; +} + +bool config_erase_flash(void) +{ + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(CFG_FLASH_OFFSET, CFG_SECTOR_SIZE); + restore_interrupts(ints); + return true; +} + +void config_dump_ini(void) +{ + char body[CFG_SECTOR_SIZE - sizeof(cfg_hdr_t)]; + size_t len = build_ini(body, sizeof(body)); + if (len == 0) { + printf("(empty)\r\n"); + return; + } + /* print line by line for CDC */ + for (size_t i = 0; i < len; i++) { + putchar(body[i]); + if (body[i] == '\n') { + putchar('\r'); + } + } + if (len == 0 || body[len - 1] != '\n') { + printf("\r\n"); + } +} + +void config_init(void) +{ + config_load_defaults(); + bool flash_ok = config_load_flash(); + /* Extra chatter only when quiet=0 (fixed boot banner is separate / always on). */ + if (config_unsolicited_ok()) { + if (flash_ok) { + printf("CONFIG: flash INI loaded\r\n"); + } else { + printf("CONFIG: defaults (no flash overlay)\r\n"); + } + } +} + +const c1224_config_t *config_get(void) +{ + return &g_cfg; +} + +c1224_config_t *config_mutable(void) +{ + return &g_cfg; +} + +bool config_unsolicited_ok(void) +{ + return !g_cfg.quiet; +} diff --git a/firmware/c1224/src/common/crystal_steer.c b/firmware/c1224/src/common/crystal_steer.c new file mode 100644 index 0000000..520682c --- /dev/null +++ b/firmware/c1224/src/common/crystal_steer.c @@ -0,0 +1,20 @@ +/* + * Crystal policy — v1.0 single-XTAL: physical rate = stuffed crystal. + * FW does not switch baud; dual UF2 obsolete; crystal_steer = no-op. + * Y1 4.4336 → 1200 · Y2 6.5536 → 2400 · GP14/15 unused (mux abandoned). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "crystal_steer.h" + +static bool g_applied; + +void crystal_steer_apply(void) +{ + /* No-op: board stuffing (not GPIO mux / not FW baud) selects the crystal. */ + g_applied = true; +} + +bool crystal_steer_applied(void) +{ + return g_applied; +} diff --git a/firmware/c1224/src/common/display.c b/firmware/c1224/src/common/display.c new file mode 100644 index 0000000..e3ac2ab --- /dev/null +++ b/firmware/c1224/src/common/display.c @@ -0,0 +1,187 @@ +/* + * HD44780 LCD via PCF8574 I2C backpack + USB mirror. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "display.h" +#include "board_pins.h" +#include "config.h" +#include "status.h" +#include "sense.h" +#include "ptt.h" +#include "hardware/i2c.h" +#include "pico/stdlib.h" +#include <stdio.h> +#include <string.h> +#include <stdint.h> + +/* PCF8574 bit map (common backpack): P0=RS P1=RW P2=E P3=BL P4-7=D4-D7 */ +#define LCD_RS 0x01 +#define LCD_RW 0x02 +#define LCD_EN 0x04 +#define LCD_BL 0x08 + +static bool g_lcd; +static uint8_t g_addr = I2C_LCD_ADDR_DEFAULT; +static char g_last_l0[17]; +static char g_last_l1[17]; + +static bool i2c_write_u8(uint8_t addr, uint8_t v) +{ + return i2c_write_blocking(i2c0, addr, &v, 1, false) >= 0; +} + +static bool i2c_probe(uint8_t addr) +{ + uint8_t b = 0; + return i2c_write_blocking(i2c0, addr, &b, 1, false) >= 0; +} + +static void lcd_write4(uint8_t nibble, uint8_t mode) +{ + uint8_t data = (uint8_t)((nibble & 0xF0) | LCD_BL | mode); + i2c_write_u8(g_addr, (uint8_t)(data | LCD_EN)); + sleep_us(1); + i2c_write_u8(g_addr, (uint8_t)(data & (uint8_t)~LCD_EN)); + sleep_us(50); +} + +static void lcd_send(uint8_t value, uint8_t mode) +{ + lcd_write4((uint8_t)(value & 0xF0), mode); + lcd_write4((uint8_t)((value << 4) & 0xF0), mode); +} + +static void lcd_cmd(uint8_t c) +{ + lcd_send(c, 0); +} + +static void lcd_data(uint8_t c) +{ + lcd_send(c, LCD_RS); +} + +static void lcd_set_cursor(uint8_t col, uint8_t row) +{ + static const uint8_t row_addr[] = {0x00, 0x40, 0x14, 0x54}; + if (row > 1) { + row = 1; + } + lcd_cmd((uint8_t)(0x80 | (row_addr[row] + col))); +} + +static void lcd_print_line(uint8_t row, const char *s) +{ + char buf[17]; + size_t n = strlen(s); + if (n > 16) { + n = 16; + } + memset(buf, ' ', 16); + memcpy(buf, s, n); + buf[16] = '\0'; + lcd_set_cursor(0, row); + for (int i = 0; i < 16; i++) { + lcd_data((uint8_t)buf[i]); + } +} + +static bool lcd_init_seq(uint8_t addr) +{ + g_addr = addr; + sleep_ms(50); + lcd_write4(0x30, 0); + sleep_ms(5); + lcd_write4(0x30, 0); + sleep_us(150); + lcd_write4(0x30, 0); + sleep_us(150); + lcd_write4(0x20, 0); /* 4-bit */ + lcd_cmd(0x28); /* 2 line, 5x8 */ + lcd_cmd(0x0C); /* display on, cursor off */ + lcd_cmd(0x06); /* entry mode */ + lcd_cmd(0x01); /* clear */ + sleep_ms(2); + return true; +} + +void display_init(void) +{ + g_lcd = false; + g_last_l0[0] = '\0'; + g_last_l1[0] = '\0'; + + uint8_t addrs[] = {0x27, 0x3F}; + for (unsigned i = 0; i < sizeof(addrs); i++) { + if (i2c_probe(addrs[i])) { + if (lcd_init_seq(addrs[i])) { + g_lcd = true; + if (config_unsolicited_ok()) { + printf("LCD: HD44780 @ 0x%02X\r\n", addrs[i]); + } + return; + } + } + } + if (config_unsolicited_ok()) { + printf("LCD: none — USB status only\r\n"); + } +} + +bool display_present(void) +{ + return g_lcd; +} + +void display_show(const char *line) +{ + if (!line) { + return; + } + if (g_lcd) { + lcd_print_line(0, line); + } +} + +void display_update(void) +{ + const c1224_config_t *cfg = config_get(); + char l0[17]; + char l1[17]; + + c1224_state_t st = status_get(); + if (st == C1224_STATE_FAULT) { + snprintf(l0, sizeof(l0), "%.16s", cfg->fault); + } else if (ptt_get()) { + snprintf(l0, sizeof(l0), "%.16s", cfg->sending); + } else if (st == C1224_STATE_STARTING) { + snprintf(l0, sizeof(l0), "%.16s", cfg->starting); + } else { + /* Power/ready on line 0 */ + snprintf(l0, sizeof(l0), "%.16s", cfg->ready); + } + + /* Line 1: Signal + Packet from sense */ + bool sig = sense_signal_active(); + bool pkt = sense_packet_active(); + if (sig && pkt) { + snprintf(l1, sizeof(l1), "%.7s %.7s", cfg->signal, cfg->packet); + } else if (sig) { + snprintf(l1, sizeof(l1), "%.16s", cfg->signal); + } else if (pkt) { + snprintf(l1, sizeof(l1), "%.16s", cfg->packet); + } else { + snprintf(l1, sizeof(l1), "%.16s", cfg->power); + } + + if (strcmp(l0, g_last_l0) != 0 || strcmp(l1, g_last_l1) != 0) { + strncpy(g_last_l0, l0, sizeof(g_last_l0)); + strncpy(g_last_l1, l1, sizeof(g_last_l1)); + g_last_l0[16] = '\0'; + g_last_l1[16] = '\0'; + if (g_lcd) { + lcd_print_line(0, l0); + lcd_print_line(1, l1); + } + } +} diff --git a/firmware/c1224/src/common/hdlc.c b/firmware/c1224/src/common/hdlc.c new file mode 100644 index 0000000..3cee0da --- /dev/null +++ b/firmware/c1224/src/common/hdlc.c @@ -0,0 +1,192 @@ +/* + * HDLC encode/decode — AX.25 bit order (LSB first), FCS-CCITT. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "hdlc.h" + +uint16_t hdlc_fcs_update(uint16_t fcs, uint8_t byte) +{ + fcs ^= (uint16_t)byte; + for (int i = 0; i < 8; i++) { + if (fcs & 1u) { + fcs = (uint16_t)((fcs >> 1) ^ 0x8408u); + } else { + fcs >>= 1; + } + } + return fcs; +} + +uint16_t hdlc_fcs_final(uint16_t fcs) +{ + return (uint16_t)~fcs; +} + +static bool push_bit(uint8_t *bits, size_t bits_cap, size_t *n, bool v) +{ + if (*n >= bits_cap) { + return false; + } + bits[*n] = v ? 1u : 0u; + (*n)++; + return true; +} + +static bool stuff_bit(uint8_t *bits, size_t bits_cap, size_t *n, + unsigned *ones, bool bit) +{ + if (!push_bit(bits, bits_cap, n, bit)) { + return false; + } + if (bit) { + (*ones)++; + if (*ones == 5u) { + if (!push_bit(bits, bits_cap, n, false)) { + return false; + } + *ones = 0; + } + } else { + *ones = 0; + } + return true; +} + +static bool emit_raw_byte(uint8_t *bits, size_t bits_cap, size_t *n, uint8_t b) +{ + for (int i = 0; i < 8; i++) { + if (!push_bit(bits, bits_cap, n, (b >> i) & 1u)) { + return false; + } + } + return true; +} + +static bool emit_stuffed_byte(uint8_t *bits, size_t bits_cap, size_t *n, + unsigned *ones, uint8_t b) +{ + for (int i = 0; i < 8; i++) { + if (!stuff_bit(bits, bits_cap, n, ones, (b >> i) & 1u)) { + return false; + } + } + return true; +} + +size_t hdlc_encode_bits(const uint8_t *info, size_t info_len, + uint8_t *bits, size_t bits_cap, + unsigned trailing_flags) +{ + if (!info || !bits || info_len > HDLC_MAX_INFO) { + return 0; + } + size_t n = 0; + unsigned ones = 0; + + if (!emit_raw_byte(bits, bits_cap, &n, HDLC_FLAG)) { + return 0; + } + + uint16_t fcs = HDLC_FCS_INIT; + for (size_t i = 0; i < info_len; i++) { + fcs = hdlc_fcs_update(fcs, info[i]); + if (!emit_stuffed_byte(bits, bits_cap, &n, &ones, info[i])) { + return 0; + } + } + fcs = hdlc_fcs_final(fcs); + if (!emit_stuffed_byte(bits, bits_cap, &n, &ones, (uint8_t)(fcs & 0xFFu))) { + return 0; + } + if (!emit_stuffed_byte(bits, bits_cap, &n, &ones, (uint8_t)((fcs >> 8) & 0xFFu))) { + return 0; + } + + ones = 0; + unsigned flags = trailing_flags ? trailing_flags : 1u; + for (unsigned f = 0; f < flags; f++) { + if (!emit_raw_byte(bits, bits_cap, &n, HDLC_FLAG)) { + return 0; + } + } + return n; +} + +void hdlc_rx_init(hdlc_rx_t *rx) +{ + rx->len = 0; + rx->ones = 0; + rx->in_frame = false; + rx->stuffing = false; + rx->byte_acc = 0; + rx->bit_count = 0; +} + +bool hdlc_rx_bit(hdlc_rx_t *rx, bool bit, uint8_t *out, size_t *out_len) +{ + if (bit) { + rx->ones++; + } else { + if (rx->ones == 5u) { + /* stuffed 0 — discard */ + rx->ones = 0; + return false; + } + if (rx->ones == 6u) { + /* flag */ + if (rx->in_frame && rx->len >= 3) { + uint16_t fcs = HDLC_FCS_INIT; + for (size_t i = 0; i < rx->len; i++) { + fcs = hdlc_fcs_update(fcs, rx->buf[i]); + } + if (fcs == HDLC_FCS_GOOD) { + size_t info_len = rx->len - 2; + for (size_t i = 0; i < info_len; i++) { + out[i] = rx->buf[i]; + } + *out_len = info_len; + rx->len = 0; + rx->bit_count = 0; + rx->byte_acc = 0; + rx->ones = 0; + rx->in_frame = true; /* abutting frames */ + return true; + } + } + rx->len = 0; + rx->bit_count = 0; + rx->byte_acc = 0; + rx->ones = 0; + rx->in_frame = true; + return false; + } + if (rx->ones > 6u) { + /* abort */ + rx->in_frame = false; + rx->len = 0; + rx->bit_count = 0; + rx->byte_acc = 0; + rx->ones = 0; + return false; + } + rx->ones = 0; + } + + if (!rx->in_frame) { + return false; + } + + rx->byte_acc |= (uint8_t)((bit ? 1u : 0u) << rx->bit_count); + rx->bit_count++; + if (rx->bit_count >= 8) { + if (rx->len < sizeof(rx->buf)) { + rx->buf[rx->len++] = rx->byte_acc; + } else { + rx->in_frame = false; + rx->len = 0; + } + rx->byte_acc = 0; + rx->bit_count = 0; + } + return false; +} diff --git a/firmware/c1224/src/common/kiss.c b/firmware/c1224/src/common/kiss.c new file mode 100644 index 0000000..1d5d0e7 --- /dev/null +++ b/firmware/c1224/src/common/kiss.c @@ -0,0 +1,98 @@ +/* + * KISS encode/decode. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "kiss.h" + +void kiss_rx_init(kiss_rx_t *kx) +{ + kx->len = 0; + kx->in_frame = false; + kx->esc = false; +} + +bool kiss_rx_byte(kiss_rx_t *kx, uint8_t b, uint8_t *out, size_t *out_len) +{ + if (b == KISS_FEND) { + if (kx->in_frame && kx->len >= 1) { + uint8_t cmd = kx->buf[0]; + if ((cmd & 0x0Fu) == KISS_CMD_DATA && kx->len > 1) { + size_t n = kx->len - 1; + for (size_t i = 0; i < n; i++) { + out[i] = kx->buf[i + 1]; + } + *out_len = n; + kx->len = 0; + kx->esc = false; + kx->in_frame = true; + return true; + } + } + kx->len = 0; + kx->esc = false; + kx->in_frame = true; + return false; + } + if (!kx->in_frame) { + return false; + } + if (kx->esc) { + if (b == KISS_TFEND) { + b = KISS_FEND; + } else if (b == KISS_TFESC) { + b = KISS_FESC; + } + kx->esc = false; + } else if (b == KISS_FESC) { + kx->esc = true; + return false; + } + if (kx->len < sizeof(kx->buf)) { + kx->buf[kx->len++] = b; + } else { + kx->in_frame = false; + kx->len = 0; + } + return false; +} + +static bool push(uint8_t *dst, size_t cap, size_t *n, uint8_t b) +{ + if (*n >= cap) { + return false; + } + dst[(*n)++] = b; + return true; +} + +static bool push_esc(uint8_t *dst, size_t cap, size_t *n, uint8_t b) +{ + if (b == KISS_FEND) { + return push(dst, cap, n, KISS_FESC) && push(dst, cap, n, KISS_TFEND); + } + if (b == KISS_FESC) { + return push(dst, cap, n, KISS_FESC) && push(dst, cap, n, KISS_TFESC); + } + return push(dst, cap, n, b); +} + +size_t kiss_encode_data(const uint8_t *payload, size_t len, + uint8_t *dst, size_t dst_cap) +{ + size_t n = 0; + if (!push(dst, dst_cap, &n, KISS_FEND)) { + return 0; + } + if (!push_esc(dst, dst_cap, &n, KISS_CMD_DATA)) { + return 0; + } + for (size_t i = 0; i < len; i++) { + if (!push_esc(dst, dst_cap, &n, payload[i])) { + return 0; + } + } + if (!push(dst, dst_cap, &n, KISS_FEND)) { + return 0; + } + return n; +} diff --git a/firmware/c1224/src/common/main.c b/firmware/c1224/src/common/main.c new file mode 100644 index 0000000..e113f91 --- /dev/null +++ b/firmware/c1224/src/common/main.c @@ -0,0 +1,61 @@ +/* + * T-Modem-c1224 — USB KISS half-TNC (HDLC on Pico + bitbang to TCM3105). + * No softmodem · no transparent Host↔Chip bit bridge · one FW · rate = crystal. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "pico/stdlib.h" +#include "board_pins.h" +#include "status.h" +#include "ptt.h" +#include "modem_probe.h" +#include "usb_cmd.h" +#include "display.h" +#include "crystal_steer.h" +#include "baud_config.h" +#include "config.h" +#include "sense.h" +#include "afsk_bitbang.h" +#include "banner.h" +#include "hardware/gpio.h" +#include <stdio.h> + +int main(void) +{ + stdio_init_all(); + sleep_ms(800); + + board_pins_init(); + ptt_init(); + config_init(); + display_init(); + sense_init(); + afsk_bitbang_init(); + usb_cmd_init(); + afsk_rx_set_callback(usb_cmd_on_rx_frame); + + status_set(C1224_STATE_STARTING); + status_publish(); + /* Fixed boot banner — always once after CDC ready; source-only (banner.c). */ + banner_print(); + + modem_probe_result_t probe = modem_probe_run(); + if (probe.ready) { + status_set(C1224_STATE_READY); + } else { + status_set(C1224_STATE_FAULT); + } + status_publish(); + + while (true) { + usb_cmd_poll(); + afsk_rx_poll(); + sense_poll(); + + gpio_put(PIN_LED_DCD, sense_signal_active() ? 1 : 0); + gpio_put(PIN_LED_PTT, ptt_get() ? 1 : 0); + gpio_put(PIN_LED_TXRX, sense_packet_active() ? 1 : 0); + + status_tick(); + sleep_us(50); + } +} diff --git a/firmware/c1224/src/common/modem_probe.c b/firmware/c1224/src/common/modem_probe.c new file mode 100644 index 0000000..33b58e4 --- /dev/null +++ b/firmware/c1224/src/common/modem_probe.c @@ -0,0 +1,38 @@ +/* + * Offline health check — CDT+RXD readable; crystal_steer no-op applied. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "modem_probe.h" +#include "config.h" +#include "board_pins.h" +#include "crystal_steer.h" +#include "hardware/gpio.h" +#include "pico/stdlib.h" +#include <stdio.h> + +modem_probe_result_t modem_probe_run(void) +{ + modem_probe_result_t r = {0}; + + crystal_steer_apply(); + r.crystal_driven = crystal_steer_applied(); + + (void)gpio_get(PIN_MODEM_CDT); + (void)gpio_get(PIN_MODEM_RXD); + r.cdt_readable = true; + r.rxd_readable = true; + + /* Optional TXD pulse — no CDT reaction required offline */ + gpio_put(PIN_MODEM_TXD, 1); + sleep_ms(1); + gpio_put(PIN_MODEM_TXD, 0); + + r.ready = r.crystal_driven && r.cdt_readable && r.rxd_readable; + + if (config_unsolicited_ok()) { + printf("PROBE crystal=%d cdt=%d rxd=%d -> %s\r\n", + (int)r.crystal_driven, (int)r.cdt_readable, (int)r.rxd_readable, + r.ready ? "READY" : "FAULT"); + } + return r; +} diff --git a/firmware/c1224/src/common/ptt.c b/firmware/c1224/src/common/ptt.c new file mode 100644 index 0000000..f54f221 --- /dev/null +++ b/firmware/c1224/src/common/ptt.c @@ -0,0 +1,31 @@ +/* + * PTT via Pico GPIO — host asserts over USB CDC (Type-A Path B), not softmodem. + * PIN_PTT_DRV = keying (required). PIN_LED_PTT = optional/spare TX LED mirror. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "ptt.h" +#include "board_pins.h" +#include "hardware/gpio.h" + +static bool g_ptt; + +void ptt_init(void) +{ + gpio_init(PIN_PTT_DRV); + gpio_set_dir(PIN_PTT_DRV, GPIO_OUT); + gpio_put(PIN_PTT_DRV, 0); + g_ptt = false; + gpio_put(PIN_LED_PTT, 0); +} + +void ptt_set(bool on) +{ + g_ptt = on; + gpio_put(PIN_PTT_DRV, on ? 1 : 0); + gpio_put(PIN_LED_PTT, on ? 1 : 0); +} + +bool ptt_get(void) +{ + return g_ptt; +} diff --git a/firmware/c1224/src/common/sense.c b/firmware/c1224/src/common/sense.c new file mode 100644 index 0000000..6bd33a4 --- /dev/null +++ b/firmware/c1224/src/common/sense.c @@ -0,0 +1,99 @@ +/* + * Dual sense: CDT (Signal) + RXD packet heuristic (edges + HDLC 0x7E). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "sense.h" +#include "board_pins.h" +#include "config.h" +#include "hardware/gpio.h" +#include "pico/stdlib.h" +#include <stdint.h> + +static bool g_signal; +static bool g_packet; +static bool g_flag; +static absolute_time_t g_packet_until; +static absolute_time_t g_window_start; +static uint16_t g_edges; +static bool g_last_rxd; +static bool g_hold_valid; + +static void try_flag_sample(void) +{ + uint16_t sh = 0; + for (int i = 0; i < 16; i++) { + bool b = gpio_get(PIN_MODEM_RXD); + sh = (uint16_t)((sh << 1) | (b ? 1u : 0u)); + if ((uint8_t)(sh & 0xFFu) == 0x7Eu) { + g_flag = true; + return; + } + sleep_us(833); + } +} + +void sense_init(void) +{ + g_signal = false; + g_packet = false; + g_flag = false; + g_hold_valid = false; + g_window_start = get_absolute_time(); + g_edges = 0; + g_last_rxd = gpio_get(PIN_MODEM_RXD); +} + +void sense_poll(void) +{ + const c1224_config_t *cfg = config_get(); + + bool cdt = gpio_get(PIN_MODEM_CDT); + g_signal = cfg->cdt_active_high ? cdt : !cdt; + + bool rxd = gpio_get(PIN_MODEM_RXD); + if (rxd != g_last_rxd) { + g_edges++; + g_last_rxd = rxd; + } + + uint32_t win_ms = cfg->packet_window_ms ? cfg->packet_window_ms : 50; + if (absolute_time_diff_us(g_window_start, get_absolute_time()) >= + (int64_t)win_ms * 1000) { + if (g_edges >= cfg->packet_edge_min) { + g_flag = false; + try_flag_sample(); + g_packet_until = make_timeout_time_ms( + cfg->packet_hold_ms ? cfg->packet_hold_ms : 500); + g_hold_valid = true; + } + g_edges = 0; + g_window_start = get_absolute_time(); + } + + if (g_hold_valid) { + if (time_reached(g_packet_until)) { + g_hold_valid = false; + g_packet = false; + g_flag = false; + } else { + g_packet = true; + } + } else { + g_packet = false; + } +} + +bool sense_signal_active(void) +{ + return g_signal; +} + +bool sense_packet_active(void) +{ + return g_packet; +} + +bool sense_packet_flag(void) +{ + return g_flag; +} diff --git a/firmware/c1224/src/common/status.c b/firmware/c1224/src/common/status.c new file mode 100644 index 0000000..8d09f22 --- /dev/null +++ b/firmware/c1224/src/common/status.c @@ -0,0 +1,78 @@ +/* + * Status from PTT + dual sense; texts from INI. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "status.h" +#include "config.h" +#include "display.h" +#include "ptt.h" +#include "sense.h" +#include <stdio.h> + +static c1224_state_t g_state = C1224_STATE_STARTING; + +const char *c1224_state_string(c1224_state_t st) +{ + const c1224_config_t *cfg = config_get(); + switch (st) { + case C1224_STATE_STARTING: + return cfg->starting; + case C1224_STATE_READY: + return cfg->ready; + case C1224_STATE_FAULT: + return cfg->fault; + case C1224_STATE_SENDING: + return cfg->sending; + case C1224_STATE_SIGNAL: + return cfg->signal; + case C1224_STATE_PACKET: + return cfg->packet; + default: + return cfg->fault; + } +} + +void status_set(c1224_state_t st) +{ + g_state = st; +} + +c1224_state_t status_get(void) +{ + return g_state; +} + +void status_publish(void) +{ + const char *s = c1224_state_string(g_state); + if (config_unsolicited_ok()) { + printf("STATUS %s\r\n", s); + } + display_update(); +} + +void status_tick(void) +{ + if (g_state == C1224_STATE_FAULT || g_state == C1224_STATE_STARTING) { + display_update(); + return; + } + + c1224_state_t next; + if (ptt_get()) { + next = C1224_STATE_SENDING; + } else if (sense_packet_active()) { + next = C1224_STATE_PACKET; + } else if (sense_signal_active()) { + next = C1224_STATE_SIGNAL; + } else { + next = C1224_STATE_READY; + } + + if (next != g_state) { + g_state = next; + status_publish(); + } else { + display_update(); + } +} diff --git a/firmware/c1224/src/common/usb_cmd.c b/firmware/c1224/src/common/usb_cmd.c new file mode 100644 index 0000000..3c412de --- /dev/null +++ b/firmware/c1224/src/common/usb_cmd.c @@ -0,0 +1,268 @@ +/* + * USB CDC multiplexer: KISS binary (0xC0 frames) + ASCII service commands. + * Transparent Host↔Chip bit bridge stays abandoned — KISS half-TNC is the exception. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "usb_cmd.h" +#include "pico/stdlib.h" +#include "ptt.h" +#include "status.h" +#include "config.h" +#include "sense.h" +#include "board_pins.h" +#include "afsk_bitbang.h" +#include "kiss.h" +#include "hardware/gpio.h" +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +#define LINE_MAX 96 + +static char g_line[LINE_MAX]; +static unsigned g_len; +static kiss_rx_t g_kiss; +static uint8_t g_kiss_payload[KISS_MAX_PAYLOAD]; + +static void trim_copy_upper_cmd(char *dst, size_t dstsz, const char *src) +{ + while (*src && isspace((unsigned char)*src)) { + src++; + } + size_t i = 0; + while (src[i] && i + 1 < dstsz) { + dst[i] = (char)toupper((unsigned char)src[i]); + i++; + } + dst[i] = '\0'; + while (i > 0 && isspace((unsigned char)dst[i - 1])) { + dst[--i] = '\0'; + } +} + +static void handle_line(void) +{ + char cmd[LINE_MAX]; + trim_copy_upper_cmd(cmd, sizeof(cmd), g_line); + if (cmd[0] == '\0') { + return; + } + + if (strcmp(cmd, "PTT ON") == 0 || strcmp(cmd, "PTT 1") == 0) { + ptt_set(true); + status_tick(); + printf("OK PTT ON\r\n"); + return; + } + if (strcmp(cmd, "PTT OFF") == 0 || strcmp(cmd, "PTT 0") == 0) { + ptt_set(false); + status_tick(); + printf("OK PTT OFF\r\n"); + return; + } + if (strcmp(cmd, "STATUS") == 0) { + printf("STATUS state=%s ptt=%d signal=%d packet=%d flag=%d " + "cdt_raw=%d rxd_raw=%d bitrate=%u kiss_half_tnc=1\r\n", + c1224_state_string(status_get()), + (int)ptt_get(), + (int)sense_signal_active(), + (int)sense_packet_active(), + (int)sense_packet_flag(), + (int)gpio_get(PIN_MODEM_CDT), + (int)gpio_get(PIN_MODEM_RXD), + afsk_bitrate()); + return; + } + if (strcmp(cmd, "HELP") == 0) { + printf("Commands:\r\n"); + printf(" PTT ON | PTT OFF\r\n"); + printf(" STATUS | HELP | ID\r\n"); + printf(" QUIET ON|OFF | VERBOSE ON|OFF\r\n"); + printf(" INI | GET <key> | SET <key>=<value>\r\n"); + printf(" SAVE | RELOAD | DEFAULTS\r\n"); + printf("Dual protocol: byte 0xC0 → KISS; line ASCII → service.\r\n"); + printf("Packet: use KISS-only (any host). Service cmds optional.\r\n"); + printf("Boot banner always on (source). quiet=1 damps extra ASCII.\r\n"); + printf("PTT = GP9 (auto during KISS TX; manual PTT ON/OFF kept).\r\n"); + printf("Rate = stuffed crystal (FW bitbang C1224_BITRATE=%u).\r\n", + afsk_bitrate()); + printf("No softmodem · no Host↔Chip bit bridge · no baud switch.\r\n"); + printf("INI: texts + callsign device_id quiet + sense keys\r\n"); + return; + } + if (strcmp(cmd, "ID") == 0) { + const c1224_config_t *cfg = config_get(); + printf("ID device_id=%s callsign=%s bitrate=%u quiet=%d\r\n", + cfg->device_id[0] ? cfg->device_id : "c1224", + cfg->callsign, + afsk_bitrate(), + (int)cfg->quiet); + return; + } + if (strcmp(cmd, "QUIET ON") == 0 || strcmp(cmd, "QUIET 1") == 0) { + config_mutable()->quiet = true; + printf("OK QUIET ON\r\n"); + return; + } + if (strcmp(cmd, "QUIET OFF") == 0 || strcmp(cmd, "QUIET 0") == 0) { + config_mutable()->quiet = false; + printf("OK QUIET OFF\r\n"); + return; + } + if (strcmp(cmd, "VERBOSE ON") == 0 || strcmp(cmd, "VERBOSE 1") == 0) { + config_mutable()->quiet = false; + printf("OK VERBOSE ON\r\n"); + return; + } + if (strcmp(cmd, "VERBOSE OFF") == 0 || strcmp(cmd, "VERBOSE 0") == 0) { + config_mutable()->quiet = true; + printf("OK VERBOSE OFF\r\n"); + return; + } + if (strcmp(cmd, "INI") == 0) { + config_dump_ini(); + return; + } + if (strcmp(cmd, "SAVE") == 0) { + printf(config_save_flash() ? "OK SAVE\r\n" : "ERR SAVE\r\n"); + return; + } + if (strcmp(cmd, "RELOAD") == 0) { + config_load_defaults(); + if (config_load_flash()) { + printf("OK RELOAD flash\r\n"); + } else { + printf("OK RELOAD defaults (no flash)\r\n"); + } + status_publish(); + return; + } + if (strcmp(cmd, "DEFAULTS") == 0) { + config_load_defaults(); + printf("OK DEFAULTS (RAM; SAVE to persist)\r\n"); + status_publish(); + return; + } + if (strncmp(cmd, "GET ", 4) == 0) { + char key[32]; + const char *p = g_line; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (strncasecmp(p, "GET", 3) == 0) { + p += 3; + } + while (*p && isspace((unsigned char)*p)) { + p++; + } + size_t i = 0; + while (*p && !isspace((unsigned char)*p) && i + 1 < sizeof(key)) { + key[i++] = *p++; + } + key[i] = '\0'; + char val[C1224_TEXT_MAX]; + if (config_get_kv(key, val, sizeof(val))) { + printf("OK %s=%s\r\n", key, val); + } else { + printf("ERR unknown key\r\n"); + } + return; + } + if (strncmp(cmd, "SET ", 4) == 0) { + const char *p = g_line; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (strncasecmp(p, "SET", 3) == 0) { + p += 3; + } + while (*p && isspace((unsigned char)*p)) { + p++; + } + char tmp[LINE_MAX]; + snprintf(tmp, sizeof(tmp), "%s", p); + char *eq = strchr(tmp, '='); + if (!eq) { + printf("ERR SET key=value\r\n"); + return; + } + *eq = '\0'; + char *k = tmp; + char *v = eq + 1; + while (*k && isspace((unsigned char)*k)) { + k++; + } + char *ke = k + strlen(k); + while (ke > k && isspace((unsigned char)ke[-1])) { + *--ke = '\0'; + } + while (*v && isspace((unsigned char)*v)) { + v++; + } + char *ve = v + strlen(v); + while (ve > v && isspace((unsigned char)ve[-1])) { + *--ve = '\0'; + } + if (config_set_kv(k, v)) { + printf("OK SET %s=%s (SAVE to persist)\r\n", k, v); + status_publish(); + } else { + printf("ERR unknown key or bad value\r\n"); + } + return; + } + + printf("ERR unknown (try HELP)\r\n"); +} + +void usb_cmd_on_rx_frame(const uint8_t *info, size_t len) +{ + uint8_t frame[KISS_MAX_PAYLOAD + 16]; + size_t n = kiss_encode_data(info, len, frame, sizeof(frame)); + for (size_t i = 0; i < n; i++) { + putchar_raw((int)frame[i]); + } +} + +void usb_cmd_poll(void) +{ + int c; + while ((c = getchar_timeout_us(0)) != PICO_ERROR_TIMEOUT) { + uint8_t b = (uint8_t)c; + + /* KISS path: FEND or mid-frame */ + if (b == KISS_FEND || g_kiss.in_frame) { + size_t plen = 0; + if (kiss_rx_byte(&g_kiss, b, g_kiss_payload, &plen)) { + if (plen > 0 && !afsk_tx_busy()) { + afsk_tx_frame(g_kiss_payload, plen); + } + } + continue; + } + + if (c == '\r' || c == '\n') { + if (g_len > 0) { + g_line[g_len] = '\0'; + handle_line(); + g_len = 0; + } + continue; + } + if (c == 0x08 || c == 0x7f) { + if (g_len > 0) { + g_len--; + } + continue; + } + if (g_len + 1 < LINE_MAX && c >= 32 && c < 127) { + g_line[g_len++] = (char)c; + } + } +} + +void usb_cmd_init(void) +{ + g_len = 0; + kiss_rx_init(&g_kiss); +} |
