From 3f4da4bc2b34d72f4d701e319b907192704969fe Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:56:35 +1100 Subject: [PATCH] Enable settings setup over serial - see guide for details --- Serial Settings Guide.md | 315 +++++++++++++ examples/companion_radio/MyMesh.cpp | 445 +++++++++++++++++- .../companion_radio/ui-new/Radiopresets.h | 34 ++ .../companion_radio/ui-new/Settingsscreen.h | 31 +- 4 files changed, 792 insertions(+), 33 deletions(-) create mode 100644 Serial Settings Guide.md create mode 100644 examples/companion_radio/ui-new/Radiopresets.h diff --git a/Serial Settings Guide.md b/Serial Settings Guide.md new file mode 100644 index 0000000..c65ee6f --- /dev/null +++ b/Serial Settings Guide.md @@ -0,0 +1,315 @@ +# Meck Serial Settings Guide + +Configure your T-Deck Pro's Meck firmware over USB serial — no companion app needed. Plug in a USB-C cable, open a serial terminal, and you have full access to every setting on the device. + +## Getting Started + +### What You Need + +- T-Deck Pro running Meck firmware +- USB-C cable +- A serial terminal application: + - **Windows:** PuTTY, TeraTerm, or the Arduino IDE Serial Monitor + - **macOS:** `screen`, CoolTerm, or the Arduino IDE Serial Monitor + - **Linux:** `screen`, `minicom`, `picocom`, or the Arduino IDE Serial Monitor + +### Connection Settings + +| Parameter | Value | +|-----------|-------| +| Baud rate | 115200 | +| Data bits | 8 | +| Parity | None | +| Stop bits | 1 | +| Line ending | CR (carriage return) or CR+LF | + +### Quick Start (macOS / Linux) + +``` +screen /dev/ttyACM0 115200 +``` + +On macOS the port is typically `/dev/cu.usbmodem*`. On Linux it is usually `/dev/ttyACM0` or `/dev/ttyUSB0`. + +### Quick Start (Arduino IDE) + +Open **Tools → Serial Monitor**, set baud to **115200** and line ending to **Carriage Return** or **Both NL & CR**. + +Once connected, type `help` and press Enter to confirm everything is working. + +--- + +## Command Reference + +All commands follow a simple pattern: `get` to read, `set` to write. + +### Viewing Settings + +| Command | Description | +|---------|-------------| +| `get all` | Dump every setting at once | +| `get name` | Device name | +| `get freq` | Radio frequency (MHz) | +| `get bw` | Bandwidth (kHz) | +| `get sf` | Spreading factor | +| `get cr` | Coding rate | +| `get tx` | TX power (dBm) | +| `get radio` | All radio params in one line | +| `get utc` | UTC offset (hours) | +| `get notify` | Keyboard flash notification (on/off) | +| `get gps` | GPS status and interval | +| `get pin` | BLE pairing PIN | +| `get channels` | List all channels with index numbers | +| `get presets` | List all radio presets with parameters | +| `get pubkey` | Device public key (hex) | +| `get firmware` | Firmware version string | + +**4G variant only:** + +| Command | Description | +|---------|-------------| +| `get modem` | Modem enabled/disabled | +| `get apn` | Current APN | +| `get imei` | Device IMEI | + +### Changing Settings + +#### Device Name + +``` +set name MyNode +``` + +Names cannot contain these characters: `[ ] / \ : , ? *` + +#### Radio Parameters (Individual) + +Each of these applies immediately — no reboot required. + +``` +set freq 910.525 +set bw 62.5 +set sf 7 +set cr 5 +set tx 22 +``` + +Valid ranges: + +| Parameter | Min | Max | +|-----------|-----|-----| +| freq | 400.0 | 928.0 | +| bw | 7.8 | 500.0 | +| sf | 5 | 12 | +| cr | 5 | 8 | +| tx | 1 | Board max (typically 22) | + +#### Radio Parameters (All at Once) + +Set frequency, bandwidth, spreading factor, and coding rate in a single command: + +``` +set radio 910.525 62.5 7 5 +``` + +#### Radio Presets + +The easiest way to configure your radio. First, list the available presets: + +``` +get presets +``` + +This prints a numbered list like: + +``` + Available radio presets: + 0 Australia 915.800 MHz BW250.0 SF10 CR5 TX22 + 1 Australia (Narrow) 916.575 MHz BW62.5 SF7 CR8 TX22 + ... + 14 USA/Canada (Recommended) 910.525 MHz BW62.5 SF7 CR5 TX22 + 15 Vietnam 920.250 MHz BW250.0 SF11 CR5 TX22 +``` + +Apply a preset by name or number: + +``` +set preset USA/Canada (Recommended) +set preset 14 +``` + +Preset names are case-insensitive, so `set preset australia` works too. The preset applies all five radio parameters (freq, bw, sf, cr, tx) and takes effect immediately. + +#### UTC Offset + +``` +set utc 10 +``` + +Range: -12 to +14. + +#### Keyboard Notification Flash + +Toggle whether the keyboard backlight flashes when a new message arrives: + +``` +set notify on +set notify off +``` + +#### BLE PIN + +``` +set pin 123456 +``` + +### Channel Management + +#### List Channels + +``` +get channels +``` + +Output: + +``` + [0] #public + [1] #meck-test + [2] #local-group +``` + +#### Add a Hashtag Channel + +``` +set channel.add meck-test +``` + +The `#` prefix is added automatically if you omit it. The channel's encryption key is derived from the name (SHA-256), matching the same method used by the on-device Settings screen and companion apps. + +#### Delete a Channel + +``` +set channel.del 2 +``` + +Channels are referenced by their index number (shown in `get channels`). Channel 0 (public) cannot be deleted. Remaining channels are automatically compacted after deletion. + +### 4G Modem (4G Variant Only) + +#### Enable / Disable Modem + +``` +set modem on +set modem off +``` + +#### Set APN + +``` +set apn telstra.internet +``` + +To clear a custom APN and revert to auto-detection on next boot: + +``` +set apn +``` + +### System Commands + +| Command | Description | +|---------|-------------| +| `reboot` | Restart the device | +| `rebuild` | Erase filesystem, re-save identity + prefs + contacts + channels | +| `erase` | Format the filesystem (caution: loses everything) | +| `ls UserData/` | List files on internal filesystem | +| `ls ExtraFS/` | List files on secondary filesystem | +| `cat UserData/` | Dump file contents as hex | +| `rm UserData/` | Delete a file | +| `help` | Show command summary | + +--- + +## Common Workflows + +### First-Time Setup + +Plug in your new T-Deck Pro and run through these commands to get on the air: + +``` +set name YourCallsign +set preset Australia +set utc 10 +set channel.add local-group +get all +``` + +### Switching to a New Region + +Moving from Australia to the US? One command: + +``` +set preset USA/Canada (Recommended) +``` + +Verify with: + +``` +get radio +``` + +### Custom Radio Configuration + +If none of the presets match your local group or you need specific parameters, set them directly. You can do it all in one command: + +``` +set radio 916.575 62.5 8 8 +set tx 20 +``` + +Or one parameter at a time if you're only adjusting part of your config: + +``` +set freq 916.575 +set bw 62.5 +set sf 8 +set cr 8 +set tx 20 +``` + +Both approaches apply immediately. Confirm with `get radio` to double-check everything took: + +``` +get radio + > freq=916.575 bw=62.5 sf=8 cr=8 tx=20 +``` + +### Troubleshooting Radio Settings + +If you're not sure what went wrong, dump everything: + +``` +get all +``` + +Compare the radio section against what others in your area are using. If you need to match exact parameters from another node: + +``` +set radio 916.575 62.5 7 8 +set tx 22 +``` + +### Backing Up Your Settings + +Use `get all` to capture a snapshot of your configuration. Copy the serial output and save it — you can manually re-enter the settings after a firmware update or device reset if your SD card backup isn't available. + +--- + +## Tips + +- **All radio changes apply live.** There is no need to reboot after changing frequency, bandwidth, spreading factor, coding rate, or TX power. The radio reconfigures on the fly. +- **Preset selection by number is faster.** Once you've seen `get presets`, use the index number instead of typing the full name. +- **Settings are persisted immediately.** Every `set` command writes to flash. If power is lost, your settings are safe. +- **SD card backup is automatic.** If your T-Deck Pro has an SD card inserted, settings are backed up after every change. On a fresh flash, settings restore automatically from the SD card. +- **The `get all` command is your friend.** When in doubt, dump everything and check. \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b47e06f..b2898df 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2,6 +2,11 @@ #include // needed for PlatformIO #include +#include "RadioPresets.h" // Shared radio presets (serial CLI + settings screen) + +#ifdef HAS_4G_MODEM + #include "ModemManager.h" // Serial CLI modem commands +#endif #define CMD_APP_START 1 #define CMD_SEND_TXT_MSG 2 @@ -2031,15 +2036,447 @@ void MyMesh::checkCLIRescueCmd() { if (len > 0 && cli_command[len - 1] == '\r') { // received complete line cli_command[len - 1] = 0; // replace newline with C string null terminator - if (memcmp(cli_command, "set ", 4) == 0) { + // ===================================================================== + // GET commands — read settings + // ===================================================================== + if (memcmp(cli_command, "get ", 4) == 0) { + const char* key = &cli_command[4]; + + if (strcmp(key, "name") == 0) { + Serial.printf(" > %s\n", _prefs.node_name); + } else if (strcmp(key, "freq") == 0) { + Serial.printf(" > %.3f\n", _prefs.freq); + } else if (strcmp(key, "bw") == 0) { + Serial.printf(" > %.1f\n", _prefs.bw); + } else if (strcmp(key, "sf") == 0) { + Serial.printf(" > %d\n", _prefs.sf); + } else if (strcmp(key, "cr") == 0) { + Serial.printf(" > %d\n", _prefs.cr); + } else if (strcmp(key, "tx") == 0) { + Serial.printf(" > %d\n", _prefs.tx_power_dbm); + } else if (strcmp(key, "utc") == 0) { + Serial.printf(" > %d\n", _prefs.utc_offset_hours); + } else if (strcmp(key, "notify") == 0) { + Serial.printf(" > %s\n", _prefs.kb_flash_notify ? "on" : "off"); + } else if (strcmp(key, "gps") == 0) { + Serial.printf(" > %s (interval: %ds)\n", + _prefs.gps_enabled ? "on" : "off", _prefs.gps_interval); + } else if (strcmp(key, "pin") == 0) { + Serial.printf(" > %06d\n", _prefs.ble_pin); + } else if (strcmp(key, "radio") == 0) { + Serial.printf(" > freq=%.3f bw=%.1f sf=%d cr=%d tx=%d\n", + _prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, _prefs.tx_power_dbm); + } else if (strcmp(key, "pubkey") == 0) { + char hex[PUB_KEY_SIZE * 2 + 1]; + mesh::Utils::toHex(hex, self_id.pub_key, PUB_KEY_SIZE); + Serial.printf(" > %s\n", hex); + } else if (strcmp(key, "firmware") == 0) { + Serial.printf(" > %s\n", FIRMWARE_VERSION); + } else if (strcmp(key, "channels") == 0) { + bool found = false; + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails ch; + if (getChannel(i, ch) && ch.name[0] != '\0') { + Serial.printf(" [%d] %s\n", i, ch.name); + found = true; + } else { + break; + } + } + if (!found) Serial.println(" (no channels)"); + } else if (strcmp(key, "presets") == 0) { + Serial.println(" Available radio presets:"); + for (int i = 0; i < (int)NUM_RADIO_PRESETS; i++) { + Serial.printf(" %2d %-30s %.3f MHz BW%.1f SF%d CR%d TX%d\n", + i, RADIO_PRESETS[i].name, RADIO_PRESETS[i].freq, + RADIO_PRESETS[i].bw, RADIO_PRESETS[i].sf, + RADIO_PRESETS[i].cr, RADIO_PRESETS[i].tx_power); + } +#ifdef HAS_4G_MODEM + } else if (strcmp(key, "modem") == 0) { + Serial.printf(" > %s\n", ModemManager::loadEnabledConfig() ? "on" : "off"); + } else if (strcmp(key, "apn") == 0) { + Serial.printf(" > %s\n", modemManager.getAPN()); + } else if (strcmp(key, "imei") == 0) { + Serial.printf(" > %s\n", modemManager.getIMEI()); +#endif + } else if (strcmp(key, "all") == 0) { + Serial.println(" === Meck Device Settings ==="); + Serial.printf(" name: %s\n", _prefs.node_name); + Serial.printf(" freq: %.3f\n", _prefs.freq); + Serial.printf(" bw: %.1f\n", _prefs.bw); + Serial.printf(" sf: %d\n", _prefs.sf); + Serial.printf(" cr: %d\n", _prefs.cr); + Serial.printf(" tx: %d\n", _prefs.tx_power_dbm); + Serial.printf(" utc: %d\n", _prefs.utc_offset_hours); + Serial.printf(" notify: %s\n", _prefs.kb_flash_notify ? "on" : "off"); + Serial.printf(" gps: %s (interval: %ds)\n", + _prefs.gps_enabled ? "on" : "off", _prefs.gps_interval); + Serial.printf(" pin: %06d\n", _prefs.ble_pin); +#ifdef HAS_4G_MODEM + Serial.printf(" modem: %s\n", ModemManager::loadEnabledConfig() ? "on" : "off"); + Serial.printf(" apn: %s\n", modemManager.getAPN()); + Serial.printf(" imei: %s\n", modemManager.getIMEI()); +#endif + // Detect current preset + bool presetFound = false; + for (int i = 0; i < (int)NUM_RADIO_PRESETS; i++) { + if (_prefs.freq == RADIO_PRESETS[i].freq && _prefs.bw == RADIO_PRESETS[i].bw && + _prefs.sf == RADIO_PRESETS[i].sf && _prefs.cr == RADIO_PRESETS[i].cr) { + Serial.printf(" preset: %s\n", RADIO_PRESETS[i].name); + presetFound = true; + break; + } + } + if (!presetFound) Serial.println(" preset: (custom)"); + Serial.printf(" firmware: %s\n", FIRMWARE_VERSION); + char hex[PUB_KEY_SIZE * 2 + 1]; + mesh::Utils::toHex(hex, self_id.pub_key, PUB_KEY_SIZE); + Serial.printf(" pubkey: %s\n", hex); + // List channels + Serial.println(" channels:"); + bool chFound = false; + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails ch; + if (getChannel(i, ch) && ch.name[0] != '\0') { + Serial.printf(" [%d] %s\n", i, ch.name); + chFound = true; + } else { + break; + } + } + if (!chFound) Serial.println(" (none)"); + } else { + Serial.printf(" Error: unknown key '%s' (try 'help')\n", key); + } + + // ===================================================================== + // SET commands — write settings + // ===================================================================== + } else if (memcmp(cli_command, "set ", 4) == 0) { const char* config = &cli_command[4]; - if (memcmp(config, "pin ", 4) == 0) { + + if (memcmp(config, "name ", 5) == 0) { + const char* val = &config[5]; + // Validate name (same rules as CommonCLI) + bool valid = true; + const char* p = val; + while (*p) { + if (*p == '[' || *p == ']' || *p == '/' || *p == '\\' || + *p == ':' || *p == ',' || *p == '?' || *p == '*') { + valid = false; + break; + } + p++; + } + if (valid && strlen(val) > 0) { + strncpy(_prefs.node_name, val, sizeof(_prefs.node_name) - 1); + _prefs.node_name[sizeof(_prefs.node_name) - 1] = '\0'; + savePrefs(); + Serial.printf(" > name = %s\n", _prefs.node_name); + } else { + Serial.println(" Error: invalid name (no []/:,?* chars)"); + } + + } else if (memcmp(config, "freq ", 5) == 0) { + float f = atof(&config[5]); + if (f >= 400.0f && f <= 928.0f) { + _prefs.freq = f; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + Serial.printf(" > freq = %.3f (applied)\n", _prefs.freq); + } else { + Serial.println(" Error: freq out of range (400-928)"); + } + + } else if (memcmp(config, "bw ", 3) == 0) { + float bw = atof(&config[3]); + if (bw >= 7.8f && bw <= 500.0f) { + _prefs.bw = bw; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + Serial.printf(" > bw = %.1f (applied)\n", _prefs.bw); + } else { + Serial.println(" Error: bw out of range (7.8-500)"); + } + + } else if (memcmp(config, "sf ", 3) == 0) { + int sf = atoi(&config[3]); + if (sf >= 5 && sf <= 12) { + _prefs.sf = (uint8_t)sf; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + Serial.printf(" > sf = %d (applied)\n", _prefs.sf); + } else { + Serial.println(" Error: sf out of range (5-12)"); + } + + } else if (memcmp(config, "cr ", 3) == 0) { + int cr = atoi(&config[3]); + if (cr >= 5 && cr <= 8) { + _prefs.cr = (uint8_t)cr; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + Serial.printf(" > cr = %d (applied)\n", _prefs.cr); + } else { + Serial.println(" Error: cr out of range (5-8)"); + } + + } else if (memcmp(config, "tx ", 3) == 0) { + int tx = atoi(&config[3]); + if (tx >= 1 && tx <= MAX_LORA_TX_POWER) { + _prefs.tx_power_dbm = (uint8_t)tx; + savePrefs(); + radio_set_tx_power(_prefs.tx_power_dbm); + Serial.printf(" > tx = %d (applied)\n", _prefs.tx_power_dbm); + } else { + Serial.printf(" Error: tx out of range (1-%d)\n", MAX_LORA_TX_POWER); + } + + } else if (memcmp(config, "utc ", 4) == 0) { + int utc = atoi(&config[4]); + if (utc >= -12 && utc <= 14) { + _prefs.utc_offset_hours = (int8_t)utc; + savePrefs(); + Serial.printf(" > utc = %d\n", _prefs.utc_offset_hours); + } else { + Serial.println(" Error: utc out of range (-12 to 14)"); + } + + } else if (memcmp(config, "notify ", 7) == 0) { + if (strcmp(&config[7], "on") == 0) { + _prefs.kb_flash_notify = 1; + } else if (strcmp(&config[7], "off") == 0) { + _prefs.kb_flash_notify = 0; + } else { + Serial.println(" Error: use 'on' or 'off'"); + cli_command[0] = 0; + return; + } + savePrefs(); + Serial.printf(" > notify = %s\n", _prefs.kb_flash_notify ? "on" : "off"); + + } else if (memcmp(config, "pin ", 4) == 0) { _prefs.ble_pin = atoi(&config[4]); savePrefs(); Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); + + } else if (memcmp(config, "radio ", 6) == 0) { + // Composite: "set radio " + char tmp[64]; + strncpy(tmp, &config[6], sizeof(tmp) - 1); + tmp[sizeof(tmp) - 1] = '\0'; + const char* parts[4]; + int num = mesh::Utils::parseTextParts(tmp, parts, 4); + if (num == 4) { + float freq = strtof(parts[0], nullptr); + float bw = strtof(parts[1], nullptr); + int sf = atoi(parts[2]); + int cr = atoi(parts[3]); + if (freq >= 400.0f && freq <= 928.0f && bw >= 7.8f && bw <= 500.0f + && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8) { + _prefs.freq = freq; + _prefs.bw = bw; + _prefs.sf = (uint8_t)sf; + _prefs.cr = (uint8_t)cr; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + Serial.printf(" > radio = %.3f/%.1f/SF%d/CR%d TX:%d (applied)\n", + _prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, _prefs.tx_power_dbm); + } else { + Serial.println(" Error: invalid radio params"); + } + } else { + Serial.println(" Usage: set radio "); + } + + } else if (memcmp(config, "preset ", 7) == 0) { + const char* name = &config[7]; + // Try exact match first (case-insensitive) + bool found = false; + for (int i = 0; i < (int)NUM_RADIO_PRESETS; i++) { + if (strcasecmp(RADIO_PRESETS[i].name, name) == 0) { + _prefs.freq = RADIO_PRESETS[i].freq; + _prefs.bw = RADIO_PRESETS[i].bw; + _prefs.sf = RADIO_PRESETS[i].sf; + _prefs.cr = RADIO_PRESETS[i].cr; + _prefs.tx_power_dbm = RADIO_PRESETS[i].tx_power; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + Serial.printf(" > Applied preset '%s' (%.3f/%.1f/SF%d/CR%d TX:%d)\n", + RADIO_PRESETS[i].name, _prefs.freq, _prefs.bw, + _prefs.sf, _prefs.cr, _prefs.tx_power_dbm); + found = true; + break; + } + } + // Try by index number if name didn't match + if (!found) { + char* endp; + long idx = strtol(name, &endp, 10); + if (endp != name && *endp == '\0' && idx >= 0 && idx < (int)NUM_RADIO_PRESETS) { + _prefs.freq = RADIO_PRESETS[idx].freq; + _prefs.bw = RADIO_PRESETS[idx].bw; + _prefs.sf = RADIO_PRESETS[idx].sf; + _prefs.cr = RADIO_PRESETS[idx].cr; + _prefs.tx_power_dbm = RADIO_PRESETS[idx].tx_power; + savePrefs(); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + radio_set_tx_power(_prefs.tx_power_dbm); + Serial.printf(" > Applied preset '%s' (%.3f/%.1f/SF%d/CR%d TX:%d)\n", + RADIO_PRESETS[idx].name, _prefs.freq, _prefs.bw, + _prefs.sf, _prefs.cr, _prefs.tx_power_dbm); + found = true; + } + } + if (!found) { + Serial.printf(" Error: unknown preset '%s' (try 'get presets')\n", name); + } + + } else if (memcmp(config, "channel.add ", 12) == 0) { + const char* name = &config[12]; + if (strlen(name) == 0) { + Serial.println(" Error: channel name required"); + cli_command[0] = 0; + return; + } + // Build channel name with # prefix if not present + char chanName[32]; + if (name[0] == '#') { + strncpy(chanName, name, sizeof(chanName)); + } else { + chanName[0] = '#'; + strncpy(&chanName[1], name, sizeof(chanName) - 1); + } + chanName[31] = '\0'; + + // Generate 128-bit PSK from SHA-256 of channel name + ChannelDetails newCh; + memset(&newCh, 0, sizeof(newCh)); + strncpy(newCh.name, chanName, sizeof(newCh.name)); + newCh.name[31] = '\0'; + + uint8_t hash[32]; + mesh::Utils::sha256(hash, 32, (const uint8_t*)chanName, strlen(chanName)); + memcpy(newCh.channel.secret, hash, 16); + + // Find next empty slot + bool added = false; + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails existing; + if (!getChannel(i, existing) || existing.name[0] == '\0') { + if (setChannel(i, newCh)) { + saveChannels(); + Serial.printf(" > Added channel '%s' at slot %d\n", chanName, i); + added = true; + } + break; + } + } + if (!added) Serial.println(" Error: no empty channel slots"); + + } else if (memcmp(config, "channel.del ", 12) == 0) { + int idx = atoi(&config[12]); + if (idx <= 0) { + Serial.println(" Error: cannot delete channel 0 (public)"); + } else if (idx >= MAX_GROUP_CHANNELS) { + Serial.printf(" Error: index out of range (1-%d)\n", MAX_GROUP_CHANNELS - 1); + } else { + // Verify channel exists + ChannelDetails ch; + if (!getChannel(idx, ch) || ch.name[0] == '\0') { + Serial.printf(" Error: no channel at index %d\n", idx); + } else { + // Compact: shift channels down + int total = 0; + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails tmp; + if (getChannel(i, tmp) && tmp.name[0] != '\0') { + total = i + 1; + } else { + break; + } + } + for (int i = idx; i < total - 1; i++) { + ChannelDetails next; + if (getChannel(i + 1, next)) { + setChannel(i, next); + } + } + ChannelDetails empty; + memset(&empty, 0, sizeof(empty)); + setChannel(total - 1, empty); + saveChannels(); + Serial.printf(" > Deleted channel %d ('%s'), compacted %d channels\n", + idx, ch.name, total); + } + } + +#ifdef HAS_4G_MODEM + } else if (memcmp(config, "apn ", 4) == 0) { + const char* apn = &config[4]; + if (strlen(apn) > 0) { + modemManager.setAPN(apn); + Serial.printf(" > apn = %s\n", apn); + } else { + ModemManager::saveAPNConfig(""); + Serial.println(" > apn cleared (will auto-detect on next boot)"); + } + + } else if (strcmp(config, "modem on") == 0) { + ModemManager::saveEnabledConfig(true); + modemManager.begin(); + Serial.println(" > modem enabled"); + + } else if (strcmp(config, "modem off") == 0) { + ModemManager::saveEnabledConfig(false); + modemManager.shutdown(); + Serial.println(" > modem disabled"); +#endif + } else { - Serial.printf(" Error: unknown config: %s\n", config); + Serial.printf(" Error: unknown setting '%s' (try 'help')\n", config); } + + // ===================================================================== + // HELP command + // ===================================================================== + } else if (strcmp(cli_command, "help") == 0) { + Serial.println("=== Meck Serial CLI ==="); + Serial.println(" get Read a setting"); + Serial.println(" set Write a setting"); + Serial.println(""); + Serial.println(" Settings keys:"); + Serial.println(" name, freq, bw, sf, cr, tx, utc, notify, pin"); + Serial.println(""); + Serial.println(" Compound commands:"); + Serial.println(" get all Dump all settings"); + Serial.println(" get radio Show all radio params"); + Serial.println(" get channels List channels"); + Serial.println(" get presets List radio presets"); + Serial.println(" get pubkey Show public key"); + Serial.println(" get firmware Show firmware version"); + Serial.println(" set radio Set all radio params"); + Serial.println(" set preset Apply radio preset"); + Serial.println(" set channel.add Add hashtag channel"); + Serial.println(" set channel.del Delete channel by index"); +#ifdef HAS_4G_MODEM + Serial.println(""); + Serial.println(" 4G modem:"); + Serial.println(" get/set apn, get imei, set modem on/off"); +#endif + Serial.println(""); + Serial.println(" System:"); + Serial.println(" rebuild Erase & rebuild filesystem"); + Serial.println(" erase Format filesystem"); + Serial.println(" reboot Restart device"); + Serial.println(" ls / cat / rm File operations"); + + // ===================================================================== + // Existing system commands (unchanged) + // ===================================================================== } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { @@ -2179,7 +2616,7 @@ void MyMesh::checkCLIRescueCmd() { } else if (strcmp(cli_command, "reboot") == 0) { board.reboot(); // doesn't return } else { - Serial.println(" Error: unknown command"); + Serial.println(" Error: unknown command (try 'help')"); } cli_command[0] = 0; // reset command buffer diff --git a/examples/companion_radio/ui-new/Radiopresets.h b/examples/companion_radio/ui-new/Radiopresets.h new file mode 100644 index 0000000..88a49d8 --- /dev/null +++ b/examples/companion_radio/ui-new/Radiopresets.h @@ -0,0 +1,34 @@ +#pragma once + +// --------------------------------------------------------------------------- +// Radio presets — shared between SettingsScreen (UI) and MyMesh (Serial CLI) +// --------------------------------------------------------------------------- + +struct RadioPreset { + const char* name; + float freq; + float bw; + uint8_t sf; + uint8_t cr; + uint8_t tx_power; +}; + +static const RadioPreset RADIO_PRESETS[] = { + { "Australia", 915.800f, 250.0f, 10, 5, 22 }, + { "Australia (Narrow)", 916.575f, 62.5f, 7, 8, 22 }, + { "Australia: SA, WA", 923.125f, 62.5f, 8, 8, 22 }, + { "Australia: QLD", 923.125f, 62.5f, 8, 5, 22 }, + { "EU/UK (Narrow)", 869.618f, 62.5f, 8, 8, 14 }, + { "EU/UK (Long Range)", 869.525f, 250.0f, 11, 5, 14 }, + { "EU/UK (Medium Range)", 869.525f, 250.0f, 10, 5, 14 }, + { "Czech Republic (Narrow)",869.432f, 62.5f, 7, 5, 14 }, + { "EU 433 (Long Range)", 433.650f, 250.0f, 11, 5, 14 }, + { "New Zealand", 917.375f, 250.0f, 11, 5, 22 }, + { "New Zealand (Narrow)", 917.375f, 62.5f, 7, 5, 22 }, + { "Portugal 433", 433.375f, 62.5f, 9, 6, 14 }, + { "Portugal 868", 869.618f, 62.5f, 7, 6, 14 }, + { "Switzerland", 869.618f, 62.5f, 8, 8, 14 }, + { "USA/Canada (Recommended)",910.525f, 62.5f, 7, 5, 22 }, + { "Vietnam", 920.250f, 250.0f, 11, 5, 22 }, +}; +#define NUM_RADIO_PRESETS (sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0])) \ No newline at end of file diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 076c2ed..08f5531 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -40,36 +40,9 @@ extern MyMesh the_mesh; #define CONTACT_MODE_COUNT 3 // --------------------------------------------------------------------------- -// Radio presets +// Radio presets (shared with Serial CLI in MyMesh.cpp) // --------------------------------------------------------------------------- -struct RadioPreset { - const char* name; - float freq; - float bw; - uint8_t sf; - uint8_t cr; - uint8_t tx_power; -}; - -static const RadioPreset RADIO_PRESETS[] = { - { "Australia", 915.800f, 250.0f, 10, 5, 22 }, - { "Australia (Narrow)", 916.575f, 62.5f, 7, 8, 22 }, - { "Australia: SA, WA", 923.125f, 62.5f, 8, 8, 22 }, - { "Australia: QLD", 923.125f, 62.5f, 8, 5, 22 }, - { "EU/UK (Narrow)", 869.618f, 62.5f, 8, 8, 14 }, - { "EU/UK (Long Range)", 869.525f, 250.0f, 11, 5, 14 }, - { "EU/UK (Medium Range)", 869.525f, 250.0f, 10, 5, 14 }, - { "Czech Republic (Narrow)",869.432f, 62.5f, 7, 5, 14 }, - { "EU 433 (Long Range)", 433.650f, 250.0f, 11, 5, 14 }, - { "New Zealand", 917.375f, 250.0f, 11, 5, 22 }, - { "New Zealand (Narrow)", 917.375f, 62.5f, 7, 5, 22 }, - { "Portugal 433", 433.375f, 62.5f, 9, 6, 14 }, - { "Portugal 868", 869.618f, 62.5f, 7, 6, 14 }, - { "Switzerland", 869.618f, 62.5f, 8, 8, 14 }, - { "USA/Canada (Recommended)",910.525f, 62.5f, 7, 5, 22 }, - { "Vietnam", 920.250f, 250.0f, 11, 5, 22 }, -}; -#define NUM_RADIO_PRESETS (sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0])) +#include "RadioPresets.h" // --------------------------------------------------------------------------- // Settings row types