diff options
Diffstat (limited to 'firmware/c1224')
33 files changed, 2314 insertions, 0 deletions
diff --git a/firmware/c1224/CMakeLists.txt b/firmware/c1224/CMakeLists.txt new file mode 100644 index 0000000..8b5db51 --- /dev/null +++ b/firmware/c1224/CMakeLists.txt @@ -0,0 +1,57 @@ +# T-Modem-c1224 Pico firmware — ONE UF2 · GPLv3 · AS-IS +# USB KISS half-TNC (HDLC on Pico + bitbang GP6/GP7). No softmodem. +cmake_minimum_required(VERSION 3.13) + +if(NOT DEFINED ENV{PICO_SDK_PATH} AND NOT DEFINED PICO_SDK_PATH) + message(FATAL_ERROR "Set PICO_SDK_PATH to the Raspberry Pi Pico SDK root") +endif() +if(DEFINED ENV{PICO_SDK_PATH} AND NOT DEFINED PICO_SDK_PATH) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) +endif() + +include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) + +project(c1224_firmware C CXX ASM) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +pico_sdk_init() + +# Physical rate = stuffed crystal. Bitbang must match: 1200 (Y1) or 2400 (Y2). +if(NOT DEFINED C1224_BITRATE) + set(C1224_BITRATE 1200) +endif() + +add_executable(c1224 + src/common/main.c + src/common/status.c + src/common/ptt.c + src/common/modem_probe.c + src/common/usb_cmd.c + src/common/display.c + src/common/board_pins.c + src/common/crystal_steer.c + src/common/baud_config.c + src/common/config.c + src/common/sense.c + src/common/hdlc.c + src/common/kiss.c + src/common/afsk_bitbang.c + src/common/banner.c +) + +target_include_directories(c1224 PRIVATE include) +target_compile_definitions(c1224 PRIVATE C1224_BITRATE=${C1224_BITRATE}) +target_link_libraries(c1224 + pico_stdlib + hardware_gpio + hardware_i2c + hardware_flash + hardware_sync +) + +pico_enable_stdio_usb(c1224 1) +pico_enable_stdio_uart(c1224 0) +pico_add_extra_outputs(c1224) + +pico_set_binary_type(c1224 default) diff --git a/firmware/c1224/README.md b/firmware/c1224/README.md new file mode 100644 index 0000000..298dc64 --- /dev/null +++ b/firmware/c1224/README.md @@ -0,0 +1,116 @@ +# T-Modem-c1224 — Pico USB KISS half-TNC firmware + +**AS-IS** · GPLv3 (repo `LICENSE`) · no support, no warranty · **v1.0-prototype**. + +**One** UF2 for Raspberry Pi Pico (RP2040). Chip does air AFSK; Pico is **USB KISS half-TNC** (HDLC + bitbang to TCM3105 TXD/RXD) plus display / PTT / sense. + +| Owns | Does **not** | +|------|----------------| +| HDLC TX/RX (flags, bit-stuff, FCS-CCITT) | Softmodem / AFSK DSP | +| Bitbang GP6 TXD / GP7 RXD at crystal rate | Transparent Host↔Chip bit bridge | +| USB CDC: KISS `0xC0` + ASCII service | Baud switch / XTAL mux | +| Auto PTT GP9 during KISS TX · `PTT ON`/`OFF` | Dual baud UF2 builds | +| Display / INI texts · CDT Signal · packet sense · fixed boot banner | INI/CDC boot-banner toggle | + +## Build + +Requires [Pico SDK](https://github.com/raspberrypi/pico-sdk) and `arm-none-eabi-gcc`. + +```bash +export PICO_SDK_PATH=/path/to/pico-sdk # or repo .deps/pico-sdk +cd firmware/c1224 +mkdir -p build && cd build +cmake .. -DC1224_BITRATE=1200 # or 2400 if Y2 stuffed +make -j$(nproc) c1224 +# → c1224.uf2 c1224.elf +``` + +`C1224_BITRATE` must match the stuffed crystal (**Y1 4.4336 → 1200**, **Y2 6.5536 → 2400**). Default **1200**. Same source tree; rebuild if field board is 2400. + +### Flash + +1. Hold **BOOTSEL**, plug USB (Path B — flash/service; board power = **12 V** barrel). +2. Copy `c1224.uf2` to RPI-RP2 (or `picotool load -f c1224.uf2`). +3. Host opens `/dev/ttyACM*` (USB CDC). + +## Any KISS terminal (standalone — no MAX25) + +**MAX25-Stack is not required.** No proprietary host protocol. **All terminals should work insofar as applicable** — any client that speaks **KISS and/or ASCII service** over a raw serial port. Optional convenience only: max25d device **`tmodem`** / `max25-terminal`. + +How-to: flash UF2 → plug USB → open `/dev/ttyACM*` → KISS `0xC0` or ASCII `HELP`. + +| Step | Detail | +|------|--------| +| Device | `/dev/ttyACM*` (Linux) / equivalent CDC ACM | +| Framing | Raw **8N1** · binary **KISS** (`0xC0` … `0xC0`) · DATA cmd `0x00` | +| Packet path | Send/receive KISS frames only — do **not** interleave ASCII while mid-frame | +| Service path | Optional line ASCII (`STATUS`, `PTT`, …) when **not** inside a KISS frame | +| Boot | Fixed source banner **always** once after CDC ready · `quiet=1` damps extra STATUS/CONFIG ASCII only | + +Examples: **kissattach** / ax25 tools, **kissutil**, Dire Wolf serial KISS, custom terminal, or optionally **max25-terminal** via **`tmodem`**. + +```bash +# Example: open CDC raw and use KISS (tool-dependent) +# stty -F /dev/ttyACM0 raw -echo +# kissutil -p /dev/ttyACM0 +# # or: kissattach /dev/ttyACM0 tm0 (ax25-tools; names vary by distro) +``` + +## Dual protocol (USB CDC) + +| Traffic | Rule | +|---------|------| +| Packet | Bytes starting with / continuing **`0xC0` KISS** → HDLC TX / RX encode | +| Service | Line-oriented ASCII → `PTT`, `STATUS`, `INI`, … | +| Custom terminals | Prefer **KISS-only** for packet; service commands optional | + +## Host path (summary) + +| Traffic | Format | +|---------|--------| +| Packet | Binary **KISS** frames (`0xC0` … `0xC0`) — DATA cmd `0x00` | +| Service | Line ASCII: `PTT`, `STATUS`, `ID`, `QUIET`/`VERBOSE`, `INI` / `GET` / `SET` / `SAVE` | + +## Pins + +| GPIO / rail | Role | +|-------------|------| +| GP0 / GP1 | I2C LCD | +| GP6 | Modem **TXD** — HDLC bitbang out | +| GP7 | Modem **RXD** — HDLC bitbang in + Packet UI sense | +| GP8 | Modem **CDT** → Signal/RX | +| GP9 | **PTT** (auto on TX + manual CDC) | +| GP10–12 | Optional LEDs | +| — | LED_PWR on **5 V rail** | + +## Boot banner + +Fixed source text in `src/common/banner.c` / `include/version.h` (not INI / CDC). **Always** printed once after USB CDC ready. Edit source only — no `banner=` INI key, no `BANNER ON|OFF`. `quiet=1` never suppresses the boot banner (only extra STATUS/PROBE/CONFIG ASCII). + +## USB CDC ASCII + +| Command | Effect | +|---------|--------| +| `HELP` | Command list | +| `STATUS` | state, PTT, signal, packet, bitrate, raw CDT/RXD | +| `ID` | `device_id` / `callsign` / bitrate / quiet (solicited) | +| `PTT ON` / `PTT OFF` | Key GP9 | +| `QUIET ON` / `OFF` | Suppress extra unsolicited CDC ASCII (default ON via INI `quiet=1`; never the boot banner) | +| `VERBOSE ON` / `OFF` | Alias: `QUIET OFF` / `QUIET ON` | +| `INI` / `GET` / `SET` / `SAVE` / `RELOAD` / `DEFAULTS` | Config | + +**`SAVE`:** writes a config **blob** into the **last 4 KiB** of Pico flash — **not** a filesystem. + +## INI (`[terminal]`) + +| Key | Default | Role | +|-----|---------|------| +| `callsign` | empty | Operator call / station string for `ID` | +| `device_id` | `c1224` | Short device mark (`c1224` or `tmodem`) | +| `quiet` | `1` | Suppress extra unsolicited CDC ASCII (not boot banner) | + +Display text keys (`starting`, `ready`, …) and sense keys unchanged. See [`share/c1224.ini.example`](../../share/c1224.ini.example). + +## Rate + +Stuff **Y1 4.4336** (1200) or **Y2 6.5536** (2400). Firmware does not switch baud at runtime. Rebuild with `-DC1224_BITRATE=2400` for Y2 boards. diff --git a/firmware/c1224/config/default.ini b/firmware/c1224/config/default.ini new file mode 100644 index 0000000..ff07c9e --- /dev/null +++ b/firmware/c1224/config/default.ini @@ -0,0 +1,25 @@ +# T-Modem-c1224 — default operator texts (embedded in FW; edit via CDC SET/SAVE) +# Physical rate = stuffed crystal (Y1 4.4336 → 1200 · Y2 6.5536 → 2400). Not a FW key. +# Boot banner is fixed in firmware source (always printed). quiet=1 suppresses extra ASCII. + +[texts] +starting=c1224 is starting +ready=c1224 is ready +fault=c1224 is fault +sending=c1224 is sending +signal=c1224 signal +packet=c1224 packet +power=c1224 power +idle=c1224 idle + +[terminal] +callsign= +device_id=c1224 +quiet=1 + +[sense] +# CDT active level: 0 = active-low (default, pull-up idle), 1 = active-high +cdt_active_high=0 +packet_hold_ms=500 +packet_edge_min=4 +packet_window_ms=50 diff --git a/firmware/c1224/include/afsk_bitbang.h b/firmware/c1224/include/afsk_bitbang.h new file mode 100644 index 0000000..886b912 --- /dev/null +++ b/firmware/c1224/include/afsk_bitbang.h @@ -0,0 +1,30 @@ +/** + * Bitbang / timed NRZI on TCM3105 TXD (GP6) and RXD (GP7). + * Rate matches stuffed crystal — compile C1224_BITRATE=1200|2400. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_AFSK_BITBANG_H +#define C1224_AFSK_BITBANG_H + +#include <stddef.h> +#include <stdint.h> +#include <stdbool.h> + +#ifndef C1224_BITRATE +#define C1224_BITRATE 1200 +#endif + +void afsk_bitbang_init(void); + +/** Transmit HDLC info payload (AX.25 frame without flags/FCS). Auto-PTT. */ +bool afsk_tx_frame(const uint8_t *info, size_t info_len); + +/** Poll RX sampler; call often from main loop. May emit via callback. */ +typedef void (*afsk_rx_frame_fn)(const uint8_t *info, size_t len); +void afsk_rx_set_callback(afsk_rx_frame_fn fn); +void afsk_rx_poll(void); + +bool afsk_tx_busy(void); +unsigned afsk_bitrate(void); + +#endif /* C1224_AFSK_BITBANG_H */ diff --git a/firmware/c1224/include/banner.h b/firmware/c1224/include/banner.h new file mode 100644 index 0000000..1592eb7 --- /dev/null +++ b/firmware/c1224/include/banner.h @@ -0,0 +1,12 @@ +/** + * Fixed CDC boot banner — source text only (not INI / CDC keys). + * Always printed once after USB ready; quiet does not suppress it. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_BANNER_H +#define C1224_BANNER_H + +/** Print fixed boot banner (call once after CDC ready). */ +void banner_print(void); + +#endif /* C1224_BANNER_H */ diff --git a/firmware/c1224/include/baud_config.h b/firmware/c1224/include/baud_config.h new file mode 100644 index 0000000..268ca15 --- /dev/null +++ b/firmware/c1224/include/baud_config.h @@ -0,0 +1,14 @@ +#ifndef C1224_BAUD_CONFIG_H +#define C1224_BAUD_CONFIG_H + +/** + * Informational only — physical rate = stuffed crystal (Y1/Y2). + * One FW image; no baud switch; no dual UF2. + */ +typedef struct { + const char *label; /* "crystal" — rate is board stuffing */ +} baud_crystal_config_t; + +const baud_crystal_config_t *baud_config_get(void); + +#endif diff --git a/firmware/c1224/include/board_pins.h b/firmware/c1224/include/board_pins.h new file mode 100644 index 0000000..1422335 --- /dev/null +++ b/firmware/c1224/include/board_pins.h @@ -0,0 +1,52 @@ +/** + * T-Modem-c1224 pin map (service Pico — not softmodem). + * Frozen copper intent (v0.25 schematic) reused as planning default for 180×180. + * GP14/GP15 = unused mux placeholders for v1.0 (single-XTAL; stuff matches image). + */ +#ifndef C1224_BOARD_PINS_H +#define C1224_BOARD_PINS_H + +#include <stdint.h> +#include <stdbool.h> + +/* I2C LCD (shared-kit class; level-shifted on board) */ +#define PIN_I2C_SDA 0 /* GP0 */ +#define PIN_I2C_SCL 1 /* GP1 */ +#define I2C_LCD_BAUDRATE 100000 +#define I2C_LCD_ADDR_DEFAULT 0x27 /* common PCF8574 backpack; probe may fail → log-only */ + +/* Modem chip digital (TCM3105-class) — Pico does NOT generate AFSK */ +#define PIN_MODEM_TXD 6 /* GP6 → chip TXD */ +#define PIN_MODEM_RXD 7 /* GP7 ← chip RXD → Packet UI sense (required · status only) */ +#define PIN_MODEM_CDT 8 /* GP8 ← chip CDT → Signal/RX (required) */ + +/* PTT keying: Pico GPIO → OC → radio PTT (USB CDC service). Not the TX LED. */ +#define PIN_PTT_DRV 9 /* GP9 · required for USB PTT ON */ + +/* + * Status LEDs (c1224 copper — active-high source via series R; LED cathode = GND). + * LED_PWR is NOT a GPIO: +5V_OR → R_LED1 → LED_PWR → GND (rail Power indicator). + * + * Settled UI (2026-07-26): + * - Dual sense REQUIRED: CDT→GP8 Signal/RX · RXD→GP7 Packet (status only, not data bridge). + * - CDT = carrier/tone · RXD activity ≠ full AX.25 decode. + * - LED_PTT (GP10) = OPTIONAL/SPARE — not required product TX indicator. + * - Display TX optional; PTT keying GPIO (GP9) stays separate from LED_PTT. + * - LED_TXRX = spare/activity. + */ +#define PIN_LED_PTT 10 /* GP10 · LED_PTT_N · optional/spare (not required TX LED) */ +#define PIN_LED_DCD 11 /* GP11 · LED_DCD_N · optional RX mirror of CDT */ +#define PIN_LED_TXRX 12 /* GP12 · LED_TXRX_N · spare / optional activity */ + +/* + * Crystal mux placeholders — UNUSED for v1.0. + * Product: one crystal stuffed matching flash (Y1 4.4336 / Y2 6.5536). + * Dual/mux path abandoned; crystal_steer is a no-op. + * Optional keep defines if mux ever returns later. + */ +#define PIN_XTAL_SEL_A 14 /* GP14 — unused v1.0 */ +#define PIN_XTAL_SEL_B 15 /* GP15 — unused v1.0 */ + +void board_pins_init(void); + +#endif /* C1224_BOARD_PINS_H */ diff --git a/firmware/c1224/include/config.h b/firmware/c1224/include/config.h new file mode 100644 index 0000000..07b1093 --- /dev/null +++ b/firmware/c1224/include/config.h @@ -0,0 +1,58 @@ +/** + * Operator INI texts + sense tuning — defaults in FW, overlay from flash. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_CONFIG_H +#define C1224_CONFIG_H + +#include <stdbool.h> +#include <stdint.h> +#include <stddef.h> + +#define C1224_TEXT_MAX 48 + +typedef struct { + char starting[C1224_TEXT_MAX]; + char ready[C1224_TEXT_MAX]; + char fault[C1224_TEXT_MAX]; + char sending[C1224_TEXT_MAX]; + char signal[C1224_TEXT_MAX]; + char packet[C1224_TEXT_MAX]; + char power[C1224_TEXT_MAX]; + char idle[C1224_TEXT_MAX]; + /* Terminal-facing identity (CDC ID) */ + char callsign[C1224_TEXT_MAX]; + char device_id[C1224_TEXT_MAX]; + /* Extra CDC ASCII — quiet=1 default; never suppresses fixed boot banner */ + bool quiet; /* true = no unsolicited STATUS/PROBE/CONFIG ASCII */ + /* Sense tuning */ + bool cdt_active_high; /* false = active-low (default pull-up idle) */ + uint16_t packet_hold_ms; + uint16_t packet_edge_min; + uint16_t packet_window_ms; +} c1224_config_t; + +void config_init(void); +const c1224_config_t *config_get(void); +c1224_config_t *config_mutable(void); +/** True when extra unsolicited CDC ASCII (STATUS/PROBE/CONFIG) is allowed. */ +bool config_unsolicited_ok(void); + +/** Apply defaults (RAM only). */ +void config_load_defaults(void); +/** Load overlay from flash INI region if valid. */ +bool config_load_flash(void); +/** Persist current config as INI text to flash. */ +bool config_save_flash(void); +/** Erase flash overlay (next boot = defaults only until SAVE). */ +bool config_erase_flash(void); + +/** SET KEY=value (case-insensitive key). Returns false if unknown key. */ +bool config_set_kv(const char *key, const char *value); +/** GET KEY → copy value into buf. */ +bool config_get_kv(const char *key, char *buf, size_t buflen); + +/** Dump INI-like text to USB (printf). */ +void config_dump_ini(void); + +#endif diff --git a/firmware/c1224/include/crystal_steer.h b/firmware/c1224/include/crystal_steer.h new file mode 100644 index 0000000..9aa55f9 --- /dev/null +++ b/firmware/c1224/include/crystal_steer.h @@ -0,0 +1,10 @@ +#ifndef C1224_CRYSTAL_STEER_H +#define C1224_CRYSTAL_STEER_H + +#include <stdbool.h> + +/** v1.0 no-op — rate = stuffed crystal; no XTAL mux. */ +void crystal_steer_apply(void); +bool crystal_steer_applied(void); + +#endif diff --git a/firmware/c1224/include/display.h b/firmware/c1224/include/display.h new file mode 100644 index 0000000..1cd78f1 --- /dev/null +++ b/firmware/c1224/include/display.h @@ -0,0 +1,17 @@ +#ifndef C1224_DISPLAY_H +#define C1224_DISPLAY_H + +#include <stdbool.h> + +/** + * I2C HD44780 via PCF8574 backpack (0x27/0x3F). USB always logs. + * Shows Power/ready, Signal, Packet from INI texts. + */ +void display_init(void); +bool display_present(void); +/** Refresh LCD from current status + sense (uses INI texts). */ +void display_update(void); +/** Legacy single-line push (also used before full UI ready). */ +void display_show(const char *line); + +#endif diff --git a/firmware/c1224/include/hdlc.h b/firmware/c1224/include/hdlc.h new file mode 100644 index 0000000..1d64278 --- /dev/null +++ b/firmware/c1224/include/hdlc.h @@ -0,0 +1,49 @@ +/** + * HDLC frame helpers — flags, bit-stuff, FCS-CCITT (AX.25). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_HDLC_H +#define C1224_HDLC_H + +#include <stddef.h> +#include <stdint.h> +#include <stdbool.h> + +#define HDLC_FLAG 0x7Eu +#define HDLC_FCS_INIT 0xFFFFu +#define HDLC_FCS_GOOD 0xF0B8u +#define HDLC_MAX_INFO 512 + +uint16_t hdlc_fcs_update(uint16_t fcs, uint8_t byte); +uint16_t hdlc_fcs_final(uint16_t fcs); + +/** + * Build on-wire bit stream (NRZI already applied by caller after this): + * FLAG + stuffed(info + FCS_lo + FCS_hi) + FLAG(+optional trailing flags). + * Returns bit count written to bits[] (MSB-first within each stuffed byte stream + * is not used — output is a raw bit array, LSB-first per AX.25 bit order). + * + * bits_cap is capacity in bits. Returns 0 on overflow / bad args. + */ +size_t hdlc_encode_bits(const uint8_t *info, size_t info_len, + uint8_t *bits, size_t bits_cap, + unsigned trailing_flags); + +/** + * Feed one NRZ bit (after NRZI decode). Returns true when a complete good + * frame is available in out[] (info only, FCS stripped). + */ +typedef struct { + uint8_t buf[HDLC_MAX_INFO + 2]; + size_t len; + uint8_t ones; + bool in_frame; + bool stuffing; + uint8_t byte_acc; + uint8_t bit_count; +} hdlc_rx_t; + +void hdlc_rx_init(hdlc_rx_t *rx); +bool hdlc_rx_bit(hdlc_rx_t *rx, bool bit, uint8_t *out, size_t *out_len); + +#endif /* C1224_HDLC_H */ diff --git a/firmware/c1224/include/kiss.h b/firmware/c1224/include/kiss.h new file mode 100644 index 0000000..0312bb4 --- /dev/null +++ b/firmware/c1224/include/kiss.h @@ -0,0 +1,34 @@ +/** + * KISS frame encode/decode (FEND 0xC0). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_KISS_H +#define C1224_KISS_H + +#include <stddef.h> +#include <stdint.h> +#include <stdbool.h> + +#define KISS_FEND 0xC0u +#define KISS_FESC 0xDBu +#define KISS_TFEND 0xDCu +#define KISS_TFESC 0xDDu +#define KISS_CMD_DATA 0x00u +#define KISS_MAX_PAYLOAD 512 + +typedef struct { + uint8_t buf[KISS_MAX_PAYLOAD + 4]; + size_t len; + bool in_frame; + bool esc; +} kiss_rx_t; + +void kiss_rx_init(kiss_rx_t *kx); +/** Feed one byte. Returns true when a DATA payload is ready in out. */ +bool kiss_rx_byte(kiss_rx_t *kx, uint8_t b, uint8_t *out, size_t *out_len); + +/** Write KISS DATA frame to dst; returns bytes written (0 on overflow). */ +size_t kiss_encode_data(const uint8_t *payload, size_t len, + uint8_t *dst, size_t dst_cap); + +#endif /* C1224_KISS_H */ diff --git a/firmware/c1224/include/modem_probe.h b/firmware/c1224/include/modem_probe.h new file mode 100644 index 0000000..402e485 --- /dev/null +++ b/firmware/c1224/include/modem_probe.h @@ -0,0 +1,15 @@ +#ifndef C1224_MODEM_PROBE_H +#define C1224_MODEM_PROBE_H + +#include <stdbool.h> + +typedef struct { + bool crystal_driven; + bool cdt_readable; + bool rxd_readable; + bool ready; +} modem_probe_result_t; + +modem_probe_result_t modem_probe_run(void); + +#endif diff --git a/firmware/c1224/include/ptt.h b/firmware/c1224/include/ptt.h new file mode 100644 index 0000000..ca5e7d8 --- /dev/null +++ b/firmware/c1224/include/ptt.h @@ -0,0 +1,10 @@ +#ifndef C1224_PTT_H +#define C1224_PTT_H + +#include <stdbool.h> + +void ptt_init(void); +void ptt_set(bool on); +bool ptt_get(void); + +#endif diff --git a/firmware/c1224/include/sense.h b/firmware/c1224/include/sense.h new file mode 100644 index 0000000..25009e8 --- /dev/null +++ b/firmware/c1224/include/sense.h @@ -0,0 +1,19 @@ +/** + * Dual sense: CDT→Signal/RX, RXD→Packet (HDLC 0x7E / edge activity). + * Status only — not a Host↔Chip data bridge. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_SENSE_H +#define C1224_SENSE_H + +#include <stdbool.h> + +void sense_init(void); +/** Poll GPIO / heuristics; call from main loop (~10 ms). */ +void sense_poll(void); + +bool sense_signal_active(void); /* CDT carrier/tone */ +bool sense_packet_active(void); /* RXD activity or flag seen (held) */ +bool sense_packet_flag(void); /* last window saw 0x7E */ + +#endif diff --git a/firmware/c1224/include/status.h b/firmware/c1224/include/status.h new file mode 100644 index 0000000..fefe6e8 --- /dev/null +++ b/firmware/c1224/include/status.h @@ -0,0 +1,23 @@ +#ifndef C1224_STATUS_H +#define C1224_STATUS_H + +#include <stdbool.h> + +typedef enum { + C1224_STATE_STARTING = 0, + C1224_STATE_READY, + C1224_STATE_FAULT, + C1224_STATE_SENDING, + C1224_STATE_SIGNAL, /* CDT active (Signal/RX) */ + C1224_STATE_PACKET /* RXD packet sense (may coexist with signal) */ +} c1224_state_t; + +/** Operator-facing text for state (from INI). */ +const char *c1224_state_string(c1224_state_t st); +void status_set(c1224_state_t st); +c1224_state_t status_get(void); +/** Recompute composite state from PTT + sense; publish if changed. */ +void status_tick(void); +void status_publish(void); + +#endif diff --git a/firmware/c1224/include/usb_cmd.h b/firmware/c1224/include/usb_cmd.h new file mode 100644 index 0000000..e0fb513 --- /dev/null +++ b/firmware/c1224/include/usb_cmd.h @@ -0,0 +1,11 @@ +#ifndef C1224_USB_CMD_H +#define C1224_USB_CMD_H + +#include <stddef.h> +#include <stdint.h> + +void usb_cmd_init(void); +void usb_cmd_poll(void); +void usb_cmd_on_rx_frame(const uint8_t *info, size_t len); + +#endif diff --git a/firmware/c1224/include/version.h b/firmware/c1224/include/version.h new file mode 100644 index 0000000..a2c81ad --- /dev/null +++ b/firmware/c1224/include/version.h @@ -0,0 +1,11 @@ +/** + * Firmware version SSoT — bump here; banner and docs follow. + * Matches product tag style without leading 'v' (tag: v1.0-prototype). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef C1224_VERSION_H +#define C1224_VERSION_H + +#define C1224_FW_VERSION "1.0-prototype" + +#endif /* C1224_VERSION_H */ 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); +} |
