diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 30435b496..8aa9a805c 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -830,6 +830,7 @@ jobs: python3 -B test/test_companion_hil_startup_frequency.py -v python3 -B test/test_companion_board_power_commands.py -v python3 -B test/test_companion_radio_diagnostics.py -v + python3 -B test/test_companion_help_writer.py -v python3 -B test/test_sdcard_cli_parity.py -v python3 -B test/test_esp32_buffered_radio_targets.py -v python3 -B test/test_xiao_battery_safety.py -v diff --git a/examples/companion_radio/CompanionHelp.inc b/examples/companion_radio/CompanionHelp.inc new file mode 100644 index 000000000..e706dbf23 --- /dev/null +++ b/examples/companion_radio/CompanionHelp.inc @@ -0,0 +1,303 @@ +// Reusable help fragments. The caller selects printing or aggregation. +// Keep feature gates here so both paths expose the same commands and options. +#if !defined(MESH_COMPANION_HELP_BEGIN) || !defined(MESH_COMPANION_HELP_END) \ +|| !defined(MESH_COMPANION_HELP_LINE_BEGIN) || !defined(MESH_COMPANION_HELP_LINE_END) +#error "CompanionHelp.inc requires all four help emission macros" +#endif +MESH_COMPANION_HELP_BEGIN + "Commands:\r\n" + " stats-core / stats-radio / stats-radio-diag / stats-packets\r\n" + " get prv.key (if export enabled)\r\n" +#if defined(ESP32_PLATFORM) && defined(WIFI_SSID) + " password (shared setup WiFi, WebConfig and WiFi OTA)\r\n" + " get password (management password status)\r\n" +#else + " get password (no admin password)\r\n" +#endif +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " erase (settings and identity)\r\n" + " board\r\n" + " version\r\n" + " get storage.layout\r\n" +MESH_COMPANION_HELP_END +#if MESH_ENABLE_TX_BEACONS +MESH_COMPANION_HELP_BEGIN + " get beacon [slot] / set beacon ...\r\n" + " delete beacon / set beacon.interval <12|24|36|48> (default 24h)\r\n" +MESH_COMPANION_HELP_END +#endif +#if MESH_ENABLE_MESHTEXT_CHAT +MESH_COMPANION_HELP_BEGIN + " get mt.status / get mt.region / get mt.slot / get mt.hops\r\n" + " get mt.node / get mt.shortname\r\n" + " get mt.rebroadcast / set mt.rebroadcast (saved; default off)\r\n" + " Relay decoded channel text only; adds 5 packet airtimes before TX\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set mt.region \r\n" + " set mt.slot <0|1-based frequency slot>\r\n" + " set mt.hops <0..7> (saved; default 7)\r\n" + " set mt.node (saved; use a unique ID)\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set mt.shortname <1..4 UTF-8 characters|default> (saved; NodeInfo uses 4 bytes)\r\n" + " send mt.nodeinfo (GPS-free; also automatic after first chat)\r\n" + " set mt.preset (saved manual radio2)\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set mt.channel <32/64 hex digits|AQ==>\r\n" + " get mt.channel / delete mt.channel \r\n" + " Join #mt-longfast or #mt-shortturbo in the app for automatic radio2\r\n" +MESH_COMPANION_HELP_END +#endif +#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH) +MESH_COMPANION_HELP_BEGIN + " get flash.health\r\n" +MESH_COMPANION_HELP_END +#endif +#if COMPANION_FEATURE_READER +MESH_COMPANION_HELP_BEGIN + " get reader [:[-]] [page] (World English Bible, offline)\r\n" +MESH_COMPANION_HELP_END +#endif +MESH_COMPANION_HELP_BEGIN + " get/set tx.media auto|radio|radio2|both|off (binary media)\r\n" + " get/set tx.ssh auto|radio|radio2|both|off (tagged tunnel data)\r\n" +MESH_COMPANION_HELP_END +#if MESH_ENABLE_ETHERNET_TUNNEL +MESH_COMPANION_HELP_BEGIN + " get ethernet / get ethernet.radio\r\n" + " set ethernet.radio ,,,[,]\r\n" + " set ethernet on|off (saved; trusted-LAN KISS TCP 5003, radio2 only)\r\n" +MESH_COMPANION_HELP_END +#endif +#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) +MESH_COMPANION_HELP_BEGIN + " get eth / set eth on|off (physical RAK13800; default off, this boot)\r\n" +MESH_COMPANION_HELP_END +#endif +#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) +MESH_COMPANION_HELP_BEGIN + " get sdcard [free|ls|dir [page]|format|erase|*]\r\n" + " set sdcard format|erase [--force]\r\n" + " ota cache [on|off] (SD archive)\r\n" +MESH_COMPANION_HELP_END +#endif +MESH_COMPANION_HELP_BEGIN + " power / get pwrmgt.support / get pwrmgt.source / get pwrmgt.bootmv\r\n" + " get system.watchdog (hardware status)\r\n" + " get bootloader.ver\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " get pwrmgt.bootreason\r\n" +#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS + " memory\r\n" +#endif + " get/set display.rotation <0|90|180|270>\r\n" +#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS) + " get/set display.controller (saved; restart to apply)\r\n" +#endif +#ifdef PIN_BUZZER + " get/set buzzer on|off\r\n" +#endif +#if defined(PIN_VIBRATION) || defined(HAS_DRV2605) + " get/set vibration on|off\r\n" +#endif +MESH_COMPANION_HELP_END +#if COMPANION_FEATURE_NOTIFICATIONS +MESH_COMPANION_HELP_BEGIN + " get notify / get notify.rules / get notify.gpio.pins\r\n" + " set notify.{sound|vibration|led|screen|gpio} on|off\r\n" + " set notify. \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set notify.remote on|off (!notify, max 15s, no GPIO)\r\n" + " notify.test / notify.stop / notify.delete \r\n" +MESH_COMPANION_HELP_END +#endif +MESH_COMPANION_HELP_BEGIN + " get/set display.inbox \r\n" + " get/set display.touch (this boot)\r\n" + " get public.key\r\n" + " set {name|lat|lon|freq|tx|af} {value}\r\n" + " get wifi|espnow|2.4ghz\r\n" + " set wifi|espnow on|off [force] (force only with off)\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set 2.4ghz on [all|force]|off [force] (this boot)\r\n" + " get/set bluetooth.name \r\n" +MESH_COMPANION_HELP_END +#if defined(BLE_PIN_CODE) +MESH_COMPANION_HELP_BEGIN + " get bluetooth (alias: get ble)\r\n" + " set bluetooth [force] (saved; force only with off)\r\n" + " get bluetooth.mac\r\n" + " set bluetooth.mac \r\n" + " get/set bluetooth.stealth on|off\r\n" +MESH_COMPANION_HELP_END +#endif +MESH_COMPANION_HELP_BEGIN +#if MESH_ENABLE_LOST_REPLY + " get/set lost.reply \r\n" +#endif + " set pin <0-999999>\r\n" + " powersaving [on|off]\r\n" +#if MESH_USB_LOGGING_AVAILABLE + " get/set usb.logging [reboot]\r\n" + " get/set usb.debug \r\n" +#if MESH_USB_CONSOLE_COOPERATIVE + " get usb.watchdog.last\r\n" + " get/set usb.watchdog (saved; default auto)\r\n" +#endif +#endif +MESH_COMPANION_HELP_END +#if defined(ESP32) && defined(WIFI_SSID) +#if defined(COMPANION_RADIO_FULL) +MESH_COMPANION_HELP_BEGIN + " start ota [ap] (WiFi firmware update on port 8080)\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " stop ota\r\n" +MESH_COMPANION_HELP_END +#endif +#if defined(COMPANION_EXCLUSIVE_WIFI_BLE) +MESH_COMPANION_HELP_BEGIN + " get companion.transport\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set companion.transport \r\n" +MESH_COMPANION_HELP_END +#endif +MESH_COMPANION_HELP_BEGIN + " get wifi.powersave\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set wifi.powersave \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " get wifi.protocol\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " get wifi.status (connection, saved/boot/live settings)\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set wifi.protocol (saved; reboot required)\r\n" +MESH_COMPANION_HELP_END +#if !defined(COMPANION_EXCLUSIVE_WIFI_BLE) +MESH_COMPANION_HELP_BEGIN + " get wifi.mode\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set wifi.mode (saved; reboot required)\r\n" +MESH_COMPANION_HELP_END +#endif +#ifdef WITH_WEBCONFIG +MESH_COMPANION_HELP_BEGIN + " get wifi.{ssid|pwd}\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set wifi.ssid \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set wifi.pwd \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " get webui\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set webui \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " start webconfig [ap]\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " stop webconfig\r\n" +MESH_COMPANION_HELP_END +#endif +#endif +#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW +MESH_COMPANION_HELP_BEGIN + " get espnow.channel\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " set espnow.channel <1-13>\r\n" +MESH_COMPANION_HELP_END +#endif +// Keep native USB producer writes within 256 bytes without changing help text. +MESH_COMPANION_HELP_BEGIN + " get radio.rxps.config\r\n" + " get/set radio.rxps \r\n" + " get/set radio.cad \r\n" + " set radio.cad timings \r\n" + " get/set radio.rxgain \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " get rxps.wd\r\n" + " get/set radio.rxps.rfrx_disabled (this boot)\r\n" + " clear stats\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " get/set radio.fem.rxgain \r\n" + " get/set radio.fem.txgain \r\n" + " get/set flood.retry.count <0-15>\r\n" + " get/set flood.retry.path <0-63|off>\r\n" + " get/set flood.retry.group.path <0-63|off>\r\n" + " get/set flood.retry.advert \r\n" + " card\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " import \r\n" + " clock\r\n" + " time \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " list [n]\r\n" + " show [adverts|channels|emergency] [on|off]\r\n" + " to [name|prefix]\r\n" + " path [direct|clear|hop,...]\r\n" + " send \r\n" + " login \r\n" + " cmd \r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " trace [name|prefix]\r\n" + " trace path <1|2|4> \r\n" + " advert\r\n" + " reset path\r\n" + " public \r\n" + " channels\r\n" + " channel \r\n" +MESH_COMPANION_HELP_END +#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH) +MESH_COMPANION_HELP_BEGIN + " repair extrafs (erases/rebuilds internal ExtraFS)\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " scan extrafs erase (USB only; reboot for destructive page test)\r\n" +MESH_COMPANION_HELP_END +#endif +#if COMPANION_FEATURE_TEMP_RADIO || COMPANION_FEATURE_OTA_CLI +#if COMPANION_FEATURE_TEMP_RADIO +MESH_COMPANION_HELP_BEGIN + " tempradio [freq,bw,sf,cr,minutes]\r\n" +MESH_COMPANION_HELP_END +MESH_COMPANION_HELP_BEGIN + " normalradio\r\n" +MESH_COMPANION_HELP_END +#endif +#if COMPANION_FEATURE_OTA_CLI +MESH_COMPANION_HELP_BEGIN + " ota {status|ls|announce|folder|config|...}\r\n" +MESH_COMPANION_HELP_END +#endif +#endif +MESH_COMPANION_HELP_BEGIN + " reboot\r\n" + " ver\r\n" +MESH_COMPANION_HELP_END +#if defined(NRF52_PLATFORM) +MESH_COMPANION_HELP_LINE_BEGIN + " uf2reset" +MESH_COMPANION_HELP_LINE_END +#endif diff --git a/examples/companion_radio/CompanionHelpWriter.h b/examples/companion_radio/CompanionHelpWriter.h new file mode 100644 index 000000000..bb26d1412 --- /dev/null +++ b/examples/companion_radio/CompanionHelpWriter.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace mesh { namespace companion { + +// Native USB accepts bounded producer writes. Honor partial progress, and stop +// if the sink cannot accept another byte rather than spinning on a closed host. +template +size_t writeHelpChunks(Output& output, const char* text, size_t length) { + if (!text) return 0; + size_t written = 0; + while (written < length) { + const size_t remaining = length - written; + const size_t chunk = remaining > 256 ? 256 : remaining; + const size_t accepted = output.write( + reinterpret_cast(text) + written, chunk); + if (accepted == 0 || accepted > chunk) break; + written += accepted; + } + return written; +} + +} } // namespace mesh::companion diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 3223fc684..dd9140086 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -21,6 +21,9 @@ #include #include "NotificationSettingsFile.h" #include "MyMesh.h" +#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) +#include "CompanionHelpWriter.h" +#endif #include #include #include @@ -10401,223 +10404,30 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, FIRMWARE_VERSION, (unsigned)FIRMWARE_VER_CODE, FIRMWARE_BUILD_DATE); } else if (strcmp(command, "help") == 0) { auto& output = terminalOutput(); - output.print( - "Commands:\r\n" - " stats-core / stats-radio / stats-radio-diag / stats-packets\r\n" - " get prv.key (if export enabled)\r\n" -#if defined(ESP32_PLATFORM) && defined(WIFI_SSID) - " password (shared setup WiFi, WebConfig and WiFi OTA)\r\n" - " get password (management password status)\r\n" +#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) + // Keep every help byte and feature gate while sharing the printing code. + static const char help_text[] = +#define MESH_COMPANION_HELP_BEGIN +#define MESH_COMPANION_HELP_END +#define MESH_COMPANION_HELP_LINE_BEGIN +#define MESH_COMPANION_HELP_LINE_END "\r\n" +#include "CompanionHelp.inc" +#undef MESH_COMPANION_HELP_BEGIN +#undef MESH_COMPANION_HELP_END +#undef MESH_COMPANION_HELP_LINE_BEGIN +#undef MESH_COMPANION_HELP_LINE_END + ; + mesh::companion::writeHelpChunks(output, help_text, sizeof(help_text) - 1); #else - " get password (no admin password)\r\n" -#endif - ); - output.print( - " erase (settings and identity)\r\n" - " board\r\n" - " version\r\n" - " get storage.layout\r\n"); -#if MESH_ENABLE_TX_BEACONS - output.print( - " get beacon [slot] / set beacon ...\r\n" - " delete beacon / set beacon.interval <12|24|36|48> (default 24h)\r\n"); -#endif -#if MESH_ENABLE_MESHTEXT_CHAT - output.print( - " get mt.status / get mt.region / get mt.slot / get mt.hops\r\n" - " get mt.node / get mt.shortname\r\n" - " get mt.rebroadcast / set mt.rebroadcast (saved; default off)\r\n" - " Relay decoded channel text only; adds 5 packet airtimes before TX\r\n"); - output.print( - " set mt.region \r\n" - " set mt.slot <0|1-based frequency slot>\r\n" - " set mt.hops <0..7> (saved; default 7)\r\n" - " set mt.node (saved; use a unique ID)\r\n"); - output.print( - " set mt.shortname <1..4 UTF-8 characters|default> (saved; NodeInfo uses 4 bytes)\r\n" - " send mt.nodeinfo (GPS-free; also automatic after first chat)\r\n" - " set mt.preset (saved manual radio2)\r\n"); - output.print( - " set mt.channel <32/64 hex digits|AQ==>\r\n" - " get mt.channel / delete mt.channel \r\n" - " Join #mt-longfast or #mt-shortturbo in the app for automatic radio2\r\n"); -#endif -#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH) - output.print(" get flash.health\r\n"); -#endif -#if COMPANION_FEATURE_READER - output.print(" get reader [:[-]] [page] (World English Bible, offline)\r\n"); -#endif - output.print( - " get/set tx.media auto|radio|radio2|both|off (binary media)\r\n" - " get/set tx.ssh auto|radio|radio2|both|off (tagged tunnel data)\r\n"); -#if MESH_ENABLE_ETHERNET_TUNNEL - output.print( - " get ethernet / get ethernet.radio\r\n" - " set ethernet.radio ,,,[,]\r\n" - " set ethernet on|off (saved; trusted-LAN KISS TCP 5003, radio2 only)\r\n"); -#endif -#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) - output.print( - " get eth / set eth on|off (physical RAK13800; default off, this boot)\r\n"); -#endif -#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE) - output.print( - " get sdcard [free|ls|dir [page]|format|erase|*]\r\n" - " set sdcard format|erase [--force]\r\n" - " ota cache [on|off] (SD archive)\r\n"); -#endif - output.print( - " power / get pwrmgt.support / get pwrmgt.source / get pwrmgt.bootmv\r\n" - " get system.watchdog (hardware status)\r\n" - " get bootloader.ver\r\n"); - output.print( - " get pwrmgt.bootreason\r\n" -#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS - " memory\r\n" -#endif - " get/set display.rotation <0|90|180|270>\r\n" -#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS) - " get/set display.controller (saved; restart to apply)\r\n" -#endif -#ifdef PIN_BUZZER - " get/set buzzer on|off\r\n" -#endif -#if defined(PIN_VIBRATION) || defined(HAS_DRV2605) - " get/set vibration on|off\r\n" -#endif - ); -#if COMPANION_FEATURE_NOTIFICATIONS - output.print( - " get notify / get notify.rules / get notify.gpio.pins\r\n" - " set notify.{sound|vibration|led|screen|gpio} on|off\r\n" - " set notify. \r\n"); - output.print( - " set notify.remote on|off (!notify, max 15s, no GPIO)\r\n" - " notify.test / notify.stop / notify.delete \r\n"); -#endif - output.print( - " get/set display.inbox \r\n" - " get/set display.touch (this boot)\r\n" - " get public.key\r\n" - " set {name|lat|lon|freq|tx|af} {value}\r\n" - " get wifi|espnow|2.4ghz\r\n" - " set wifi|espnow on|off [force] (force only with off)\r\n"); - output.print( - " set 2.4ghz on [all|force]|off [force] (this boot)\r\n" - " get/set bluetooth.name \r\n"); -#if defined(BLE_PIN_CODE) - output.print( - " get bluetooth (alias: get ble)\r\n" - " set bluetooth [force] (saved; force only with off)\r\n" - " get bluetooth.mac\r\n" - " set bluetooth.mac \r\n" - " get/set bluetooth.stealth on|off\r\n"); -#endif - output.print( -#if MESH_ENABLE_LOST_REPLY - " get/set lost.reply \r\n" -#endif - " set pin <0-999999>\r\n" - " powersaving [on|off]\r\n" -#if MESH_USB_LOGGING_AVAILABLE - " get/set usb.logging [reboot]\r\n" - " get/set usb.debug \r\n" -#if MESH_USB_CONSOLE_COOPERATIVE - " get usb.watchdog.last\r\n" - " get/set usb.watchdog (saved; default auto)\r\n" -#endif -#endif - ); -#if defined(ESP32) && defined(WIFI_SSID) -#if defined(COMPANION_RADIO_FULL) - output.print(" start ota [ap] (WiFi firmware update on port 8080)\r\n"); - output.print(" stop ota\r\n"); -#endif -#if defined(COMPANION_EXCLUSIVE_WIFI_BLE) - output.print(" get companion.transport\r\n"); - output.print(" set companion.transport \r\n"); -#endif - output.print(" get wifi.powersave\r\n"); - output.print(" set wifi.powersave \r\n"); - output.print(" get wifi.protocol\r\n"); - output.print(" get wifi.status (connection, saved/boot/live settings)\r\n"); - output.print(" set wifi.protocol (saved; reboot required)\r\n"); -#if !defined(COMPANION_EXCLUSIVE_WIFI_BLE) - output.print(" get wifi.mode\r\n"); - output.print(" set wifi.mode (saved; reboot required)\r\n"); -#endif -#ifdef WITH_WEBCONFIG - output.print(" get wifi.{ssid|pwd}\r\n"); - output.print(" set wifi.ssid \r\n"); - output.print(" set wifi.pwd \r\n"); - output.print(" get webui\r\n"); - output.print(" set webui \r\n"); - output.print(" start webconfig [ap]\r\n"); - output.print(" stop webconfig\r\n"); -#endif -#endif -#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW - output.print(" get espnow.channel\r\n"); - output.print(" set espnow.channel <1-13>\r\n"); -#endif - // Keep native USB producer writes within 256 bytes without changing help text. - output.print( - " get radio.rxps.config\r\n" - " get/set radio.rxps \r\n" - " get/set radio.cad \r\n" - " set radio.cad timings \r\n" - " get/set radio.rxgain \r\n"); - output.print( - " get rxps.wd\r\n" - " get/set radio.rxps.rfrx_disabled (this boot)\r\n" - " clear stats\r\n"); - output.print( - " get/set radio.fem.rxgain \r\n" - " get/set radio.fem.txgain \r\n" - " get/set flood.retry.count <0-15>\r\n" - " get/set flood.retry.path <0-63|off>\r\n" - " get/set flood.retry.group.path <0-63|off>\r\n" - " get/set flood.retry.advert \r\n" - " card\r\n"); - output.print( - " import \r\n" - " clock\r\n" - " time \r\n"); - output.print( - " list [n]\r\n" - " show [adverts|channels|emergency] [on|off]\r\n" - " to [name|prefix]\r\n" - " path [direct|clear|hop,...]\r\n" - " send \r\n" - " login \r\n" - " cmd \r\n"); - output.print( - " trace [name|prefix]\r\n" - " trace path <1|2|4> \r\n" - " advert\r\n" - " reset path\r\n" - " public \r\n" - " channels\r\n" - " channel \r\n"); -#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH) - output.print(" repair extrafs (erases/rebuilds internal ExtraFS)\r\n"); - output.print(" scan extrafs erase (USB only; reboot for destructive page test)\r\n"); -#endif -#if COMPANION_FEATURE_TEMP_RADIO || COMPANION_FEATURE_OTA_CLI -#if COMPANION_FEATURE_TEMP_RADIO - output.print(" tempradio [freq,bw,sf,cr,minutes]\r\n"); - output.print(" normalradio\r\n"); -#endif -#if COMPANION_FEATURE_OTA_CLI - output.print(" ota {status|ls|announce|folder|config|...}\r\n"); -#endif -#endif - output.print( - " reboot\r\n" - " ver\r\n"); -#if defined(NRF52_PLATFORM) - output.println(" uf2reset"); +#define MESH_COMPANION_HELP_BEGIN output.print( +#define MESH_COMPANION_HELP_END ); +#define MESH_COMPANION_HELP_LINE_BEGIN output.println( +#define MESH_COMPANION_HELP_LINE_END ); +#include "CompanionHelp.inc" +#undef MESH_COMPANION_HELP_BEGIN +#undef MESH_COMPANION_HELP_END +#undef MESH_COMPANION_HELP_LINE_BEGIN +#undef MESH_COMPANION_HELP_LINE_END #endif if (_terminal_mode) { output.print(" +++MESHCORE-TERM-STOP\r\n"); diff --git a/test/test_companion_help_writer.py b/test/test_companion_help_writer.py new file mode 100644 index 000000000..f8bfb70bf --- /dev/null +++ b/test/test_companion_help_writer.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Execute actual help branches; preserve807 UI and bounded partial writes.""" +from pathlib import Path +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +FEATURES = ['COMPANION_FEATURE_READER=1', 'COMPANION_FEATURE_TEMP_RADIO=1', + 'COMPANION_FEATURE_OTA_CLI=1', 'COMPANION_FEATURE_NOTIFICATIONS=1', + 'MESH_ENABLE_LOST_REPLY=1', 'MESH_ENABLE_TX_BEACONS=1', + 'MESH_ENABLE_MESHTEXT_CHAT=1', 'MESH_ENABLE_ETHERNET_TUNNEL=1'] +PROFILES = { + 'rak_full': ['NRF52_PLATFORM', 'RAK_4631', 'COMPANION_RADIO_FULL', 'EXTRAFS', + 'DISPLAY_CLASS', 'OLED_AUTO_DETECT=1', 'BLE_PIN_CODE=123456', + 'OTA_RAK15002_SD_STORE', 'ETHERNET_USE_RAK13800', 'OTA_SD_STORE', + 'MESH_USB_LOGGING_AVAILABLE=1', 'MESH_USB_CONSOLE_COOPERATIVE=1', *FEATURES], + 'nrf_full': ['NRF52_PLATFORM', 'COMPANION_RADIO_FULL', 'EXTRAFS', 'DISPLAY_CLASS', + 'OLED_AUTO_DETECT=1', 'BLE_PIN_CODE=123456', 'MESH_USB_LOGGING_AVAILABLE=1', + 'MESH_USB_CONSOLE_COOPERATIVE=1', *FEATURES], + 'nrf_minimal': ['NRF52_PLATFORM'], + 'esp_full': ['ESP32_PLATFORM', 'ESP32', 'WIFI_SSID', 'COMPANION_RADIO_FULL', + 'COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1', 'WITH_WEBCONFIG', + 'MESH_USB_LOGGING_AVAILABLE=1', 'MESH_USB_CONSOLE_COOPERATIVE=1', + 'MESH_PRIMARY_ESPNOW=1', 'BLE_PIN_CODE=123456', 'PIN_BUZZER', + 'PIN_VIBRATION', *FEATURES], +} +# Generated from807's original literal groups, including its dynamic TERM-STOP +# suffix. Non-RAK call hashes bind print/println order as well as all text bytes. +GOLDEN = {'rak_full': {'text': '9a68ce5a10905a201c3bddeb19f4819ce03c6dcaff248cff9c140803b70b14a7', 'calls': '56ed09dee7d53881b2c9fcf3f4ec9b6223e1562ce63e110a065e46706702b53b'}, 'nrf_full': {'text': 'b114ac12ae2ea01b7c47609b72f7eafb616c1dbeb7fb796a95b1377465fd9105', 'calls': '4973a7f21887d07b129cce4ae4cc8145f3f35e664bae09be658957210440d52e'}, 'nrf_minimal': {'text': '15fa8f290cd8c75cfde2fd2ac1cb5b5151a6755c68448fb2793dde3f27669033', 'calls': '6063fd1a699f9c0b588252bd8ce5922e0ff31cec6bbd24522789284ab2a37644'}, 'esp_full': {'text': '1e3b6781c81b5603f7092108e5e309543c60cef8ff4d7cb41a891f67396f716f', 'calls': 'b6335df2bd05b9cab44a7933469b76c5b2611c4b81057436a959feaf3bfbaf60'}} + +HARNESS = r''' +#include "CompanionHelpWriter.h" +#include +#include +#include +#include +#include +#include +#include +struct Output { + std::string text, trace; + std::vector writes; + size_t throttle = 256, credit = size_t(-1); + bool overreport = false; + void record(char kind, const char* value, size_t length) { + trace += kind; + for (unsigned i=0;i<4;++i) trace += char((length>>(i*8))&255); + trace.append(value,length); + } + size_t print(const char* value) { + size_t n=strlen(value);text+=value;record('P',value,n);return n; + } + size_t println(const char* value) { + std::string line=std::string(value)+"\r\n"; + text+=line;record('L',line.data(),line.size());return line.size(); + } + size_t write(const uint8_t* value, size_t length) { + assert(length<=256);writes.push_back(length); + if (overreport) return length+1; + size_t n=std::min(length,std::min(throttle,credit)); + text.append(reinterpret_cast(value),n);credit-=n; + record('W',reinterpret_cast(value),n);return n; + } +} output; +Output& terminalOutput() {return output;} +void actualHelp(bool _terminal_mode) { +@ACTUAL_HELP@ +} +int main() { + actualHelp(true); +#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) + assert(!output.writes.empty()); +#else + assert(output.writes.empty()); +#endif + fwrite(output.text.data(),1,output.text.size(),stdout); + fwrite(output.trace.data(),1,output.trace.size(),stderr); + std::string text(777,'x'); + Output partial;partial.throttle=7; + assert(mesh::companion::writeHelpChunks(partial,text.data(),text.size())==text.size()); + assert(partial.text==text && partial.writes.size()<=text.size()); + Output stalled;stalled.credit=13; + assert(mesh::companion::writeHelpChunks(stalled,text.data(),text.size())==13); + assert(stalled.text==text.substr(0,13) && stalled.writes.size()==2); + Output closed;closed.credit=0; + assert(mesh::companion::writeHelpChunks(closed,text.data(),text.size())==0); + assert(closed.writes.size()==1 && closed.text.empty()); + Output invalid;invalid.overreport=true; + assert(mesh::companion::writeHelpChunks(invalid,text.data(),text.size())==0); + assert(invalid.writes.size()==1 && invalid.text.empty()); + Output empty; + assert(mesh::companion::writeHelpChunks(empty,nullptr,1)==0); + assert(mesh::companion::writeHelpChunks(empty,"",0)==0 && empty.writes.empty()); +} +''' + + +class CompanionHelpWriterTests(unittest.TestCase): + def test_actual_branch_preserves_ui_and_non_rak_call_sequence(self): + compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++') + if not compiler: + self.skipTest('host C++17 compiler required') + source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text() + start = source.index(' auto& output = terminalOutput();', + source.index('} else if (strcmp(command, "help") == 0)')) + end = source.index(' } else if (handleCommand(command, 0, local_reply))', start) + harness = HARNESS.replace('@ACTUAL_HELP@', source[start:end]) + for name, defines in PROFILES.items(): + with self.subTest(profile=name), tempfile.TemporaryDirectory(prefix='companion-help-') as folder: + work = Path(folder);(work / 'test.cpp').write_text(harness) + sanitizer = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all', + '-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else [] + result = subprocess.run([compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', + *sanitizer, *['-D'+value for value in defines], + '-I'+str(ROOT/'examples/companion_radio'), str(work/'test.cpp'), + '-o', str(work/'test')], capture_output=True, text=True) + self.assertEqual(result.returncode,0,result.stdout+result.stderr) + result = subprocess.run([str(work/'test')],capture_output=True) + self.assertEqual(result.returncode,0,result.stderr.decode(errors='replace')) + self.assertEqual(hashlib.sha256(result.stdout).hexdigest(),GOLDEN[name]['text']) + if name != 'rak_full': + self.assertEqual(hashlib.sha256(result.stderr).hexdigest(),GOLDEN[name]['calls']) + + +if __name__ == '__main__': + unittest.main()