Add saved raw radio2 KISS host sessions to Full firmware
This commit is contained in:
1 parent
8a8c80ee6a
commit
cb3c92a8da
50 files changed
+2750
-74
No files matched your search
@@ -578,6 +578,10 @@ jobs:
|
||||
python3 -B test/test_companion_channel_publication.py -v
|
||||
python3 -B test/test_companion_tx_raw_alias.py -v
|
||||
python3 -B test/test_companion_traffic_routing.py -v
|
||||
python3 -B test/test_host_kiss_radio.py -v
|
||||
python3 -B test/test_host_kiss_infrastructure.py -v
|
||||
python3 -B test/test_host_kiss.py -v
|
||||
python3 -B test/test_host_kiss_usb.py -v
|
||||
python3 -B test/test_ethernet_kiss.py -v
|
||||
python3 -B test/test_ethernet_radio.py -v
|
||||
python3 -B test/test_companion_ethernet.py -v
|
||||
|
||||
@@ -5504,6 +5504,20 @@ build_firmware_one_profile() {
|
||||
apply_rak4631_rak15002_sd_store_profile "$env_name"
|
||||
apply_radio_overrides
|
||||
apply_firmware_profile_overrides
|
||||
# Full LoRa images retain their normal role while a connected host uses
|
||||
# an explicitly enabled raw radio2 KISS session. Reduced profiles omit it.
|
||||
if [ "$BUILD_PROFILE_FOR_TARGET" = "full" ] \
|
||||
|| { [ "$env_platform" = "NRF52_PLATFORM" ] \
|
||||
&& [ "${NRF52_OTA_SENSOR_PROFILE:-}" = "full" ] \
|
||||
&& is_nrf52_sensor_ota_pair_target "$env_name"; }; then
|
||||
case "$env_platform" in
|
||||
ESP32_PLATFORM|NRF52_PLATFORM)
|
||||
if ! is_kiss_modem_target "$env_name"; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMESH_ENABLE_HOST_KISS=1"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
declare_build_capability_contract "$env_name" "$env_platform"
|
||||
|
||||
if [ "$ESP32_FULL_BUILD" = "1" ] || is_esp32_companion_radio_full_target "$env_name"; then
|
||||
|
||||
@@ -1212,6 +1212,39 @@ station port 5003. It does not add an SSH server, use `tx.ssh` tags, or bridge
|
||||
Ethernet into MeshCore. Conflicting radio2 owners are not overwritten.
|
||||
See [Ethernet setup, host commands and limits](radio_profiles.md#esp32-companion-ethernet-over-lora-kiss).
|
||||
|
||||
## Full firmware raw radio2 KISS modem
|
||||
|
||||
```text
|
||||
get kiss
|
||||
set kiss.transport usb
|
||||
set kiss on
|
||||
set kiss off
|
||||
```
|
||||
|
||||
Full ESP32 and nRF52 LoRa images can keep their normal MeshCore role on
|
||||
radio1 while a connected computer sends and receives raw packets on radio2.
|
||||
The setting is saved and defaults to **off**. First configure an explicit
|
||||
saved `radio2` tuple in `rxtx` mode. `get kiss` reports the transport, state,
|
||||
packet counters and any radio2 conflict. Setting changes require a local
|
||||
USB, BLE or network management connection.
|
||||
|
||||
With USB selected, the normal CLI stays available until the computer sends
|
||||
a KISS frame delimiter (`0xC0`). That session then owns USB; functional text
|
||||
and debug logging are suppressed without changing their saved preferences.
|
||||
KISS RETURN (`C0 FF C0`) or a detected USB disconnect returns to the CLI.
|
||||
|
||||
On ESP32, `set kiss.transport tcp` selects a separate KISS listener on the
|
||||
station WiFi address, port **5004**. It does not start WiFi or listen on the
|
||||
setup AP. One computer can connect at a time. This is an unauthenticated raw
|
||||
modem endpoint for a trusted LAN, like the existing Ethernet KISS service.
|
||||
|
||||
KISS port 0 DATA maps only to radio2. Packets are 1-255 bytes, unchanged over
|
||||
the air, using the normal private LoRa sync word, explicit headers, CRC and
|
||||
normal IQ. The host must honor transmit pacing. This interface does not add
|
||||
RNode framing, fragmentation or Reticulum routing; those remain host tasks.
|
||||
See [Full host KISS framing and limits](kiss_modem_protocol.md#full-firmware-host-kiss)
|
||||
for the wire protocol and session rules.
|
||||
|
||||
## Set repeater, room-server, and sensor reply profiles
|
||||
|
||||
```text
|
||||
|
||||
@@ -12,6 +12,72 @@ fast-switches between `radio` and `radio2`; it does not create a second radio.
|
||||
|
||||
## Serial Configuration
|
||||
|
||||
### Full firmware host KISS
|
||||
|
||||
Full ESP32 and nRF52 LoRa firmware includes a separate, default-off raw
|
||||
radio2 host service. It preserves the device's normal Companion, Repeater,
|
||||
Room Server or Sensor role on radio1. It is enabled in the Full sensor/OTA
|
||||
profile on nRF52; reduced infrastructure images omit the service.
|
||||
|
||||
Configure the desired saved radio2 RF tuple in `rxtx` mode, then use:
|
||||
|
||||
```text
|
||||
set kiss.transport usb
|
||||
set kiss on
|
||||
get kiss
|
||||
```
|
||||
|
||||
Both choices are saved. `set kiss off` stops the service and releases its
|
||||
runtime radio2 protocol lease without changing the saved radio2 tuple.
|
||||
Temporary radio settings and Meshtext/Ethernet radio2 owners block admission.
|
||||
The service never chooses a frequency, takes another service's radio2 lease,
|
||||
crosses raw packets onto radio1, or feeds them into MeshCore parsing/bridges.
|
||||
|
||||
USB remains the normal CLI until an initial KISS FEND (`C0`). During the raw
|
||||
session, USB Companion framing and text/debug output are parked. Send the
|
||||
standard KISS RETURN frame (`C0 FF C0`) to leave the session, then issue CLI
|
||||
commands normally. A detected USB disconnect/reset also ends the session.
|
||||
UART-to-USB adapters without a disconnect signal may require RETURN.
|
||||
If the radio2 settings change mid-session, USB stays quarantined until RETURN
|
||||
or disconnect, so leftover binary bytes cannot become CLI commands.
|
||||
|
||||
ESP32 also supports `set kiss.transport tcp`. Enable station WiFi separately
|
||||
and connect to its address on TCP **5004**. The service accepts one client,
|
||||
does not bind the setup AP, and has no authentication. Use a trusted LAN.
|
||||
WiFi loss, RETURN, setting changes and invalidated radio2 generations close
|
||||
the connection and discard queued host work. A second client cannot displace
|
||||
an existing owner. USB remains available for management in TCP mode.
|
||||
|
||||
The Full service uses standard KISS escaping and the following frames:
|
||||
|
||||
| Type | Direction | Payload and meaning |
|
||||
| --- | --- | --- |
|
||||
| `00` | Both | Raw DATA, 1-255 bytes, exclusively radio2 |
|
||||
| `0F` | Device to host | `01`: READY, initially and after each completed TX |
|
||||
| `06` | Device to host | `F8 01`: TX succeeded; `F8 00`: TX failed |
|
||||
| `FF` | Host to device | Empty payload: RETURN to CLI/close TCP |
|
||||
|
||||
Other ports and parameter/control commands are ignored. RF parameters are
|
||||
configured through the ordinary saved radio2 CLI, not RNode control commands.
|
||||
Wait for READY before sending the next packet. The device retains at most
|
||||
two queued host frames plus one in-flight transmission, uses bounded I/O,
|
||||
and preserves normal CAD, duty budget and MeshCore transmit fairness.
|
||||
`get kiss` includes RX/TX/failure/drop/malformed counters. A drop counter can
|
||||
include received packets that could not fit in the bounded host output queue.
|
||||
|
||||
The raw RF payload has no additional header. It uses the ordinary private
|
||||
LoRa sync word (`0x12`, represented as `0x1424` by SX126x), explicit headers,
|
||||
CRC and normal IQ. Peers must match RF parameters, including preamble timing
|
||||
appropriate for the time-shared receiver. Only one physical radio is present.
|
||||
|
||||
This is a generic host modem. Reticulum/RNode interoperability requires host
|
||||
adaptation for RNode's one-byte RF header, fragmentation/reassembly of larger
|
||||
packets and transmit pacing. The Full image runs no Reticulum router and
|
||||
imports no RNode firmware code. The separate dedicated KISS firmware below
|
||||
retains its original two-port interface and binary configuration extensions.
|
||||
|
||||
### Dedicated modem serial configuration
|
||||
|
||||
115200 baud, 8N1, no flow control.
|
||||
|
||||
## Frame Format
|
||||
|
||||
@@ -23,6 +23,14 @@ exposes equivalent RAM-only binary `SetRadio2`/`SetTempRadio2` commands and
|
||||
logical KISS Data ports, documented in [the KISS modem protocol](kiss_modem_protocol.md).
|
||||
It does not use the Mesh CLI's persistent profile file or Mesh retry queues.
|
||||
|
||||
Full ESP32 and nRF52 LoRa images also offer an optional
|
||||
[raw host KISS service](kiss_modem_protocol.md#full-firmware-host-kiss).
|
||||
`set kiss on` enables it, default off, while radio1 retains its normal role.
|
||||
It borrows an explicitly configured saved `radio2` profile for the host's
|
||||
session. Meshtext, Ethernet and temporary radio2 owners cannot share that
|
||||
lease. This service is a raw modem; the connected computer handles its own
|
||||
network protocol and routing.
|
||||
|
||||
## Quick setup
|
||||
|
||||
Keep the primary channel configured with `radio`, then add the second:
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include "NotificationSettingsFile.h"
|
||||
#include "MyMesh.h"
|
||||
#include <helpers/HostKiss.h>
|
||||
#if defined(RAK_4631) && defined(COMPANION_RADIO_FULL) \
|
||||
|| (defined(STM32_PLATFORM) && defined(STM32WLxx) && COMPANION_FEATURE_TEXT_TERMINAL)
|
||||
#include "CompanionHelpWriter.h"
|
||||
@@ -3388,6 +3389,9 @@ void MyMesh::begin(bool has_display, bool radio_available,
|
||||
#endif
|
||||
|
||||
configureRadioFromPrefs();
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::begin(_store->getPrimaryFS(), *_radio, _radio_profiles, *this);
|
||||
#endif
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
beginBeacons();
|
||||
#endif
|
||||
@@ -10789,6 +10793,9 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
|
||||
// router is also used by USB/BLE/browser commands without a timestamp.
|
||||
if (mesh::esp32::handlePowerTestCommand(command, reply, reply_capacity,
|
||||
sender_timestamp == 0)) return true;
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::handleCommand(command, reply, reply_capacity, sender_timestamp == 0)) return true;
|
||||
#endif
|
||||
if (mesh::power::handleRegulatorCommand(command, reply, reply_capacity)) return true;
|
||||
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
|
||||
@@ -11164,6 +11171,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
|
||||
}
|
||||
|
||||
void MyMesh::checkCLIRescueCmd() {
|
||||
if (mesh::isUsbBinaryStreamOwned()) return;
|
||||
Stream& output = mesh::usbTerminalPort();
|
||||
Stream& input = mesh::usbCompanionPort();
|
||||
size_t len = strnlen(cli_command, sizeof(cli_command));
|
||||
@@ -11525,6 +11533,9 @@ void MyMesh::loop() {
|
||||
#endif
|
||||
|
||||
if (!mesh::morseTimingActive(_radio)) _radio_profiles.loop();
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (!mesh::morseTimingActive(_radio)) mesh::hostkiss::loop();
|
||||
#endif
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
if (!mesh::morseTimingActive(_radio)) loopBeacons();
|
||||
#endif
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <helpers/MeshtextConfig.h>
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#include <helpers/EthernetTunnelSettings.h>
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <helpers/esp32/EthernetKissServer.h>
|
||||
#endif
|
||||
namespace mesh { class BeaconManager; }
|
||||
@@ -443,6 +444,9 @@ protected:
|
||||
// Direct-only OTA inherits the Dispatcher's drop/no-op foreign callbacks.
|
||||
bool allowForeignRadioTransmit() const override {
|
||||
return _radio_available && _scheduled_reboot_at == 0 && (
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::radioActive() ||
|
||||
#endif
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT
|
||||
_mt_active ||
|
||||
#endif
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <Mesh.h>
|
||||
#include <helpers/BluetoothMac.h>
|
||||
#include "MyMesh.h"
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <helpers/CompanionReader.h>
|
||||
#include "CompanionBluetooth.h"
|
||||
#include "CompanionWireless.h"
|
||||
@@ -1050,9 +1051,21 @@ static bool serviceUsbMotaResponseBoundary() {
|
||||
// loss edge, before any protocol parser runs. No line, login, recipient, queued
|
||||
// text, or mOTA attachment may survive into the next host session.
|
||||
static bool usb_terminal_host_reset_completion_pending = false;
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
static bool host_kiss_usb_parked = false;
|
||||
static bool host_kiss_usb_restore_enabled = false;
|
||||
#endif
|
||||
static uint32_t usb_terminal_host_reset_retry_at = 0;
|
||||
|
||||
static void resetUsbTerminalHostSession() {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::resetUsbSession();
|
||||
if (host_kiss_usb_parked) {
|
||||
mesh::setUsbBinaryStreamOwned(false);
|
||||
if (host_kiss_usb_restore_enabled && interface_manager.isEnabled()) usb_serial_interface.enable();
|
||||
host_kiss_usb_parked = host_kiss_usb_restore_enabled = false;
|
||||
}
|
||||
#endif
|
||||
#if COMPANION_FEATURE_READER
|
||||
mesh::cancelReaderCommand(&mesh::usbTerminalPort());
|
||||
#endif
|
||||
@@ -1096,6 +1109,49 @@ static void resetUsbTerminalHostSession() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// A saved KISS feature takes USB only after an explicit KISS delimiter.
|
||||
// Park this one Companion transport; BLE/WiFi and the primary mesh stay live.
|
||||
static bool serviceCompanionHostKissUsb() {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
const bool claimed = mesh::hostkiss::serviceUsb();
|
||||
if (claimed && mesh::hostkiss::usbActive() && !host_kiss_usb_parked) {
|
||||
host_kiss_usb_restore_enabled = usb_serial_interface.isEnabled();
|
||||
cancelUsbSerialOperations();
|
||||
#if COMPANION_FEATURE_READER
|
||||
mesh::cancelReaderCommand(&mesh::usbTerminalPort());
|
||||
#endif
|
||||
the_mesh.resetUsbHostSessionInput();
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
if (usb_mota_mode) leaveUsbMotaMode(false);
|
||||
mesh::ota::OtaContext::serialFolderSource().resetSessionState();
|
||||
#endif
|
||||
if (the_mesh.isTerminalMode()) leaveUsbTerminalMode(false);
|
||||
usb_binary_startup_probe.cancel();
|
||||
usb_ascii_session_default.cancel();
|
||||
clearUsbTerminalLine();
|
||||
usb_terminal_discard_line = usb_logging_terminal_mode = false;
|
||||
mesh::discardUsbTerminalOutput();
|
||||
usb_serial_interface.disable();
|
||||
host_kiss_usb_parked = true;
|
||||
}
|
||||
if (host_kiss_usb_parked && !claimed && !mesh::hostkiss::usbActive()) {
|
||||
mesh::setUsbBinaryStreamOwned(false);
|
||||
host_kiss_usb_parked = false;
|
||||
if (host_kiss_usb_restore_enabled && interface_manager.isEnabled()) usb_serial_interface.enable();
|
||||
host_kiss_usb_restore_enabled = false;
|
||||
clearUsbTerminalLine();
|
||||
usb_terminal_discard_line = false;
|
||||
usb_serial_interface.resetSessionState();
|
||||
usb_ascii_session_default.request(usb_serial_interface.getCompletedFrameCount());
|
||||
}
|
||||
if (host_kiss_usb_parked && claimed && !mesh::hostkiss::usbActive())
|
||||
mesh::setUsbBinaryStreamOwned(true); // RETURN retires at the next poll.
|
||||
return claimed;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void serviceUsbTerminalHostSessionReset() {
|
||||
#if defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) \
|
||||
|| MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
@@ -1392,6 +1448,7 @@ static void serviceUsbTerminal() {
|
||||
}
|
||||
|
||||
static void serviceUsbLoggingOwnership(bool logging_enabled) {
|
||||
if (mesh::hostkiss::usbActive()) return;
|
||||
// A saved logging-on preference makes the one available TTY behave like a
|
||||
// logging repeater: plaintext diagnostics plus an input-capable CLI. Put the
|
||||
// Companion interface into passthrough before it can mix framed traffic with
|
||||
@@ -4199,6 +4256,7 @@ static void serviceUsbLoggingOnlySession() {
|
||||
}
|
||||
|
||||
static bool usbLoggingRecoverySafe(void*) {
|
||||
if (mesh::hostkiss::usbActive()) return false;
|
||||
if (board.isOTAUpdateRunning() || board.isRadioTestActive()
|
||||
|| radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()
|
||||
|| !the_mesh.canRecoverUsbLogging()) return false;
|
||||
@@ -4245,9 +4303,12 @@ void loop() {
|
||||
serviceUsbLoggingOnlySession();
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
serviceUsbTerminalHostSessionReset();
|
||||
serviceUsbAsciiSessionDefault();
|
||||
serviceUsbLoggingReplyBarrier();
|
||||
serviceUsbLoggingOwnership(mesh::isUsbLoggingEnabled()); // Before framed dispatch.
|
||||
const bool host_kiss_usb_claimed = serviceCompanionHostKissUsb();
|
||||
if (!host_kiss_usb_claimed) {
|
||||
serviceUsbAsciiSessionDefault();
|
||||
serviceUsbLoggingReplyBarrier();
|
||||
serviceUsbLoggingOwnership(mesh::isUsbLoggingEnabled()); // Before framed dispatch.
|
||||
}
|
||||
#endif
|
||||
// Identify nRF52 CDC 1 when a terminal opens it. Doing this on the connection
|
||||
// edge avoids losing the marker before the host has opened the port.
|
||||
@@ -4258,13 +4319,15 @@ void loop() {
|
||||
mesh::serviceUsbTerminalPort();
|
||||
#endif
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
expireUsbBinaryStartupProbeBeforeDispatch();
|
||||
if (!host_kiss_usb_claimed) {
|
||||
expireUsbBinaryStartupProbeBeforeDispatch();
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
serviceUsbMotaResponseBoundary();
|
||||
serviceUsbMotaResponseBoundary();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
syncUsbTerminalInputEditing();
|
||||
if (!host_kiss_usb_claimed) syncUsbTerminalInputEditing();
|
||||
#endif
|
||||
the_mesh.loop();
|
||||
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
|
||||
@@ -4286,8 +4349,10 @@ void loop() {
|
||||
serviceCompanionRadioRecovery();
|
||||
#endif
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
serviceUsbTerminal();
|
||||
syncUsbTerminalInputEditing();
|
||||
if (!host_kiss_usb_claimed) {
|
||||
serviceUsbTerminal();
|
||||
syncUsbTerminalInputEditing();
|
||||
}
|
||||
// Commands can synchronously enqueue a multi-line reply. Give it the
|
||||
// remaining CDC FIFO capacity now; later loops retain any unwritten suffix.
|
||||
mesh::serviceUsbTerminalPort();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#endif
|
||||
#include <helpers/UsbLoggingWatchdog.h>
|
||||
#include <helpers/UsbLoggingClientActivity.h>
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#include <helpers/HilStartupTrace.h>
|
||||
@@ -476,14 +477,31 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
|
||||
bool usb_ready = true;
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
mesh::serviceUsbLoggingPort();
|
||||
mesh::serviceUsbTerminalPort();
|
||||
if (mesh::takeUsbTerminalSessionReset()) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::resetUsbSession();
|
||||
#endif
|
||||
command[0] = 0;
|
||||
command_overflow = false;
|
||||
the_mesh.cancelPendingSerialOutput();
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
// Claim binary input before a pending terminal listing can append text.
|
||||
// Keep the rest of this function alive for Ethernet and board housekeeping.
|
||||
if (mesh::hostkiss::serviceUsb()) {
|
||||
command[0] = 0;
|
||||
command_overflow = false;
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
the_mesh.cancelPendingSerialOutput();
|
||||
#endif
|
||||
usb_ready = false;
|
||||
}
|
||||
#endif
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
if (usb_ready) mesh::serviceUsbTerminalPort();
|
||||
// A busy USB session cleanup may defer the CLI, never the radio loop.
|
||||
usb_ready = mesh::tryCompleteUsbTerminalSessionReset();
|
||||
usb_ready = usb_ready && mesh::tryCompleteUsbTerminalSessionReset();
|
||||
// Large listings advance from MyMesh::loop without blocking radio service.
|
||||
usb_ready = usb_ready && !the_mesh.hasPendingSerialOutput()
|
||||
&& mesh::canAcceptUsbConsoleCommand();
|
||||
@@ -614,6 +632,9 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
|
||||
}
|
||||
|
||||
static bool usbLoggingRecoverySafe(void*) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::usbActive()) return false;
|
||||
#endif
|
||||
if (board.isOTAUpdateRunning() || board.isRadioTestActive()
|
||||
|| radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()
|
||||
|| !the_mesh.canRecoverUsbLogging()) return false;
|
||||
@@ -670,6 +691,9 @@ void loop() {
|
||||
rtc_clock.tick();
|
||||
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (!mesh::hostkiss::usbActive())
|
||||
#endif
|
||||
mesh::serviceUsbTerminalPort();
|
||||
#endif
|
||||
|
||||
@@ -702,6 +726,10 @@ void loop() {
|
||||
&& sleep_guard(!board.isUsbDataConnected(), mesh::powerdiag::Reason::UsbSession)
|
||||
&& sleep_guard(!mesh::isUsbLoggingWatchdogArmed(), mesh::powerdiag::Reason::UsbRecovery)
|
||||
&& sleep_guard(!mesh::wireless::control().pending(), mesh::powerdiag::Reason::Wireless);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
can_power_save = can_power_save
|
||||
&& sleep_guard(!mesh::hostkiss::usbActive(), mesh::powerdiag::Reason::UsbSession);
|
||||
#endif
|
||||
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
||||
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP \
|
||||
&& defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#endif
|
||||
#include <helpers/UsbLoggingWatchdog.h>
|
||||
#include <helpers/UsbLoggingClientActivity.h>
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#if MESH_PACKET_LOGGING
|
||||
@@ -331,6 +332,9 @@ void setup() {
|
||||
}
|
||||
|
||||
static bool usbLoggingRecoverySafe(void*) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::usbActive()) return false;
|
||||
#endif
|
||||
if (board.isOTAUpdateRunning() || board.isRadioTestActive()
|
||||
|| radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()
|
||||
|| !the_mesh.canRecoverUsbLogging()) return false;
|
||||
@@ -355,12 +359,26 @@ void loop() {
|
||||
bool usb_ready = true;
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
mesh::serviceUsbLoggingPort();
|
||||
mesh::serviceUsbTerminalPort();
|
||||
if (mesh::takeUsbTerminalSessionReset()) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::resetUsbSession();
|
||||
#endif
|
||||
command[0] = 0;
|
||||
the_mesh.cancelPendingSerialOutput();
|
||||
}
|
||||
usb_ready = mesh::tryCompleteUsbTerminalSessionReset();
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::serviceUsb()) {
|
||||
command[0] = 0;
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
the_mesh.cancelPendingSerialOutput();
|
||||
#endif
|
||||
usb_ready = false;
|
||||
}
|
||||
#endif
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
if (usb_ready) mesh::serviceUsbTerminalPort();
|
||||
usb_ready = usb_ready && mesh::tryCompleteUsbTerminalSessionReset();
|
||||
usb_ready = usb_ready && !the_mesh.hasPendingSerialOutput()
|
||||
&& mesh::canAcceptUsbConsoleCommand();
|
||||
#endif
|
||||
@@ -449,6 +467,9 @@ void loop() {
|
||||
board.loop();
|
||||
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (!mesh::hostkiss::usbActive())
|
||||
#endif
|
||||
mesh::serviceUsbTerminalPort();
|
||||
#endif
|
||||
#ifdef TBEAM_1W
|
||||
@@ -477,6 +498,10 @@ void loop() {
|
||||
&& sleep_guard(!board.isUsbDataConnected(), mesh::powerdiag::Reason::UsbSession)
|
||||
&& sleep_guard(!mesh::isUsbLoggingWatchdogArmed(), mesh::powerdiag::Reason::UsbRecovery)
|
||||
&& sleep_guard(!mesh::wireless::control().pending(), mesh::powerdiag::Reason::Wireless);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
can_power_save = can_power_save
|
||||
&& sleep_guard(!mesh::hostkiss::usbActive(), mesh::powerdiag::Reason::UsbSession);
|
||||
#endif
|
||||
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
||||
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP \
|
||||
&& defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <helpers/UsbLogging.h>
|
||||
#include <helpers/UsbLoggingWatchdog.h>
|
||||
#include <helpers/UsbLoggingClientActivity.h>
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#include <helpers/PowerManagementUtils.h>
|
||||
#if defined(NRF52_PLATFORM) && MESH_ENABLE_SENSOR_TRACKER
|
||||
@@ -325,6 +326,9 @@ void setup() {
|
||||
}
|
||||
|
||||
static bool usbLoggingRecoverySafe(void*) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::usbActive()) return false;
|
||||
#endif
|
||||
if (board.isOTAUpdateRunning() || board.isRadioTestActive()
|
||||
|| radio_driver.isWatchdogObserving() || radio_driver.isCalibratingNoiseFloor()
|
||||
|| !the_mesh.canRecoverUsbLogging()) return false;
|
||||
@@ -343,9 +347,22 @@ void loop() {
|
||||
bool usb_ready = true;
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
mesh::serviceUsbLoggingPort();
|
||||
mesh::serviceUsbTerminalPort();
|
||||
if (mesh::takeUsbTerminalSessionReset()) command[0] = 0;
|
||||
usb_ready = mesh::tryCompleteUsbTerminalSessionReset()
|
||||
if (mesh::takeUsbTerminalSessionReset()) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::resetUsbSession();
|
||||
#endif
|
||||
command[0] = 0;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::serviceUsb()) {
|
||||
command[0] = 0;
|
||||
usb_ready = false;
|
||||
}
|
||||
#endif
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
if (usb_ready) mesh::serviceUsbTerminalPort();
|
||||
usb_ready = usb_ready && mesh::tryCompleteUsbTerminalSessionReset()
|
||||
&& mesh::canAcceptUsbConsoleCommand();
|
||||
#endif
|
||||
Stream& console = mesh::usbConsolePort();
|
||||
@@ -405,6 +422,9 @@ void loop() {
|
||||
rtc_clock.tick();
|
||||
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (!mesh::hostkiss::usbActive())
|
||||
#endif
|
||||
mesh::serviceUsbTerminalPort();
|
||||
#endif
|
||||
#ifdef HAS_EXTERNAL_WATCHDOG
|
||||
@@ -430,6 +450,10 @@ void loop() {
|
||||
&& sleep_guard(!morse_timing, mesh::powerdiag::Reason::Morse)
|
||||
&& sleep_guard(!board.isUsbDataConnected(), mesh::powerdiag::Reason::UsbSession)
|
||||
&& sleep_guard(!mesh::isUsbLoggingWatchdogArmed(), mesh::powerdiag::Reason::UsbRecovery);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
can_power_save = can_power_save
|
||||
&& sleep_guard(!mesh::hostkiss::usbActive(), mesh::powerdiag::Reason::UsbSession);
|
||||
#endif
|
||||
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
|
||||
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP \
|
||||
&& defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Dispatcher.h"
|
||||
#include "helpers/HostKissHooks.h"
|
||||
#include "helpers/ota/OtaTiming.h"
|
||||
#if MESH_ENABLE_OTA_KISS_DIRECT
|
||||
#include "helpers/ota/OtaDirectWire.h"
|
||||
@@ -289,6 +290,9 @@ bool Dispatcher::supportsForeignRadioProtocol(RadioProfileProtocol protocol) con
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
case RadioProfileProtocol::Ethernet: return _radio->supportsEthernetTunnel();
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
case RadioProfileProtocol::Kiss: return _radio->supportsHostKiss();
|
||||
#endif
|
||||
default: return false;
|
||||
}
|
||||
@@ -370,6 +374,9 @@ bool Dispatcher::queueForeignRadioFrame(const uint8_t* bytes, size_t length,
|
||||
void Dispatcher::finishForeignRadioFrame(bool success, bool account_airtime) {
|
||||
if (!hasForeignRadioFrame()) return;
|
||||
const bool direct_ota = foreign_radio_protocol == RadioProfileProtocol::MeshCore;
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
const bool host_kiss = foreign_radio_protocol == RadioProfileProtocol::Kiss;
|
||||
#endif
|
||||
if (account_airtime) {
|
||||
const uint32_t elapsed = (uint32_t)_ms->getMillis() - foreign_radio_start;
|
||||
total_air_time += elapsed;
|
||||
@@ -400,6 +407,10 @@ void Dispatcher::finishForeignRadioFrame(bool success, bool account_airtime) {
|
||||
// either transport's application counters or trigger its callbacks.
|
||||
if (direct_ota) {
|
||||
if (success) ++n_sent_direct;
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
} else if (host_kiss) {
|
||||
hostkiss::sendComplete(success);
|
||||
#endif
|
||||
} else onForeignRadioSendComplete(success);
|
||||
}
|
||||
|
||||
@@ -1086,6 +1097,11 @@ void Dispatcher::checkRecv() {
|
||||
&& _radio->receiveProfileGeneration() == profiles->generation[profile]) {
|
||||
rx_air_time += _radio->getProfileAirtime(profile, len);
|
||||
++_radio->profiles()->rx_packets[profile];
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (profiles->params(profile).protocol == RadioProfileProtocol::Kiss) {
|
||||
hostkiss::receive(raw, (size_t)len, profile, profiles->generation[profile], snr, rssi);
|
||||
} else
|
||||
#endif
|
||||
onForeignRadioFrame(raw, (size_t)len, profile, profiles->generation[profile], snr, rssi);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -114,6 +114,9 @@ public:
|
||||
virtual bool validateProfile(const RadioProfileParams&) const { return false; }
|
||||
virtual bool supportsMeshtext() const { return false; }
|
||||
virtual bool supportsEthernetTunnel() const { return false; }
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
virtual bool supportsHostKiss() const { return false; }
|
||||
#endif
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
// Check the requested antenna power at the prospective beacon frequency.
|
||||
// Validation must not retune the radio or alter its current power setting.
|
||||
@@ -678,6 +681,17 @@ public:
|
||||
beacon_wake_at = now + delay;
|
||||
beacon_wake_enabled = enabled;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
// The raw host modem can use radio2 only. It shares the bounded scheduler
|
||||
// slot and normal TX admission; it never falls back onto radio1.
|
||||
bool submitHostKissFrame(const uint8_t* bytes, size_t length, uint32_t generation) {
|
||||
return queueForeignRadioFrame(bytes, length, 1, generation, RadioProfileProtocol::Kiss);
|
||||
}
|
||||
bool cancelHostKissFrame() { return discardQueuedForeignRadioFrame(RadioProfileProtocol::Kiss); }
|
||||
bool hostKissFramePending() const {
|
||||
return hasForeignRadioFrame() && foreign_radio_protocol == RadioProfileProtocol::Kiss;
|
||||
}
|
||||
#endif
|
||||
Radio* getProfileRadio() { return _radio; }
|
||||
bool isDualRadioActive() const { return _radio->profiles() && _radio->profiles()->enabled(); }
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ namespace mesh {
|
||||
|
||||
enum class RadioProfileMode : uint8_t { Off = 0, Rx = 1, RxTx = 2 };
|
||||
enum class RadioCrossMode : uint8_t { Auto = 0, On = 1, Off = 2 };
|
||||
enum class RadioProfileProtocol : uint8_t { MeshCore = 0, Meshtext = 1, Ethernet = 2 };
|
||||
enum class RadioProfileProtocol : uint8_t { MeshCore = 0, Meshtext = 1, Ethernet = 2, Kiss = 3 };
|
||||
|
||||
// These are modulation profiles of ONE transceiver, never additional radios.
|
||||
struct RadioProfileParams {
|
||||
@@ -150,7 +150,7 @@ class RadioProfiles {
|
||||
if (!(p.freq >= 137 && p.freq <= 2500)
|
||||
|| p.sf < 5 || p.sf > 12 || p.cr < 5 || p.cr > 8
|
||||
|| (p.protocol != RadioProfileProtocol::MeshCore && p.protocol != RadioProfileProtocol::Meshtext
|
||||
&& p.protocol != RadioProfileProtocol::Ethernet)
|
||||
&& p.protocol != RadioProfileProtocol::Ethernet && p.protocol != RadioProfileProtocol::Kiss)
|
||||
|| (p.preamble != 0 && (p.preamble < 8 || p.preamble > MaxPreamble))) return false;
|
||||
// The first ten entries are the shared SX126x/SX127x bandwidth subset.
|
||||
static constexpr float bandwidths[] = {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#endif
|
||||
#include "PrefsSaveReplyGuard.h"
|
||||
#include "UsbLoggingWatchdog.h"
|
||||
#include "HostKiss.h"
|
||||
#include "PowerSavingDiagnostics.h"
|
||||
#include "HilStartupTrace.h"
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -182,6 +183,9 @@ void CommonCLI::loop() {
|
||||
servicePendingTxPower();
|
||||
servicePendingRxSettings();
|
||||
_radio_profiles.loop();
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
mesh::hostkiss::loop();
|
||||
#endif
|
||||
loopManagement();
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
loopBeacons();
|
||||
@@ -3486,6 +3490,9 @@ bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp,
|
||||
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply);
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (mesh::hostkiss::handleCommand(command, reply, 157, sender_timestamp == 0)) return;
|
||||
#endif
|
||||
if (mesh::power::handleBatteryCalibrationCommand(*_board, command, reply, 160,
|
||||
[this](float multiplier, char* result, size_t capacity) {
|
||||
return applyAndSaveAdcMultiplier(multiplier, result, capacity);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "ManagementFeatureConfig.h"
|
||||
#include "CommonCLI.h"
|
||||
#include "HostKissConfig.h"
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
#include "HostKiss.h"
|
||||
#endif
|
||||
#include <new>
|
||||
#if MESH_ENABLE_MANAGEMENT_REPORTING
|
||||
#include "ManagementReporter.h"
|
||||
@@ -264,6 +268,10 @@ static bool resolveDataScope(DataRouteState& route, TransportKey& scope,
|
||||
|
||||
void CommonCLI::beginManagement(mesh::Mesh& mesh, FILESYSTEM* fs) {
|
||||
_management_mesh = &mesh; _management_fs = fs;
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (auto* radio = _callbacks->getProfileRadio())
|
||||
mesh::hostkiss::begin(fs, *radio, _radio_profiles, mesh);
|
||||
#endif
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
beginBeacons();
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
#include "HostKiss.h"
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
|
||||
#include "HostKissCodec.h"
|
||||
#include "HostKissSettings.h"
|
||||
#include "RadioProfileCLI.h"
|
||||
#include "UsbLogging.h"
|
||||
#include <Dispatcher.h>
|
||||
#include <new>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#if defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#include <errno.h>
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace mesh { namespace hostkiss {
|
||||
namespace {
|
||||
constexpr size_t ByteBudget = 128;
|
||||
constexpr uint16_t TcpPort = 5004;
|
||||
Settings settings;
|
||||
Radio* radio = nullptr;
|
||||
RadioProfileCLI* profiles = nullptr;
|
||||
Dispatcher* dispatcher = nullptr;
|
||||
Codec* codec = nullptr; // All frame buffers are absent while the setting is off.
|
||||
bool usb_session = false, suspended = false, leased = false, transmitting = false;
|
||||
bool ready_pending = false, completion_pending = false, completion_success = false;
|
||||
uint8_t quarantine_return = 0;
|
||||
uint32_t profile_generation = 0, intent_generation = 0;
|
||||
uint32_t received = 0, transmitted = 0, failed = 0, discarded = 0;
|
||||
#if defined(ESP32)
|
||||
struct Tcp {
|
||||
WiFiServer server;
|
||||
WiFiClient client;
|
||||
uint32_t address = 0, last_attempt = 0;
|
||||
bool listening = false, attempted = false, connected = false;
|
||||
Tcp() : server(TcpPort, 2) {}
|
||||
};
|
||||
Tcp* tcp = nullptr;
|
||||
#endif
|
||||
|
||||
bool tcpConnected() {
|
||||
#if defined(ESP32)
|
||||
return tcp && tcp->connected;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
bool active() { return usb_session || tcpConnected(); }
|
||||
|
||||
void releaseRadio() {
|
||||
if (dispatcher) dispatcher->cancelHostKissFrame();
|
||||
transmitting = false;
|
||||
const auto* p = radio ? radio->profiles() : nullptr;
|
||||
// This service is the sole owner of the Kiss overlay. Its generation may
|
||||
// already have changed; let RadioProfileCLI reconcile that change itself.
|
||||
// An unrelated foreign protocol must never be cleared on our way out.
|
||||
if (leased && profiles && p && p->secondary.params.protocol == RadioProfileProtocol::Kiss)
|
||||
profiles->clearSecondaryProtocol();
|
||||
leased = false;
|
||||
}
|
||||
|
||||
void resetFrames() {
|
||||
if (codec) codec->reset();
|
||||
ready_pending = completion_pending = false;
|
||||
quarantine_return = 0;
|
||||
}
|
||||
|
||||
void closeSession() {
|
||||
releaseRadio();
|
||||
resetFrames();
|
||||
usb_session = suspended = false;
|
||||
setUsbBinaryStreamOwned(false);
|
||||
#if defined(ESP32)
|
||||
if (tcp) { tcp->client.stop(); tcp->connected = false; }
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* admission() {
|
||||
if (!radio || !profiles || !dispatcher || !radio->supportsHostKiss()) return "unsupported radio";
|
||||
if (!profiles->manualSecondaryConfigured()
|
||||
|| profiles->savedSecondaryConfig().mode != RadioProfileMode::RxTx) return "configure saved radio2 rxtx";
|
||||
if (profiles->savedSecondaryConfig().params.protocol != RadioProfileProtocol::MeshCore)
|
||||
return "radio2 protocol reserved";
|
||||
const auto* p = radio->profiles();
|
||||
if (!p || !profiles->secondaryRuntimeAvailable()) return "temporary or pending radio settings";
|
||||
if (p->secondary.mode != RadioProfileMode::RxTx || p->primary_temporary || p->secondary_temporary)
|
||||
return "radio2 unavailable";
|
||||
if (p->secondary.params.protocol != (leased ? RadioProfileProtocol::Kiss : RadioProfileProtocol::MeshCore))
|
||||
return "radio2 owned by another service";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool leaseRadio() {
|
||||
if (admission() || !profiles->useSecondaryProtocol(RadioProfileProtocol::Kiss)) return false;
|
||||
const auto* p = radio->profiles();
|
||||
if (!p || p->secondary.params.protocol != RadioProfileProtocol::Kiss) return false;
|
||||
leased = true;
|
||||
profile_generation = p->generation[1];
|
||||
intent_generation = profiles->secondaryIntentGeneration();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool current() {
|
||||
if (!leased || !active() || suspended || admission()) return false;
|
||||
const auto* p = radio->profiles();
|
||||
return p && p->generation[1] == profile_generation
|
||||
&& profiles->secondaryIntentGeneration() == intent_generation;
|
||||
}
|
||||
|
||||
const char* state(const Selection& selected, const char* reason) {
|
||||
if (!selected.enabled) return "off";
|
||||
if (suspended) return "suspended";
|
||||
if (!codec) return "unavailable";
|
||||
if (active()) return "connected";
|
||||
if (reason) return "blocked";
|
||||
#if defined(ESP32)
|
||||
if (selected.transport == Transport::Tcp) {
|
||||
if (!(WiFi.getMode() & WIFI_STA) || WiFi.status() != WL_CONNECTED
|
||||
|| !uint32_t(WiFi.localIP())) return "waiting-wifi";
|
||||
return tcp && tcp->listening ? "listening" : "starting";
|
||||
}
|
||||
#endif
|
||||
return "ready";
|
||||
}
|
||||
|
||||
void invalidateSession() {
|
||||
++discarded;
|
||||
releaseRadio();
|
||||
resetFrames();
|
||||
if (usb_session) {
|
||||
// Keep binary input quarantined until RETURN or a USB session boundary.
|
||||
// Releasing it to the ASCII parser could interpret a stale binary tail.
|
||||
suspended = true;
|
||||
} else {
|
||||
closeSession();
|
||||
#if defined(ESP32)
|
||||
// Drop accepts queued in the old profile session, not only its owner.
|
||||
if (tcp) { tcp->server.end(); tcp->listening = tcp->attempted = false; }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void queueControls() {
|
||||
if (!codec) return;
|
||||
// Completion must survive a full RX queue, rather than being silently lost.
|
||||
if (completion_pending && codec->queuedOutput() < Codec::QueueDepth) {
|
||||
codec->queueTxDone(completion_success);
|
||||
completion_pending = false;
|
||||
ready_pending = true;
|
||||
}
|
||||
if (ready_pending && codec->queuedOutput() < Codec::QueueDepth) {
|
||||
codec->queueReady();
|
||||
ready_pending = false;
|
||||
}
|
||||
}
|
||||
|
||||
void pumpTransmit() {
|
||||
if (!current() || transmitting || completion_pending || dispatcher->hostKissFramePending()) return;
|
||||
const uint8_t* bytes;
|
||||
size_t length;
|
||||
if (codec->peekTransmit(bytes, length)
|
||||
&& dispatcher->submitHostKissFrame(bytes, length, profile_generation)) {
|
||||
codec->consumeTransmit();
|
||||
transmitting = true;
|
||||
}
|
||||
}
|
||||
|
||||
void stopTransport() {
|
||||
closeSession();
|
||||
#if defined(ESP32)
|
||||
if (tcp) tcp->server.end();
|
||||
delete tcp;
|
||||
tcp = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool allocate(const Selection& selection, Codec*& next_codec
|
||||
#if defined(ESP32)
|
||||
, Tcp*& next_tcp
|
||||
#endif
|
||||
) {
|
||||
next_codec = codec;
|
||||
#if defined(ESP32)
|
||||
next_tcp = tcp;
|
||||
#endif
|
||||
if (!selection.enabled) return true;
|
||||
if (!next_codec) next_codec = new (std::nothrow) Codec;
|
||||
if (!next_codec) return false;
|
||||
#if defined(ESP32)
|
||||
if (selection.transport == Transport::Tcp && !next_tcp) {
|
||||
next_tcp = new (std::nothrow) Tcp;
|
||||
if (!next_tcp) {
|
||||
if (next_codec != codec) delete next_codec;
|
||||
next_codec = codec;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
void loopTcp() {
|
||||
if (!tcp) return;
|
||||
const bool station = (WiFi.getMode() & WIFI_STA) && WiFi.status() == WL_CONNECTED;
|
||||
const uint32_t address = station ? uint32_t(WiFi.localIP()) : 0;
|
||||
if (!address && !tcp->address && !tcp->listening && !tcp->connected) return;
|
||||
if (!address || address != tcp->address) {
|
||||
closeSession();
|
||||
tcp->server.end();
|
||||
tcp->listening = tcp->attempted = false;
|
||||
tcp->address = address;
|
||||
if (!address) return;
|
||||
// Never bind this unauthenticated modem to the setup AP.
|
||||
tcp->server = WiFiServer(IPAddress(address), TcpPort, 2);
|
||||
}
|
||||
const uint32_t now = millis();
|
||||
if (tcp->listening && !bool(tcp->server)) {
|
||||
tcp->server.end();
|
||||
tcp->listening = tcp->attempted = false;
|
||||
}
|
||||
if (!tcp->listening && (!tcp->attempted || uint32_t(now - tcp->last_attempt) >= 1000)) {
|
||||
tcp->last_attempt = now;
|
||||
tcp->attempted = true;
|
||||
tcp->server.begin();
|
||||
tcp->listening = bool(tcp->server);
|
||||
}
|
||||
if (tcp->connected && !tcp->client.connected()) closeSession();
|
||||
if (tcp->listening) {
|
||||
WiFiClient incoming = tcp->server.available();
|
||||
if (incoming) {
|
||||
if (tcp->connected || admission()) incoming.stop();
|
||||
else {
|
||||
resetFrames();
|
||||
if (!leaseRadio()) incoming.stop();
|
||||
else {
|
||||
tcp->client = incoming;
|
||||
tcp->client.setNoDelay(true);
|
||||
tcp->connected = true;
|
||||
suspended = false;
|
||||
ready_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!tcp->connected) return;
|
||||
queueControls();
|
||||
const uint8_t* bytes;
|
||||
size_t length;
|
||||
if (codec->peekOutput(bytes, length)) {
|
||||
if (length > ByteBudget) length = ByteBudget;
|
||||
const int written = ::send(tcp->client.fd(), bytes, length, MSG_DONTWAIT);
|
||||
if (written > 0) codec->consumeOutput(size_t(written));
|
||||
else if (written < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
||||
closeSession(); return;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < ByteBudget && codec->canAcceptByte(); ++i) {
|
||||
uint8_t byte;
|
||||
const int got = ::recv(tcp->client.fd(), &byte, 1, MSG_DONTWAIT);
|
||||
if (got == 1) {
|
||||
codec->feed(byte);
|
||||
if (codec->returned()) { closeSession(); break; }
|
||||
} else {
|
||||
if (!got || (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) closeSession();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void begin(FILESYSTEM* fs, Radio& r, RadioProfileCLI& p, Dispatcher& d) {
|
||||
stopTransport();
|
||||
delete codec;
|
||||
codec = nullptr;
|
||||
radio = &r;
|
||||
profiles = &p;
|
||||
dispatcher = &d;
|
||||
received = transmitted = failed = discarded = 0;
|
||||
settings.begin(fs);
|
||||
const auto& selected = settings.selection();
|
||||
if (!selected.enabled || !r.supportsHostKiss()) return;
|
||||
#if !defined(ESP32)
|
||||
if (selected.transport == Transport::Tcp) return;
|
||||
#endif
|
||||
Codec* next_codec = nullptr;
|
||||
#if defined(ESP32)
|
||||
Tcp* next_tcp = nullptr;
|
||||
if (allocate(selected, next_codec, next_tcp)) { codec = next_codec; tcp = next_tcp; }
|
||||
#else
|
||||
if (allocate(selected, next_codec)) codec = next_codec;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool handleCommand(const char* command, char* reply, size_t capacity, bool local) {
|
||||
const bool get = !strncmp(command, "get ", 4);
|
||||
const bool set = !strncmp(command, "set ", 4);
|
||||
if (!get && !set) return false;
|
||||
const char* key = command + 4;
|
||||
bool transport = !strncmp(key, "kiss.transport", 14)
|
||||
&& (!key[14] || key[14] == ' ');
|
||||
const bool status = !strncmp(key, "kiss", 4) && (!key[4] || key[4] == ' ');
|
||||
if (!transport && !status) return false;
|
||||
const char* value = key + (transport ? 14 : 4);
|
||||
if (get) {
|
||||
if (*value) { snprintf(reply, capacity, "Error: use get kiss%s", transport ? ".transport" : ""); return true; }
|
||||
const auto& selected = settings.selection();
|
||||
if (transport) snprintf(reply, capacity, "> %s", selected.transport == Transport::Usb ? "usb" : "tcp");
|
||||
else {
|
||||
const char* reason = admission();
|
||||
snprintf(reply, capacity,
|
||||
"> %s transport=%s state=%s rx=%lu tx=%lu fail=%lu drop=%lu bad=%lu%s%s",
|
||||
selected.enabled ? "on" : "off", selected.transport == Transport::Usb ? "usb" : "tcp:5004",
|
||||
state(selected, reason),
|
||||
(unsigned long)received, (unsigned long)transmitted, (unsigned long)failed,
|
||||
(unsigned long)(discarded + (codec ? codec->droppedFrames() : 0)),
|
||||
(unsigned long)(codec ? codec->malformedFrames() : 0),
|
||||
selected.enabled && !active() && reason ? " reason=" : "",
|
||||
selected.enabled && !active() && reason ? reason : "");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!local) { snprintf(reply, capacity, "Error: KISS settings require a local connection"); return true; }
|
||||
while (*value == ' ') ++value;
|
||||
Selection candidate = settings.selection();
|
||||
if (transport) {
|
||||
if (!strcmp(value, "usb")) candidate.transport = Transport::Usb;
|
||||
else if (!strcmp(value, "tcp")) candidate.transport = Transport::Tcp;
|
||||
else { snprintf(reply, capacity, "Error: use set kiss.transport usb|tcp"); return true; }
|
||||
#if !defined(ESP32)
|
||||
if (candidate.transport == Transport::Tcp) { snprintf(reply, capacity, "Error: TCP KISS requires ESP32 station WiFi"); return true; }
|
||||
#endif
|
||||
} else if (!strcmp(value, "on")) candidate.enabled = true;
|
||||
else if (!strcmp(value, "off")) candidate.enabled = false;
|
||||
else { snprintf(reply, capacity, "Error: use set kiss on|off"); return true; }
|
||||
if (candidate.enabled && (!radio || !radio->supportsHostKiss())) {
|
||||
snprintf(reply, capacity, "Error: raw radio2 KISS is unsupported by this radio"); return true;
|
||||
}
|
||||
if (candidate == settings.selection() && settings.healthy()) { snprintf(reply, capacity, "OK - KISS unchanged"); return true; }
|
||||
Codec* next_codec;
|
||||
#if defined(ESP32)
|
||||
Tcp* next_tcp;
|
||||
if (!allocate(candidate, next_codec, next_tcp)) {
|
||||
#else
|
||||
if (!allocate(candidate, next_codec)) {
|
||||
#endif
|
||||
snprintf(reply, capacity, "Error: insufficient memory; KISS unchanged"); return true;
|
||||
}
|
||||
// Persist before disrupting the old stream. A failed save leaves its exact
|
||||
// radio lease and pending frames intact.
|
||||
if (!settings.save(candidate)) {
|
||||
if (next_codec != codec) delete next_codec;
|
||||
#if defined(ESP32)
|
||||
if (next_tcp != tcp) delete next_tcp;
|
||||
#endif
|
||||
snprintf(reply, capacity, "Error: KISS save failed; service unchanged"); return true;
|
||||
}
|
||||
closeSession();
|
||||
codec = next_codec;
|
||||
#if defined(ESP32)
|
||||
tcp = next_tcp;
|
||||
if (tcp && (!candidate.enabled || candidate.transport != Transport::Tcp)) {
|
||||
tcp->server.end(); delete tcp; tcp = nullptr;
|
||||
}
|
||||
#endif
|
||||
if (!candidate.enabled) { delete codec; codec = nullptr; }
|
||||
snprintf(reply, capacity, candidate.enabled
|
||||
? "OK - KISS on (%s); get kiss for status" : "OK - KISS off",
|
||||
candidate.transport == Transport::Usb ? "USB: send KISS FEND to start" : "station TCP 5004");
|
||||
return true;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!settings.selection().enabled || !codec) return;
|
||||
if (active() && !suspended && !current()) invalidateSession();
|
||||
#if defined(ESP32)
|
||||
if (settings.selection().transport == Transport::Tcp) loopTcp();
|
||||
#endif
|
||||
pumpTransmit();
|
||||
}
|
||||
|
||||
bool serviceUsb() {
|
||||
if (!settings.selection().enabled || settings.selection().transport != Transport::Usb) return false;
|
||||
Stream& port = usbCompanionPort();
|
||||
if (!usb_session) {
|
||||
if (!port.available() || port.peek() != Codec::Fend) return false;
|
||||
resetFrames();
|
||||
usb_session = true;
|
||||
suspended = !codec || !leaseRadio();
|
||||
setUsbBinaryStreamOwned(true);
|
||||
ready_pending = !suspended;
|
||||
}
|
||||
// USB text writers may have been admitted just before the binary claim.
|
||||
// Their nonblocking retirement must finish before READY or host bytes move.
|
||||
if (!isUsbBinaryStreamReady()) return true;
|
||||
if (!codec) {
|
||||
// Allocation/capability failures must also keep binary bytes out of CLI.
|
||||
// This three-state RETURN recognizer needs no packet buffer.
|
||||
for (size_t i = 0; i < ByteBudget && port.available(); ++i) {
|
||||
const int value = port.read();
|
||||
if (value < 0) break;
|
||||
if (value == Codec::Fend) {
|
||||
if (quarantine_return == 2) { closeSession(); return true; }
|
||||
quarantine_return = 1;
|
||||
} else if (value == 0xff && quarantine_return == 1) quarantine_return = 2;
|
||||
else quarantine_return = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!suspended && !current()) invalidateSession();
|
||||
if (!suspended) {
|
||||
queueControls();
|
||||
const uint8_t* bytes;
|
||||
size_t length;
|
||||
if (codec->peekOutput(bytes, length)) {
|
||||
if (length > ByteBudget) length = ByteBudget;
|
||||
codec->consumeOutput(port.write(bytes, length));
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < ByteBudget && port.available() && codec->canAcceptByte(); ++i) {
|
||||
const int value = port.read();
|
||||
if (value < 0) break;
|
||||
codec->feed(uint8_t(value));
|
||||
if (codec->returned()) { closeSession(); return true; }
|
||||
if (suspended) codec->consumeTransmit(); // Never queue TX while suspended.
|
||||
}
|
||||
pumpTransmit();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool usbActive() { return usb_session; }
|
||||
bool radioActive() { return current(); }
|
||||
void resetUsbSession() { if (usb_session) closeSession(); }
|
||||
|
||||
void receive(const uint8_t* bytes, size_t length, uint8_t profile,
|
||||
uint32_t generation, float, float) {
|
||||
if (!codec || profile != 1 || generation != profile_generation || !current()) return;
|
||||
if (codec->queueReceive(bytes, length)) ++received;
|
||||
}
|
||||
|
||||
void sendComplete(bool success) {
|
||||
// A completion from an invalidated session must not reach its replacement.
|
||||
if (!transmitting) return;
|
||||
transmitting = false;
|
||||
if (!current()) return;
|
||||
if (success) ++transmitted;
|
||||
else ++failed;
|
||||
completion_success = success;
|
||||
completion_pending = true;
|
||||
}
|
||||
|
||||
}} // namespace mesh::hostkiss
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/HostKissConfig.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
#include <helpers/IdentityStore.h>
|
||||
namespace mesh {
|
||||
class Radio;
|
||||
class RadioProfileCLI;
|
||||
class Dispatcher;
|
||||
namespace hostkiss {
|
||||
void begin(FILESYSTEM* fs, Radio& radio, RadioProfileCLI& profiles, Dispatcher& dispatcher);
|
||||
bool handleCommand(const char* command, char* reply, size_t capacity, bool local);
|
||||
void loop();
|
||||
bool serviceUsb();
|
||||
bool usbActive();
|
||||
bool radioActive();
|
||||
void resetUsbSession();
|
||||
void receive(const uint8_t* bytes, size_t length, uint8_t profile,
|
||||
uint32_t generation, float snr, float rssi);
|
||||
void sendComplete(bool success);
|
||||
}}
|
||||
#else
|
||||
namespace mesh { namespace hostkiss {
|
||||
template<class F, class R, class P, class D> inline void begin(F*, R&, P&, D&) {}
|
||||
inline bool handleCommand(const char*, char*, size_t, bool) { return false; }
|
||||
inline void loop() {}
|
||||
inline bool serviceUsb() { return false; }
|
||||
inline bool usbActive() { return false; }
|
||||
inline bool radioActive() { return false; }
|
||||
inline void resetUsbSession() {}
|
||||
inline void receive(const uint8_t*, size_t, uint8_t, uint32_t, float, float) {}
|
||||
inline void sendComplete(bool) {}
|
||||
}}
|
||||
#endif
|
||||
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace hostkiss {
|
||||
|
||||
// Generic KISS framing, independently implemented. No RNode commands or
|
||||
// over-air fragmentation are implied by the host stream.
|
||||
class Codec {
|
||||
public:
|
||||
static constexpr uint8_t Fend = 0xc0, Fesc = 0xdb, Tfend = 0xdc, Tfesc = 0xdd;
|
||||
static constexpr size_t MaximumFrame = 255, QueueDepth = 2;
|
||||
static constexpr size_t MaximumEncoded = MaximumFrame * 2 + 3;
|
||||
static constexpr uint8_t Ready = 0x0f, TxDone = 0xf8;
|
||||
private:
|
||||
struct Frame { uint8_t bytes[MaximumFrame]; uint16_t length = 0; } tx_[QueueDepth];
|
||||
struct Output { uint8_t bytes[MaximumEncoded]; uint16_t length = 0; } out_[QueueDepth];
|
||||
uint8_t tx_head_ = 0, tx_count_ = 0, out_head_ = 0, out_count_ = 0;
|
||||
uint8_t input_[MaximumFrame];
|
||||
uint16_t input_size_ = 0, output_offset_ = 0;
|
||||
uint8_t command_ = 0;
|
||||
bool framed_ = false, has_command_ = false, escaped_ = false, discard_ = false;
|
||||
bool return_ = false;
|
||||
uint32_t malformed_ = 0, dropped_ = 0;
|
||||
|
||||
void malformed() { if (!discard_) ++malformed_; discard_ = true; }
|
||||
bool queueOutput(uint8_t command, const uint8_t* bytes, size_t length) {
|
||||
if (length > MaximumFrame || out_count_ >= QueueDepth || (!bytes && length)) {
|
||||
++dropped_; return false;
|
||||
}
|
||||
auto& frame = out_[(out_head_ + out_count_) % QueueDepth];
|
||||
size_t at = 0;
|
||||
frame.bytes[at++] = Fend;
|
||||
// Commands used by this service never need escaping.
|
||||
frame.bytes[at++] = command;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
if (bytes[i] == Fend || bytes[i] == Fesc) {
|
||||
frame.bytes[at++] = Fesc;
|
||||
frame.bytes[at++] = bytes[i] == Fend ? Tfend : Tfesc;
|
||||
} else frame.bytes[at++] = bytes[i];
|
||||
}
|
||||
frame.bytes[at++] = Fend;
|
||||
frame.length = uint16_t(at);
|
||||
++out_count_;
|
||||
return true;
|
||||
}
|
||||
public:
|
||||
void reset() {
|
||||
tx_head_ = tx_count_ = out_head_ = out_count_ = 0;
|
||||
input_size_ = output_offset_ = 0;
|
||||
framed_ = has_command_ = escaped_ = discard_ = return_ = false;
|
||||
}
|
||||
bool canAcceptByte() const { return tx_count_ < QueueDepth && !return_; }
|
||||
// False leaves ownership of this byte with the transport (backpressure).
|
||||
bool feed(uint8_t byte) {
|
||||
if (!canAcceptByte()) return false;
|
||||
if (byte == Fend) {
|
||||
if (framed_ && has_command_ && !discard_) {
|
||||
if (escaped_) malformed();
|
||||
else if (command_ == 0xff) {
|
||||
if (!input_size_) return_ = true;
|
||||
else malformed();
|
||||
} else if (command_ == 0) {
|
||||
if (!input_size_) malformed();
|
||||
else {
|
||||
auto& frame = tx_[(tx_head_ + tx_count_) % QueueDepth];
|
||||
memcpy(frame.bytes, input_, input_size_);
|
||||
frame.length = input_size_;
|
||||
++tx_count_;
|
||||
}
|
||||
}
|
||||
}
|
||||
framed_ = true;
|
||||
has_command_ = escaped_ = discard_ = false;
|
||||
input_size_ = 0;
|
||||
return true;
|
||||
}
|
||||
if (!framed_ || discard_) return true;
|
||||
if (!has_command_) {
|
||||
has_command_ = true;
|
||||
command_ = byte;
|
||||
// Port 0 DATA always means radio2. Other ports and unsupported KISS
|
||||
// parameter commands cannot alter the radio or reach MeshCore radio1.
|
||||
if (command_ != 0 && command_ != 0xff) discard_ = true;
|
||||
return true;
|
||||
}
|
||||
if (escaped_) {
|
||||
escaped_ = false;
|
||||
if (byte == Tfend) byte = Fend;
|
||||
else if (byte == Tfesc) byte = Fesc;
|
||||
else { malformed(); return true; }
|
||||
} else if (byte == Fesc) {
|
||||
escaped_ = true;
|
||||
return true;
|
||||
}
|
||||
if (input_size_ >= MaximumFrame) { malformed(); return true; }
|
||||
input_[input_size_++] = byte;
|
||||
return true;
|
||||
}
|
||||
bool returned() const { return return_; }
|
||||
bool peekTransmit(const uint8_t*& bytes, size_t& length) const {
|
||||
if (!tx_count_) { bytes = nullptr; length = 0; return false; }
|
||||
bytes = tx_[tx_head_].bytes;
|
||||
length = tx_[tx_head_].length;
|
||||
return true;
|
||||
}
|
||||
void consumeTransmit() {
|
||||
if (tx_count_) { tx_head_ = (tx_head_ + 1) % QueueDepth; --tx_count_; }
|
||||
}
|
||||
bool queueReceive(const uint8_t* bytes, size_t length) {
|
||||
if (!length) { ++dropped_; return false; }
|
||||
return queueOutput(0, bytes, length);
|
||||
}
|
||||
bool queueReady() { const uint8_t value = 1; return queueOutput(Ready, &value, 1); }
|
||||
bool queueTxDone(bool success) {
|
||||
// Match the existing standalone modem's SetHardware extension. A command
|
||||
// byte of 0xf8 would instead mean port 15, command 8 to a generic parser.
|
||||
const uint8_t value[] = {TxDone, uint8_t(success ? 1 : 0)};
|
||||
return queueOutput(0x06, value, sizeof(value));
|
||||
}
|
||||
bool peekOutput(const uint8_t*& bytes, size_t& length) const {
|
||||
if (!out_count_) { bytes = nullptr; length = 0; return false; }
|
||||
bytes = out_[out_head_].bytes + output_offset_;
|
||||
length = out_[out_head_].length - output_offset_;
|
||||
return true;
|
||||
}
|
||||
void consumeOutput(size_t length) {
|
||||
if (!out_count_) return;
|
||||
const size_t remaining = out_[out_head_].length - output_offset_;
|
||||
if (length >= remaining) {
|
||||
output_offset_ = 0;
|
||||
out_head_ = (out_head_ + 1) % QueueDepth;
|
||||
--out_count_;
|
||||
} else output_offset_ += uint16_t(length);
|
||||
}
|
||||
uint8_t queuedTransmit() const { return tx_count_; }
|
||||
uint8_t queuedOutput() const { return out_count_; }
|
||||
uint32_t malformedFrames() const { return malformed_; }
|
||||
uint32_t droppedFrames() const { return dropped_; }
|
||||
};
|
||||
|
||||
}} // namespace mesh::hostkiss
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
// A host-assisted raw radio2 modem in Full firmware. Keep this independent of
|
||||
// chip/Arduino headers: all translation units must choose identical layouts.
|
||||
// Full infrastructure builds opt in explicitly; reduced builds stay unchanged.
|
||||
#ifndef MESH_ENABLE_HOST_KISS
|
||||
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) \
|
||||
&& defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL \
|
||||
&& !(defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW)
|
||||
#define MESH_ENABLE_HOST_KISS 1
|
||||
#else
|
||||
#define MESH_ENABLE_HOST_KISS 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ESP-NOW-only images have no independent LoRa modem for this transport.
|
||||
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
|
||||
#undef MESH_ENABLE_HOST_KISS
|
||||
#define MESH_ENABLE_HOST_KISS 0
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "HostKissConfig.h"
|
||||
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
namespace mesh {
|
||||
namespace hostkiss {
|
||||
// Called by the Dispatcher only after radio2 protocol/generation validation.
|
||||
// This is the sole RX consumer, before MeshCore logs, parsing and bridges.
|
||||
void receive(const uint8_t* bytes, size_t length, uint8_t profile,
|
||||
uint32_t generation, float snr, float rssi);
|
||||
void sendComplete(bool success);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "HostKissSettings.h"
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
#include "CheckedSettingsFile.h"
|
||||
#include "FilePresence.h"
|
||||
#include "PersistentStoreFormat.h"
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include "AtomicFileWriter.h"
|
||||
#else
|
||||
#include "ContactFileTransaction.h"
|
||||
#endif
|
||||
|
||||
namespace mesh { namespace hostkiss {
|
||||
namespace {
|
||||
constexpr char Path[] = "/host_kiss";
|
||||
constexpr size_t ImageSize = 12;
|
||||
void encode(uint8_t (&image)[ImageSize], const Selection& value) {
|
||||
memset(image, 0, sizeof(image));
|
||||
memcpy(image, "HKS1", 4);
|
||||
image[4] = value.enabled ? 1 : 0;
|
||||
image[5] = uint8_t(value.transport);
|
||||
storage::writeLE32(image + 8, storage::updateCRC32(0xffffffff, image, 8));
|
||||
}
|
||||
bool valid(const uint8_t (&image)[ImageSize]) {
|
||||
return memcmp(image, "HKS1", 4) == 0 && image[4] <= 1 && image[5] <= 1
|
||||
&& image[6] == 0 && image[7] == 0
|
||||
&& storage::readLE32(image + 8) == storage::updateCRC32(0xffffffff, image, 8);
|
||||
}
|
||||
}
|
||||
bool Settings::begin(FILESYSTEM* fs) {
|
||||
fs_ = fs;
|
||||
healthy_ = false;
|
||||
selection_ = {};
|
||||
if (!fs_) return false;
|
||||
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) || defined(ESP32)
|
||||
if (!ContactFileTransaction::recover(fs_, Path, filePresence<FILESYSTEM>)) return false;
|
||||
#endif
|
||||
bool present = false;
|
||||
if (!filePresence(fs_, Path, present)) return false;
|
||||
if (!present) { healthy_ = true; return true; }
|
||||
uint8_t image[ImageSize];
|
||||
const auto result = storage::readFixedSettingsFileResult(fs_, Path, image, sizeof(image));
|
||||
if (result == storage::FixedSettingsRead::Unreadable) return false;
|
||||
healthy_ = true; // A complete malformed record may be explicitly replaced.
|
||||
if (result != storage::FixedSettingsRead::Complete || !valid(image)) return false;
|
||||
selection_.enabled = image[4] != 0;
|
||||
selection_.transport = Transport(image[5]);
|
||||
return true;
|
||||
}
|
||||
bool Settings::save(const Selection& selection) {
|
||||
if (!healthy_ || !fs_ || uint8_t(selection.transport) > 1) return false;
|
||||
uint8_t image[ImageSize];
|
||||
encode(image, selection);
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
AtomicFileWriter writer(fs_, Path);
|
||||
const bool wrote = writer && writer.write(image, sizeof(image)) == sizeof(image);
|
||||
const bool saved = writer.commit(wrote);
|
||||
if (writer.outcome() == AtomicFileWriter::Outcome::Indeterminate) healthy_ = false;
|
||||
if (!saved) return false;
|
||||
#else
|
||||
ContactFileTransaction writer(fs_, Path, filePresence<FILESYSTEM>);
|
||||
if (!writer || writer.write(image, sizeof(image)) != sizeof(image)) {
|
||||
if (writer.outcome() == ContactFileTransaction::Outcome::Indeterminate) healthy_ = false;
|
||||
return false;
|
||||
}
|
||||
writer.commit();
|
||||
if (writer.outcome() == ContactFileTransaction::Outcome::NotCommitted) return false;
|
||||
#endif
|
||||
uint8_t verified[ImageSize];
|
||||
healthy_ = storage::readFixedSettingsFile(fs_, Path, verified, sizeof(verified))
|
||||
&& memcmp(image, verified, sizeof(image)) == 0;
|
||||
if (!healthy_) return false;
|
||||
selection_ = selection;
|
||||
return true;
|
||||
}
|
||||
}} // namespace mesh::hostkiss
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/HostKissConfig.h>
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
#include <helpers/IdentityStore.h>
|
||||
|
||||
namespace mesh { namespace hostkiss {
|
||||
enum class Transport : uint8_t { Usb = 0, Tcp = 1 };
|
||||
struct Selection {
|
||||
bool enabled = false;
|
||||
Transport transport = Transport::Usb;
|
||||
bool operator==(const Selection& value) const {
|
||||
return enabled == value.enabled && transport == value.transport;
|
||||
}
|
||||
};
|
||||
class Settings {
|
||||
FILESYSTEM* fs_ = nullptr;
|
||||
bool healthy_ = false;
|
||||
Selection selection_;
|
||||
public:
|
||||
bool begin(FILESYSTEM* fs);
|
||||
bool save(const Selection& selection);
|
||||
const Selection& selection() const { return selection_; }
|
||||
bool healthy() const { return healthy_; }
|
||||
};
|
||||
}}
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "BeaconFeatureConfig.h"
|
||||
#include "EthernetTunnelConfig.h"
|
||||
#include "HostKissConfig.h"
|
||||
#include "ota/OtaDirectConfig.h"
|
||||
|
||||
// Full Companion includes channel chat on both supported platforms. Keep this
|
||||
@@ -37,5 +38,5 @@
|
||||
// dispatcher slot. Direct OTA retains its temporary MeshCore profile; foreign
|
||||
// transports keep their existing radio2-only ownership.
|
||||
#ifndef MESH_ENABLE_FOREIGN_RADIO
|
||||
#define MESH_ENABLE_FOREIGN_RADIO (MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_OTA_KISS_DIRECT)
|
||||
#define MESH_ENABLE_FOREIGN_RADIO (MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_OTA_KISS_DIRECT || MESH_ENABLE_HOST_KISS)
|
||||
#endif
|
||||
@@ -184,7 +184,7 @@ bool RadioProfileCLI::prepareSavedImage(const char* path, const RadioProfileConf
|
||||
uint16_t preamble, RadioCrossMode cross, int8_t manual_secondary) {
|
||||
if (!fs_ || hold_) return false;
|
||||
uint8_t image[ImageSize] = {'R', '2', 1, (uint8_t)config.mode};
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
// Version 2 keeps the 24-byte layout and CRC; bit 7 records explicit radio2
|
||||
// intent. Other settings can save an unused Off slot without claiming it.
|
||||
image[2] = 2;
|
||||
@@ -408,7 +408,7 @@ void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool in
|
||||
recoverable_corrupt_store_ = false;
|
||||
manual_secondary_ = false;
|
||||
secondary_intent_generation_ = 0;
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
auto_secondary_requested_ = secondary_protocol_overlay_ = false;
|
||||
owns_secondary_runtime_ = external_secondary_override_ = false;
|
||||
#endif
|
||||
@@ -512,7 +512,7 @@ void RadioProfileCLI::publish() {
|
||||
? (validReplySetting(reply_setting_) ? reply_setting_ & 7 : RADIO_TX_BOTH) : RADIO_TX_AUTO;
|
||||
profiles->reply_force = infrastructure_replies_
|
||||
&& validReplySetting(reply_setting_) && (reply_setting_ & 8);
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
protectExternalSecondary();
|
||||
if (external_secondary_override_) return;
|
||||
#endif
|
||||
@@ -522,7 +522,7 @@ void RadioProfileCLI::publish() {
|
||||
if (hasReplyMutation() && !remote_delivered_) return;
|
||||
publish_pending_ = false;
|
||||
RadioProfileConfig config = temp_active_ ? temporary_ : saved_;
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (auto_secondary_requested_ && canAutoSecondary()) config = auto_secondary_;
|
||||
else if (secondary_protocol_overlay_ && manualSecondaryConfigured() && !secondaryRuntimeBusy()) {
|
||||
config.params.protocol = secondary_protocol_;
|
||||
@@ -530,7 +530,7 @@ void RadioProfileCLI::publish() {
|
||||
}
|
||||
#endif
|
||||
profiles->setSecondary(config, temp_active_);
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
owns_secondary_runtime_ = auto_secondary_requested_ || secondary_protocol_overlay_;
|
||||
if (owns_secondary_runtime_) {
|
||||
owned_secondary_snapshot_ = config;
|
||||
@@ -539,7 +539,7 @@ void RadioProfileCLI::publish() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
bool RadioProfileCLI::runtimeSecondaryUnchanged() const {
|
||||
if (!owns_secondary_runtime_ || !radio_ || !radio_->profiles()) return true;
|
||||
const auto* profiles = radio_->profiles();
|
||||
@@ -564,7 +564,7 @@ bool RadioProfileCLI::hasSecondarySchedule() const {
|
||||
}
|
||||
|
||||
bool RadioProfileCLI::secondaryRuntimeBusy() const {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (external_secondary_override_ || !runtimeSecondaryUnchanged()) return true;
|
||||
#endif
|
||||
if (!fs_ || !radio_) return true;
|
||||
@@ -576,14 +576,14 @@ bool RadioProfileCLI::secondaryRuntimeBusy() const {
|
||||
|
||||
void RadioProfileCLI::noteSecondaryManualChange() {
|
||||
++secondary_intent_generation_;
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
auto_secondary_requested_ = secondary_protocol_overlay_ = false;
|
||||
owns_secondary_runtime_ = external_secondary_override_ = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool RadioProfileCLI::canAutoSecondary() const {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (manual_secondary_ || saved_.mode != RadioProfileMode::Off || secondaryRuntimeBusy()) return false;
|
||||
const auto& current = radio_->profiles()->secondary;
|
||||
return current.mode == RadioProfileMode::Off || (auto_secondary_requested_
|
||||
@@ -594,7 +594,7 @@ bool RadioProfileCLI::canAutoSecondary() const {
|
||||
}
|
||||
|
||||
bool RadioProfileCLI::requestAutoSecondary(const RadioProfileConfig& config) {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (!canAutoSecondary() || config.mode == RadioProfileMode::Off
|
||||
|| (uint8_t)config.mode > (uint8_t)RadioProfileMode::RxTx
|
||||
|| !radio_->validateProfile(config.params)) return false;
|
||||
@@ -614,7 +614,7 @@ bool RadioProfileCLI::requestAutoSecondary(const RadioProfileConfig& config) {
|
||||
}
|
||||
|
||||
void RadioProfileCLI::clearAutoSecondary() {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (!auto_secondary_requested_) return;
|
||||
protectExternalSecondary();
|
||||
auto_secondary_requested_ = false;
|
||||
@@ -624,7 +624,7 @@ void RadioProfileCLI::clearAutoSecondary() {
|
||||
}
|
||||
|
||||
bool RadioProfileCLI::autoSecondaryActive() const {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (!auto_secondary_requested_ || !canAutoSecondary()) return false;
|
||||
const auto& current = radio_->profiles()->secondary;
|
||||
return !radio_->profiles()->secondary_temporary && current.mode == auto_secondary_.mode
|
||||
@@ -635,7 +635,7 @@ bool RadioProfileCLI::autoSecondaryActive() const {
|
||||
}
|
||||
|
||||
bool RadioProfileCLI::useSecondaryProtocol(RadioProfileProtocol protocol) {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (protocol == RadioProfileProtocol::MeshCore) {
|
||||
clearSecondaryProtocol();
|
||||
return true;
|
||||
@@ -646,6 +646,9 @@ bool RadioProfileCLI::useSecondaryProtocol(RadioProfileProtocol protocol) {
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
protocol == RadioProfileProtocol::Ethernet ||
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
protocol == RadioProfileProtocol::Kiss ||
|
||||
#endif
|
||||
false;
|
||||
if (!supported_protocol || !manualSecondaryConfigured()
|
||||
@@ -669,7 +672,7 @@ bool RadioProfileCLI::useSecondaryProtocol(RadioProfileProtocol protocol) {
|
||||
}
|
||||
|
||||
void RadioProfileCLI::clearSecondaryProtocol() {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (!secondary_protocol_overlay_) return;
|
||||
protectExternalSecondary();
|
||||
secondary_protocol_overlay_ = false;
|
||||
@@ -1146,7 +1149,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
return true;
|
||||
}
|
||||
if ((base || temporary) && verb == Get && !*args) {
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (base && autoSecondaryActive()) {
|
||||
formatConfig(reply, capacity, auto_secondary_, false, 0, true);
|
||||
return true;
|
||||
@@ -1161,7 +1164,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
temporary, temp_pending_ ? pending_duration_ms_ : temp_remaining_ms_);
|
||||
return true;
|
||||
}
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
if (base && verb == Set && !strcmp(args, "auto")) {
|
||||
// This clears only saved permission, never an active/pending lease or a
|
||||
// schedule. Remote acknowledgement staging must not weaken that contract.
|
||||
@@ -1322,7 +1325,7 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
noteSecondaryManualChange();
|
||||
pending_temporary_ = config; pending_duration_ms_ = minutes * 60000UL;
|
||||
temp_start_ms_ = millis() + 2000; temp_pending_ = true;
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
publish_pending_ = true; publish_after_ms_ = temp_start_ms_;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class RadioProfileCLI {
|
||||
// Saved Off can be an explicit veto, rather than an unused secondary slot.
|
||||
bool manual_secondary_ = false;
|
||||
uint32_t secondary_intent_generation_ = 0;
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT || MESH_ENABLE_ETHERNET_TUNNEL || MESH_ENABLE_HOST_KISS
|
||||
RadioProfileConfig auto_secondary_;
|
||||
bool auto_secondary_requested_ = false;
|
||||
bool secondary_protocol_overlay_ = false;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Guard cached text Stream references as well as new port lookups. Binary
|
||||
// owners write through their separate raw facade and are never wrapped here.
|
||||
class UsbBinaryTextGuard : public Stream {
|
||||
std::atomic<Stream*> _port{nullptr};
|
||||
bool (*_owned)();
|
||||
bool (*_enter)();
|
||||
void (*_leave)();
|
||||
bool enter() { return _enter ? _enter() : !_owned(); }
|
||||
void leave() { if (_leave) _leave(); }
|
||||
public:
|
||||
explicit UsbBinaryTextGuard(bool (*owned)(), bool (*enter)() = nullptr,
|
||||
void (*leave)() = nullptr)
|
||||
: _owned(owned), _enter(enter), _leave(leave) { }
|
||||
void use(Stream& port) { _port.store(&port, std::memory_order_release); }
|
||||
int available() override {
|
||||
auto* port = _port.load(std::memory_order_acquire);
|
||||
return !_owned() && port ? port->available() : 0;
|
||||
}
|
||||
int read() override {
|
||||
auto* port = _port.load(std::memory_order_acquire);
|
||||
return !_owned() && port ? port->read() : -1;
|
||||
}
|
||||
int peek() override {
|
||||
auto* port = _port.load(std::memory_order_acquire);
|
||||
return !_owned() && port ? port->peek() : -1;
|
||||
}
|
||||
void flush() override {
|
||||
if (!enter()) return;
|
||||
auto* port = _port.load(std::memory_order_acquire);
|
||||
if (port) port->flush();
|
||||
leave();
|
||||
}
|
||||
int availableForWrite() override {
|
||||
auto* port = _port.load(std::memory_order_acquire);
|
||||
return !_owned() && port ? port->availableForWrite() : 0;
|
||||
}
|
||||
size_t write(uint8_t byte) override { return write(&byte, 1); }
|
||||
size_t write(const uint8_t* data, size_t length) override {
|
||||
if (!data) return 0;
|
||||
// Suppressed text is deliberately consumed, never deferred into the next
|
||||
// CLI session. The caller must not retry it after the raw owner releases.
|
||||
if (!enter()) return length;
|
||||
auto* port = _port.load(std::memory_order_acquire);
|
||||
const size_t written = port ? port->write(data, length) : 0;
|
||||
leave();
|
||||
return written;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "UsbLogging.h"
|
||||
#include "UsbLoggingClientActivity.h"
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
#include "UsbBinaryTextGuard.h"
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO)
|
||||
#include <Arduino.h>
|
||||
@@ -91,6 +94,19 @@ static std::atomic<bool> usb_logging_enabled{true};
|
||||
#endif
|
||||
static std::atomic<bool> usb_logging_preference_known{false};
|
||||
static std::atomic<bool> usb_debug_enabled{false};
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
static std::atomic<bool> usb_binary_stream_owned{false};
|
||||
static std::atomic<uint32_t> usb_text_writers{0};
|
||||
|
||||
static bool beginUsbTextWrite() {
|
||||
if (usb_binary_stream_owned.load()) return false;
|
||||
usb_text_writers.fetch_add(1);
|
||||
if (!usb_binary_stream_owned.load()) return true;
|
||||
usb_text_writers.fetch_sub(1);
|
||||
return false;
|
||||
}
|
||||
static void endUsbTextWrite() { usb_text_writers.fetch_sub(1); }
|
||||
#endif
|
||||
static std::atomic<uint32_t> usb_logging_tx_progress{0};
|
||||
static std::atomic<bool> usb_logging_tx_waiting{false};
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING \
|
||||
@@ -364,7 +380,11 @@ static void serviceEsp32TinyUsbPorts() {
|
||||
esp32_tinyusb_terminal_reported_dropped_bytes =
|
||||
esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed);
|
||||
}
|
||||
if (canAccessEsp32TinyUsb(nullptr)) {
|
||||
if (isUsbBinaryStreamOwned()) {
|
||||
esp32_tinyusb_text_queue.discardPending();
|
||||
esp32_tinyusb_terminal_reported_dropped_bytes =
|
||||
esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed);
|
||||
} else if (canAccessEsp32TinyUsb(nullptr)) {
|
||||
esp32_tinyusb_text_queue.service();
|
||||
const uint32_t dropped =
|
||||
esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed);
|
||||
@@ -1245,6 +1265,31 @@ static WholeRecordNonBlockingStream<>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool isUsbBinaryStreamOwned() {
|
||||
return usb_binary_stream_owned.load();
|
||||
}
|
||||
|
||||
void setUsbBinaryStreamOwned(bool owned) {
|
||||
usb_binary_stream_owned.store(owned);
|
||||
if (owned) {
|
||||
discardUsbTerminalOutput();
|
||||
#if MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
invalidateEsp32SdkLogging();
|
||||
#endif
|
||||
}
|
||||
setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled());
|
||||
}
|
||||
|
||||
bool isUsbBinaryStreamReady() {
|
||||
// The producer may have obtained its cached Stream before ownership changed.
|
||||
// Never wait for it here: the raw session retries this gate next loop.
|
||||
if (!isUsbBinaryStreamOwned() || usb_text_writers.load() != 0) return false;
|
||||
discardUsbTerminalOutput();
|
||||
return !hasPendingUsbTerminalOutput();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool isUsbLoggingEnabled() {
|
||||
return usb_logging_enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
@@ -1598,7 +1643,7 @@ bool resetUsbCompanionTransport() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Stream& usbLoggingPort() {
|
||||
static Stream& rawUsbLoggingPort() {
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
if (!isUsbLoggingPacketStream()) {
|
||||
if (isUsbLoggingEnabled() && dedicated_usb_logging_port_started
|
||||
@@ -1620,6 +1665,16 @@ Stream& usbLoggingPort() {
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbLoggingPort() {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
static UsbBinaryTextGuard guarded(isUsbBinaryStreamOwned, beginUsbTextWrite, endUsbTextWrite);
|
||||
guarded.use(rawUsbLoggingPort());
|
||||
return guarded;
|
||||
#else
|
||||
return rawUsbLoggingPort();
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbDebugPort() {
|
||||
if (!isUsbDebugLoggingEnabled()) return null_usb_logging_stream;
|
||||
return usbLoggingPort();
|
||||
@@ -1655,7 +1710,7 @@ Stream& usbMotaPort() {
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbTerminalPort(bool enabled) {
|
||||
static Stream& rawUsbTerminalPort(bool enabled) {
|
||||
if (!enabled) return null_usb_logging_stream;
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
return buffered_esp32_tinyusb_terminal_port;
|
||||
@@ -1666,7 +1721,7 @@ Stream& usbTerminalPort(bool enabled) {
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbConsolePort() {
|
||||
static Stream& rawUsbConsolePort() {
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
return buffered_esp32_tinyusb_terminal_port;
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
@@ -1678,7 +1733,29 @@ Stream& usbConsolePort() {
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbTerminalPort(bool enabled) {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
static UsbBinaryTextGuard guarded(isUsbBinaryStreamOwned, beginUsbTextWrite, endUsbTextWrite);
|
||||
if (!enabled) return null_usb_logging_stream;
|
||||
guarded.use(rawUsbTerminalPort(true));
|
||||
return guarded;
|
||||
#else
|
||||
return rawUsbTerminalPort(enabled);
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbConsolePort() {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
static UsbBinaryTextGuard guarded(isUsbBinaryStreamOwned, beginUsbTextWrite, endUsbTextWrite);
|
||||
guarded.use(rawUsbConsolePort());
|
||||
return guarded;
|
||||
#else
|
||||
return rawUsbConsolePort();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool canAcceptUsbConsoleCommand() {
|
||||
if (isUsbBinaryStreamOwned()) return false;
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
return buffered_esp32_tinyusb_terminal_port.availableForWrite()
|
||||
>= static_cast<int>(esp32_tinyusb_functional_reserve);
|
||||
@@ -1709,6 +1786,11 @@ void serviceUsbTerminalPort() {
|
||||
primary_usb_terminal_discard_pending.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (isUsbBinaryStreamOwned()) {
|
||||
if (!buffered_primary_usb_terminal_port.tryDiscardPending())
|
||||
primary_usb_terminal_discard_pending.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
buffered_primary_usb_terminal_port.service();
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
serviceEsp32HwcdcTxKick();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "UsbLoggingStatus.h"
|
||||
#include "HostKissConfig.h"
|
||||
|
||||
// Canonical USB-capable images compile at least one of these two diagnostics.
|
||||
// A master USB output gate and a separate debug-verbosity gate keep packet
|
||||
@@ -53,6 +54,19 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// A raw host session owns the primary USB byte stream without changing the
|
||||
// saved packet/debug logging preference. Functional text and diagnostics must
|
||||
// stay out of that binary stream, including writers retaining a Stream reference.
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool isUsbBinaryStreamOwned();
|
||||
void setUsbBinaryStreamOwned(bool owned);
|
||||
bool isUsbBinaryStreamReady();
|
||||
#else
|
||||
inline bool isUsbBinaryStreamOwned() { return false; }
|
||||
inline void setUsbBinaryStreamOwned(bool) { }
|
||||
inline bool isUsbBinaryStreamReady() { return true; }
|
||||
#endif
|
||||
|
||||
#ifndef MESH_ESP32_USB_TX_BUFFER_SIZE
|
||||
#define MESH_ESP32_USB_TX_BUFFER_SIZE 4096
|
||||
#endif
|
||||
@@ -83,7 +97,7 @@ void setUsbLoggingEnabled(bool enabled);
|
||||
bool isUsbDebugEnabled();
|
||||
void setUsbDebugEnabled(bool enabled);
|
||||
inline bool isUsbDebugLoggingEnabled() {
|
||||
return isUsbLoggingEnabled() && isUsbDebugEnabled();
|
||||
return !isUsbBinaryStreamOwned() && isUsbLoggingEnabled() && isUsbDebugEnabled();
|
||||
}
|
||||
|
||||
// Kept as a platform-neutral persistence hook. Single-TTY builds need no
|
||||
|
||||
@@ -19,6 +19,9 @@ public:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -26,10 +29,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
&& protocol != mesh::RadioProfileProtocol::Kiss
|
||||
#endif
|
||||
) return false;
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
return static_cast<CustomLLCC68*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ public:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
@@ -48,10 +51,14 @@ protected:
|
||||
void setDriverSwitchFeatures(uint8_t features) override {
|
||||
static_cast<CustomLR1110*>(_radio)->setProfileSwitchFeatures(features);
|
||||
}
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
&& protocol != mesh::RadioProfileProtocol::Kiss
|
||||
#endif
|
||||
) return false;
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
return static_cast<CustomLR1110*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ public:
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
@@ -34,10 +37,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
&& protocol != mesh::RadioProfileProtocol::Kiss
|
||||
#endif
|
||||
) return false;
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
return static_cast<CustomLR1121*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -173,6 +173,9 @@ public:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -198,10 +201,14 @@ protected:
|
||||
recordApply(stage, status);
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
&& protocol != mesh::RadioProfileProtocol::Kiss
|
||||
#endif
|
||||
) return false;
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
return recordApply(mesh::lr2021::ApplyStatus::Stage::Protocol,
|
||||
static_cast<CustomLR2021*>(_radio)->setProfileProtocol(sync)) == RADIOLIB_ERR_NONE;
|
||||
|
||||
@@ -31,17 +31,23 @@ public:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_MORSE_RX
|
||||
bool supportsMorseRx() const override { return supportsRadioDeepInit(); }
|
||||
#endif
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
uint8_t sync;
|
||||
if (protocol == mesh::RadioProfileProtocol::MeshCore) sync = 0x12;
|
||||
else if (protocol == mesh::RadioProfileProtocol::Meshtext) sync = 0x2b;
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
else if (protocol == mesh::RadioProfileProtocol::Kiss) sync = 0x12;
|
||||
#endif
|
||||
else return false;
|
||||
return static_cast<CustomSTM32WLx*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ public:
|
||||
}
|
||||
~CustomSX1262Wrapper() { unregisterInterruptAction(); }
|
||||
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
bool supportsCarrierWave() const override { return true; }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
@@ -103,7 +106,7 @@ protected:
|
||||
return restoreSX126xPacketRamp(static_cast<CustomSX1262*>(_radio)) == RADIOLIB_ERR_NONE
|
||||
&& RadioLibWrapper::restoreCarrierWaveModem();
|
||||
}
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
uint8_t sync = 0x12;
|
||||
if (protocol == mesh::RadioProfileProtocol::Meshtext) {
|
||||
@@ -113,11 +116,15 @@ protected:
|
||||
} else if (protocol == mesh::RadioProfileProtocol::Ethernet) {
|
||||
if (!supportsEthernetTunnel()) return false;
|
||||
sync = 0x16;
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
} else if (protocol == mesh::RadioProfileProtocol::Kiss) {
|
||||
if (!supportsHostKiss()) return false;
|
||||
#endif
|
||||
} else if (protocol != mesh::RadioProfileProtocol::MeshCore) return false;
|
||||
// No alternate wire implementation means no modem write is needed.
|
||||
// Ethernet-only builds must still restore the MeshCore syncword.
|
||||
#if !MESH_ENABLE_ETHERNET_WIRE
|
||||
#if !MESH_ENABLE_ETHERNET_WIRE && !MESH_ENABLE_HOST_KISS
|
||||
if (!supportsMeshtext()) return protocol == mesh::RadioProfileProtocol::MeshCore;
|
||||
#endif
|
||||
auto* radio = static_cast<CustomSX1262*>(_radio);
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
bool supportsMeshtext() const override { return true; }
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
@@ -30,10 +33,14 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
if (protocol != mesh::RadioProfileProtocol::MeshCore
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext) return false;
|
||||
&& protocol != mesh::RadioProfileProtocol::Meshtext
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
&& protocol != mesh::RadioProfileProtocol::Kiss
|
||||
#endif
|
||||
) return false;
|
||||
const uint8_t sync = protocol == mesh::RadioProfileProtocol::Meshtext ? 0x2b : 0x12;
|
||||
return static_cast<CustomSX1268*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ class CustomSX1276Wrapper : public RadioLibWrapper {
|
||||
public:
|
||||
~CustomSX1276Wrapper() { unregisterInterruptAction(); }
|
||||
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
bool supportsHostKiss() const override { return true; }
|
||||
#endif
|
||||
bool supportsCarrierWave() const override { return supportsRadioDeepInit(); }
|
||||
bool supportsMorseRx() const override { return MESH_ENABLE_MORSE_RX && supportsRadioDeepInit(); }
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
@@ -26,7 +29,7 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
bool applyProtocol(mesh::RadioProfileProtocol protocol) override {
|
||||
uint8_t sync = 0x12;
|
||||
if (protocol == mesh::RadioProfileProtocol::Meshtext) {
|
||||
@@ -36,6 +39,10 @@ protected:
|
||||
} else if (protocol == mesh::RadioProfileProtocol::Ethernet) {
|
||||
if (!supportsEthernetTunnel()) return false;
|
||||
sync = 0x16;
|
||||
#endif
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
} else if (protocol == mesh::RadioProfileProtocol::Kiss) {
|
||||
if (!supportsHostKiss()) return false;
|
||||
#endif
|
||||
} else if (protocol != mesh::RadioProfileProtocol::MeshCore) return false;
|
||||
return static_cast<CustomSX1276*>(_radio)->setProfileProtocol(sync) == RADIOLIB_ERR_NONE;
|
||||
|
||||
@@ -615,7 +615,7 @@ bool RadioLibWrapper::restoreAfterDeepInit() {
|
||||
restored = _radio->setPreambleLength(currentPreambleLength())
|
||||
== RADIOLIB_ERR_NONE;
|
||||
}
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
restored = restored && applyProtocol(_physical_protocol);
|
||||
#endif
|
||||
if (_dbm_valid) {
|
||||
@@ -647,7 +647,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
|
||||
const uint8_t previous_sf = _cur_sf;
|
||||
const uint8_t previous_cr = _cur_cr;
|
||||
const uint16_t previous_preamble = _physical_preamble;
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
const auto previous_protocol = _physical_protocol;
|
||||
#endif
|
||||
const uint8_t previous_profile = _active_profile;
|
||||
@@ -664,7 +664,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
|
||||
_physical_preamble = profilePreamble(0);
|
||||
|
||||
bool success = applyParams(freq, bw, sf, cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
success = success && applyProtocol(requested.protocol);
|
||||
if (success) _physical_protocol = requested.protocol;
|
||||
#endif
|
||||
@@ -701,7 +701,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
|
||||
_profile_generation = previous_generation;
|
||||
_profile_refresh_required = previous_refresh;
|
||||
_physical_preamble = previous_preamble;
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
_physical_protocol = previous_protocol;
|
||||
#endif
|
||||
_rx_ps_enabled = previous_rxps;
|
||||
@@ -712,7 +712,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
|
||||
bool restored = had_previous_params
|
||||
&& _radio->standby() == RADIOLIB_ERR_NONE
|
||||
&& applyParams(previous_freq, previous_bw, previous_sf, previous_cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
restored = restored && applyProtocol(previous_protocol);
|
||||
#endif
|
||||
|
||||
@@ -745,6 +745,11 @@ bool RadioLibWrapper::validateProfile(const mesh::RadioProfileParams& p) const {
|
||||
if (!mesh::RadioProfiles::valid(p)) return false;
|
||||
#endif
|
||||
if (p.protocol == mesh::RadioProfileProtocol::Meshtext && !supportsMeshtext()) return false;
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (p.protocol == mesh::RadioProfileProtocol::Kiss && !supportsHostKiss()) return false;
|
||||
#else
|
||||
if (p.protocol == mesh::RadioProfileProtocol::Kiss) return false;
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_WIRE
|
||||
if (p.protocol == mesh::RadioProfileProtocol::Ethernet && !supportsEthernetTunnel()) return false;
|
||||
#else
|
||||
@@ -846,12 +851,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
|
||||
const bool old_refresh = _profile_refresh_required;
|
||||
const bool old_params_valid = _params_valid;
|
||||
const uint16_t old_preamble = _physical_preamble;
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
const auto old_protocol = _physical_protocol;
|
||||
#endif
|
||||
_physical_preamble = preamble;
|
||||
bool applied = applyParams(p.freq, p.bw, p.sf, p.cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
// Changing only the protocol still changes the sync word. Conversely a
|
||||
// failed write must not publish the new generation or cached wire format.
|
||||
if (applied && (_physical_protocol != p.protocol || _profile_refresh_required)) {
|
||||
@@ -903,12 +908,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
|
||||
_profile_generation = old_generation;
|
||||
_profile_refresh_required = old_refresh;
|
||||
_physical_preamble = old_preamble;
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
_physical_protocol = old_protocol;
|
||||
#endif
|
||||
bool restored = old_params_valid && _radio->standby() == RADIOLIB_ERR_NONE
|
||||
&& applyParams(old_freq, old_bw, old_sf, old_cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
restored = restored && applyProtocol(old_protocol);
|
||||
#endif
|
||||
if (!restored) restored = restoreAfterDeepInit();
|
||||
@@ -1016,7 +1021,7 @@ void RadioLibWrapper::cacheBeaconRestoreParams() {
|
||||
_active_profile = profile;
|
||||
_profile_generation = _profiles.generation[profile];
|
||||
_physical_preamble = profilePreamble(profile);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
_physical_protocol = p.protocol;
|
||||
#endif
|
||||
_profile_refresh_required = true;
|
||||
@@ -1051,7 +1056,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit(
|
||||
? 16 : (p.preamble ? p.preamble : 32);
|
||||
bool applied = _radio->standby() == RADIOLIB_ERR_NONE
|
||||
&& applyParams(p.freq, p.bw, p.sf, p.cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
applied = applied && applyProtocol(p.protocol);
|
||||
if (applied) _physical_protocol = p.protocol;
|
||||
#endif
|
||||
@@ -1094,7 +1099,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::restoreBeaconReceive() {
|
||||
&& _radio->standby() == RADIOLIB_ERR_NONE;
|
||||
beginProfileRetune(false);
|
||||
restored = restored && applyParams(p.freq, p.bw, p.sf, p.cr);
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
restored = restored && applyProtocol(p.protocol);
|
||||
#endif
|
||||
restored = _dbm_applied = restored && applyCachedTxPower(_cur_dbm) == RADIOLIB_ERR_NONE;
|
||||
@@ -2954,7 +2959,7 @@ bool RadioLibWrapper::hilPacketTransmitAllowed() const {
|
||||
return mesh::hilTransmitFrequencyAllowed(_cur_freq)
|
||||
&& _cur_freq == p.freq && _cur_bw == p.bw && _cur_sf == p.sf && _cur_cr == p.cr
|
||||
&& _physical_preamble == profilePreamble(_active_profile)
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
&& _physical_protocol == p.protocol
|
||||
#endif
|
||||
;
|
||||
|
||||
@@ -116,7 +116,7 @@ protected:
|
||||
mesh::RadioProfiles _profiles;
|
||||
uint8_t _active_profile = 0;
|
||||
uint16_t _physical_preamble = 0;
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE || MESH_ENABLE_ETHERNET_WIRE || MESH_ENABLE_HOST_KISS
|
||||
mesh::RadioProfileProtocol _physical_protocol = mesh::RadioProfileProtocol::MeshCore;
|
||||
// Wire settings are a transaction with the modulation tuple. Unsupported
|
||||
// wrappers decline rather than claiming a profile was applied.
|
||||
|
||||
@@ -174,7 +174,8 @@ class CompanionPacketStreamTests(unittest.TestCase):
|
||||
functions = usb[start:end] + '\n' + '\n'.join(method(usb, sig) for sig in (
|
||||
'bool hasDedicatedUsbLoggingPort()', 'bool isDedicatedUsbLoggingPortConfigured()',
|
||||
'bool usbLoggingInterfaceRestartRequired()', 'const char* usbLoggingPortDescription()',
|
||||
'void beginUsbLoggingPort()', 'Stream& usbLoggingPort()'))
|
||||
'void beginUsbLoggingPort()', 'static Stream& rawUsbLoggingPort()',
|
||||
'Stream& usbLoggingPort()'))
|
||||
start = mesh.index(' if (strcmp(command, "get usb.logging") == 0)')
|
||||
end = mesh.index('\n#endif', start)
|
||||
boot_start = mesh.index(' const bool usb_logging_enabled = _prefs.usb_logging_enabled != 0;')
|
||||
|
||||
@@ -21,6 +21,8 @@ def function(signature):
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#define MESH_ENABLE_HOST_KISS 0
|
||||
#include "helpers/HostKiss.h"
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
|
||||
@@ -16,6 +16,8 @@ CONTEXT = (ROOT / "src/helpers/ota/OtaContext.h").read_text()
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#define MESH_ENABLE_HOST_KISS 0
|
||||
#include "helpers/HostKiss.h"
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -9,6 +9,8 @@ from test_replay_reset_integration import extract_braced
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#define MESH_ENABLE_HOST_KISS 0
|
||||
#include "helpers/HostKiss.h"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -5,6 +5,8 @@ from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROLES = ("simple_repeater", "simple_room_server")
|
||||
@@ -19,7 +21,7 @@ class Esp32TinyUsbRoleHygieneTest(unittest.TestCase):
|
||||
text = source("src/helpers/UsbLogging.cpp")
|
||||
self.assertRegex(
|
||||
text,
|
||||
r"Stream& usbConsolePort\(\)\s*\{\s*"
|
||||
r"static Stream& rawUsbConsolePort\(\)\s*\{\s*"
|
||||
r"#if MESH_ESP32_TINYUSB_NONBLOCKING\s+"
|
||||
r"return buffered_esp32_tinyusb_terminal_port;\s+"
|
||||
r"#elif MESH_ESP32_HWCDC_SESSION_GUARD\s+"
|
||||
@@ -28,6 +30,11 @@ class Esp32TinyUsbRoleHygieneTest(unittest.TestCase):
|
||||
r"return buffered_primary_usb_terminal_port;\s+"
|
||||
r"#else\s+return Serial;\s+#endif",
|
||||
)
|
||||
# The shared binary-owner facade wraps the same transport selection.
|
||||
# With host KISS omitted, every platform still gets its original port.
|
||||
facade = body(text, "Stream& usbConsolePort()")
|
||||
self.assertIn("guarded.use(rawUsbConsolePort());", facade)
|
||||
self.assertRegex(facade, r"#else\s+return rawUsbConsolePort\(\);\s+#endif")
|
||||
|
||||
def test_role_output_never_bypasses_the_usb_facade(self):
|
||||
# A capacity check outside Serial.write() cannot make Arduino's
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run production KISS framing, persistence and session logic on the host."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
CODEC = r'''
|
||||
#include <helpers/HostKissCodec.h>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
using mesh::hostkiss::Codec;
|
||||
std::vector<uint8_t> output(Codec& codec) {
|
||||
const uint8_t* bytes;size_t length;std::vector<uint8_t> result;
|
||||
while(codec.peekOutput(bytes,length)) {
|
||||
const size_t amount=length>7?7:length;
|
||||
result.insert(result.end(),bytes,bytes+amount);codec.consumeOutput(amount);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void feed(Codec& codec,const std::vector<uint8_t>& bytes) {
|
||||
for(auto byte:bytes) assert(codec.feed(byte));
|
||||
}
|
||||
int main() {
|
||||
Codec encoder,decoder;
|
||||
for(unsigned length=1;length<=255;++length) {
|
||||
std::vector<uint8_t> bytes(length);
|
||||
for(unsigned i=0;i<length;++i) bytes[i]=uint8_t(i+length);
|
||||
assert(encoder.queueReceive(bytes.data(),bytes.size()));
|
||||
feed(decoder,output(encoder));
|
||||
const uint8_t* decoded;size_t size;
|
||||
assert(decoder.peekTransmit(decoded,size)&&size==bytes.size());
|
||||
assert(std::vector<uint8_t>(decoded,decoded+size)==bytes);
|
||||
decoder.consumeTransmit();
|
||||
}
|
||||
const uint8_t escaped[]={0xc0,0xdb,0xc0};
|
||||
assert(encoder.queueReceive(escaped,3));
|
||||
assert(output(encoder)==std::vector<uint8_t>({0xc0,0,0xdb,0xdc,0xdb,0xdd,0xdb,0xdc,0xc0}));
|
||||
// Oversized, malformed escape, empty DATA, unknown command/port and RETURN
|
||||
// with payload cannot be transformed into a valid radio packet.
|
||||
for(auto bytes:std::vector<std::vector<uint8_t>>{{0xc0,0,0xc0},
|
||||
{0xc0,0,1,0xdb,0xc0},{0xc0,0,1,0xdb,0x11,0xc0},
|
||||
{0xc0,0x10,1,2,0xc0},{0xc0,1,99,0xc0},{0xc0,0xff,1,0xc0}}) {
|
||||
decoder.reset();feed(decoder,bytes);
|
||||
const uint8_t* data;size_t size;assert(!decoder.peekTransmit(data,size)&&!decoder.returned());
|
||||
}
|
||||
std::vector<uint8_t> oversized(260,1);oversized[0]=0xc0;oversized[1]=0;oversized.back()=0xc0;
|
||||
decoder.reset();feed(decoder,oversized);
|
||||
const uint8_t* data;size_t size;assert(!decoder.peekTransmit(data,size));
|
||||
assert(decoder.malformedFrames()>=5);
|
||||
feed(decoder,{0xc0,0,17,0xc0,0,18,0xc0});
|
||||
assert(decoder.queuedTransmit()==2&&!decoder.canAcceptByte()&&!decoder.feed(0xc0));
|
||||
decoder.consumeTransmit();assert(decoder.canAcceptByte());
|
||||
assert(decoder.peekTransmit(data,size)&&size==1&&data[0]==18);
|
||||
decoder.reset();feed(decoder,{1,2,3,0xc0,0xff,0xc0});
|
||||
assert(decoder.returned()&&!decoder.canAcceptByte());decoder.reset();
|
||||
assert(!decoder.returned()&&decoder.canAcceptByte());
|
||||
assert(encoder.queueReady()&&encoder.queueTxDone(false));
|
||||
assert(!encoder.queueReceive(escaped,3)&&encoder.droppedFrames()==1);
|
||||
assert(output(encoder)==std::vector<uint8_t>({0xc0,0x0f,1,0xc0,0xc0,0x06,0xf8,0,0xc0}));
|
||||
assert(!encoder.queueReceive(nullptr,1)&&!encoder.queueReceive(escaped,0));
|
||||
// Fuzz framing under sanitizers with periodic draining/backpressure.
|
||||
uint32_t state=17;
|
||||
for(unsigned i=0;i<200000;++i) {
|
||||
state=state*1664525+1013904223;
|
||||
if(!decoder.canAcceptByte()) decoder.reset();
|
||||
decoder.feed(uint8_t(state>>24));
|
||||
if(i%317==0) decoder.reset();
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
SETTINGS = r'''
|
||||
#include <helpers/HostKissSettings.h>
|
||||
#include <helpers/PersistentStoreFormat.h>
|
||||
#include <cassert>
|
||||
using namespace mesh::hostkiss;
|
||||
using Bytes=std::vector<uint8_t>;
|
||||
constexpr char Path[]="/host_kiss";
|
||||
Bytes image(Selection selection) {
|
||||
Bytes bytes={'H','K','S','1',uint8_t(selection.enabled),uint8_t(selection.transport),0,0,0,0,0,0};
|
||||
mesh::storage::writeLE32(bytes.data()+8,mesh::storage::updateCRC32(0xffffffff,bytes.data(),8));return bytes;
|
||||
}
|
||||
int main() {
|
||||
Selection on{true,Transport::Usb},tcp{true,Transport::Tcp},off{false,Transport::Usb};
|
||||
GuestMemoryFS fs;Settings store;
|
||||
assert(store.begin(&fs)&&store.healthy()&&store.selection()==off&&fs.writes==0);
|
||||
assert(store.save(on)&&store.selection()==on&&fs.files.at(Path)==image(on));
|
||||
assert(store.save(tcp)&&store.selection()==tcp&&fs.files.at(Path)==image(tcp));
|
||||
Settings reboot;assert(reboot.begin(&fs)&&reboot.selection()==tcp);
|
||||
assert(reboot.save(off)&&reboot.selection()==off);
|
||||
for(unsigned size=0;size<25;++size) {
|
||||
if(size==12)continue;
|
||||
GuestMemoryFS broken;broken.files[Path]=image(on);broken.files[Path].resize(size);
|
||||
Settings read;assert(!read.begin(&broken)&&read.selection()==off);
|
||||
assert(read.save(off));
|
||||
}
|
||||
for(unsigned byte=0;byte<12;++byte) for(unsigned bit=0;bit<8;++bit) {
|
||||
GuestMemoryFS broken;broken.files[Path]=image(tcp);broken.files[Path][byte]^=1<<bit;
|
||||
Settings read;assert(!read.begin(&broken)&&read.selection()==off);
|
||||
}
|
||||
for(unsigned fault=0;fault<7;++fault) {
|
||||
GuestMemoryFS broken;broken.files[Path]=image(on);Settings read;assert(read.begin(&broken));
|
||||
if(fault==0)broken.fail_open_write=true;
|
||||
if(fault==1)broken.short_write=5;
|
||||
if(fault==2)broken.corrupt_stage=true;
|
||||
if(fault==3)broken.fail_stage_open=true;
|
||||
if(fault==4)broken.fail_stage_read=true;
|
||||
if(fault==5)broken.fail_publish=true;
|
||||
if(fault==6)broken.false_publish=true;
|
||||
assert(!read.save(off)&&read.selection()==on);
|
||||
assert(broken.files.at(Path)==image(on));
|
||||
}
|
||||
for(auto old:{off,on,tcp}) {
|
||||
GuestMemoryFS backing;backing.files[Path]=image(old);Settings read;assert(read.begin(&backing));
|
||||
backing.capture=true;assert(read.save(old==off?tcp:off));
|
||||
const auto snapshots=backing.snapshots;assert(snapshots.size()>10);
|
||||
for(const auto& snapshot:snapshots) {
|
||||
GuestMemoryFS resumed;resumed.files=snapshot;Settings recovered;assert(recovered.begin(&resumed));
|
||||
if(snapshot.count(Path)) assert(image(recovered.selection())==snapshot.at(Path));
|
||||
else if(snapshot.count("/host_kiss.bak")) assert(image(recovered.selection())==snapshot.at("/host_kiss.bak"));
|
||||
else assert(recovered.selection()==off);
|
||||
}
|
||||
}
|
||||
GuestMemoryFS scratch;scratch.files["/host_kiss.tmp"]=image(tcp);
|
||||
Settings discarded;assert(discarded.begin(&scratch)&&discarded.selection()==off);
|
||||
}
|
||||
'''
|
||||
|
||||
RADIO_STUB = r'''
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <vector>
|
||||
namespace mesh {
|
||||
enum class RadioProfileProtocol:uint8_t {MeshCore=0,Meshtext=1,Ethernet=2,Kiss=3};
|
||||
enum class RadioProfileMode:uint8_t {Off=0,Rx=1,RxTx=2};
|
||||
struct RadioProfileParams {RadioProfileProtocol protocol=RadioProfileProtocol::MeshCore;};
|
||||
struct RadioProfileConfig {RadioProfileParams params;RadioProfileMode mode=RadioProfileMode::RxTx;};
|
||||
struct RadioProfiles {RadioProfileConfig secondary;uint32_t generation[2]={1,1};bool primary_temporary=false,secondary_temporary=false;};
|
||||
class Radio {public:RadioProfiles p;bool supported=true;bool supportsHostKiss()const{return supported;}RadioProfiles* profiles(){return &p;}};
|
||||
class Dispatcher {public:
|
||||
bool pending=false,allow=true;unsigned submitted=0,cancelled=0;uint32_t gen=0;std::vector<uint8_t> packet;
|
||||
bool submitHostKissFrame(const uint8_t* bytes,size_t size,uint32_t generation){
|
||||
if(pending||!allow)return false;pending=true;++submitted;gen=generation;packet.assign(bytes,bytes+size);return true;}
|
||||
bool cancelHostKissFrame(){++cancelled;if(!pending)return false;pending=false;return true;}
|
||||
bool hostKissFramePending()const{return pending;}
|
||||
};
|
||||
}
|
||||
'''
|
||||
PROFILE_STUB = r'''
|
||||
#pragma once
|
||||
#include <Dispatcher.h>
|
||||
namespace mesh {class RadioProfileCLI {
|
||||
public:Radio* radio;RadioProfileConfig saved;bool manual=true,busy=false,overlay=false;uint32_t intent=0;unsigned clear=0;
|
||||
explicit RadioProfileCLI(Radio& value):radio(&value){}
|
||||
bool manualSecondaryConfigured()const{return manual;}
|
||||
const RadioProfileConfig& savedSecondaryConfig()const{return saved;}
|
||||
bool secondaryRuntimeAvailable()const{return !busy;}
|
||||
uint32_t secondaryIntentGeneration()const{return intent;}
|
||||
bool useSecondaryProtocol(RadioProfileProtocol protocol){
|
||||
if(busy||!manual)return false;radio->p.secondary.params.protocol=protocol;++radio->p.generation[1];overlay=true;return true;}
|
||||
void clearSecondaryProtocol(){++clear;if(overlay){radio->p.secondary=saved;++radio->p.generation[1];overlay=false;}}
|
||||
};}
|
||||
'''
|
||||
USB_STUB = r'''
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
struct Stream {
|
||||
std::deque<uint8_t> input;std::vector<uint8_t> output;size_t write_budget=128;
|
||||
int available(){return int(input.size());}int peek(){return input.empty()?-1:input.front();}
|
||||
int read(){if(input.empty())return -1;int byte=input.front();input.pop_front();return byte;}
|
||||
size_t write(const uint8_t* bytes,size_t length){if(length>write_budget)length=write_budget;output.insert(output.end(),bytes,bytes+length);return length;}
|
||||
};
|
||||
inline Stream serial;
|
||||
inline uint32_t fake_millis=0;
|
||||
inline uint32_t millis(){return fake_millis;}
|
||||
namespace mesh {
|
||||
inline bool binary_owned=false;
|
||||
inline bool binary_ready=true;
|
||||
inline Stream& usbCompanionPort(){return serial;}
|
||||
inline void setUsbBinaryStreamOwned(bool value){binary_owned=value;}
|
||||
inline bool isUsbBinaryStreamReady(){return binary_ready;}
|
||||
}
|
||||
'''
|
||||
|
||||
NETWORK_STUB = r'''
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <cerrno>
|
||||
#define WIFI_STA 1
|
||||
#define WIFI_AP 2
|
||||
#define WL_CONNECTED 3
|
||||
#define MSG_DONTWAIT 0x40
|
||||
struct Socket {bool connected=true;std::deque<uint8_t> input;std::vector<uint8_t> output;size_t write_budget=128;};
|
||||
inline std::map<int,std::shared_ptr<Socket>> sockets;
|
||||
inline std::deque<int> accepts;
|
||||
inline int next_socket=1;
|
||||
inline int newSocket(){int id=next_socket++;sockets[id]=std::make_shared<Socket>();return id;}
|
||||
class IPAddress {uint32_t value_;public:IPAddress(uint32_t value=0):value_(value){}operator uint32_t()const{return value_;}};
|
||||
struct Wifi {int mode=0,status_value=0;uint32_t address=0;
|
||||
int getMode()const{return mode;}int status()const{return status_value;}IPAddress localIP()const{return IPAddress(address);}};
|
||||
inline Wifi WiFi;
|
||||
class WiFiClient {int id_=0;public:
|
||||
WiFiClient()=default;explicit WiFiClient(int id):id_(id){}
|
||||
explicit operator bool()const{return id_&&sockets.at(id_)->connected;}
|
||||
bool connected()const{return bool(*this);}int fd()const{return id_;}
|
||||
void stop(){if(id_)sockets.at(id_)->connected=false;id_=0;}void setNoDelay(bool){}
|
||||
};
|
||||
inline uint32_t bound_address=0;inline uint16_t bound_port=0;inline unsigned listens=0;
|
||||
class WiFiServer {uint32_t address_=0;uint16_t port_=0;bool started_=false;
|
||||
public:WiFiServer(uint16_t port,int):port_(port){}WiFiServer(IPAddress address,uint16_t port,int):address_(address),port_(port){}
|
||||
void begin(){started_=true;bound_address=address_;bound_port=port_;++listens;}
|
||||
void end(){started_=false;while(!accepts.empty()){sockets.at(accepts.front())->connected=false;accepts.pop_front();}}
|
||||
explicit operator bool()const{return started_;}
|
||||
WiFiClient available(){if(accepts.empty())return {};int id=accepts.front();accepts.pop_front();return WiFiClient(id);}
|
||||
};
|
||||
inline int send(int fd,const void* data,size_t length,int flags){
|
||||
if(flags!=MSG_DONTWAIT)__builtin_trap();auto sock=sockets.at(fd);
|
||||
if(!sock->connected){errno=EPIPE;return -1;}if(length>sock->write_budget)length=sock->write_budget;
|
||||
const auto* bytes=static_cast<const uint8_t*>(data);sock->output.insert(sock->output.end(),bytes,bytes+length);return int(length);
|
||||
}
|
||||
inline int recv(int fd,void* data,size_t length,int flags){
|
||||
if(flags!=MSG_DONTWAIT||length!=1)__builtin_trap();auto sock=sockets.at(fd);
|
||||
if(!sock->connected)return 0;if(sock->input.empty()){errno=EAGAIN;return -1;}
|
||||
*static_cast<uint8_t*>(data)=sock->input.front();sock->input.pop_front();return 1;
|
||||
}
|
||||
'''
|
||||
|
||||
TCP_RUNTIME = r'''
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <Dispatcher.h>
|
||||
#include <RadioProfileCLI.h>
|
||||
#include <UsbLogging.h>
|
||||
#include <WiFi.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
using namespace mesh;
|
||||
char reply[240];
|
||||
void command(const char* text){assert(hostkiss::handleCommand(text,reply,sizeof(reply),true));}
|
||||
void input(int socket,std::initializer_list<uint8_t> bytes){auto& q=sockets.at(socket)->input;q.insert(q.end(),bytes);}
|
||||
int main(){
|
||||
GuestMemoryFS fs;Radio radio;RadioProfileCLI profiles(radio);Dispatcher dispatcher;
|
||||
hostkiss::begin(&fs,radio,profiles,dispatcher);
|
||||
command("set kiss.transport tcp");assert(strstr(reply,"OK"));
|
||||
command("set kiss on");assert(strstr(reply,"OK"));
|
||||
WiFi.mode=WIFI_AP;WiFi.status_value=WL_CONNECTED;WiFi.address=123;
|
||||
hostkiss::loop();assert(listens==0);
|
||||
command("get kiss");assert(strstr(reply,"waiting-wifi"));
|
||||
WiFi.mode=WIFI_STA|WIFI_AP;hostkiss::loop();assert(listens==1&&bound_address==123&&bound_port==5004);
|
||||
command("get kiss");assert(strstr(reply,"listening"));
|
||||
const int first=newSocket();accepts.push_back(first);hostkiss::loop();
|
||||
assert(hostkiss::radioActive()&&!hostkiss::usbActive()&&!binary_owned);
|
||||
assert(sockets.at(first)->output==std::vector<uint8_t>({0xc0,0x0f,1,0xc0}));
|
||||
const int rejected=newSocket();accepts.push_back(rejected);hostkiss::loop();
|
||||
assert(!sockets.at(rejected)->connected&&sockets.at(first)->connected);
|
||||
input(first,{0xc0,0,1,2,3,0xc0});hostkiss::loop();
|
||||
assert(dispatcher.submitted==1&&dispatcher.packet==std::vector<uint8_t>({1,2,3}));
|
||||
dispatcher.pending=false;hostkiss::sendComplete(true);hostkiss::loop();hostkiss::loop();
|
||||
assert(sockets.at(first)->output==std::vector<uint8_t>({0xc0,0x0f,1,0xc0,0xc0,6,0xf8,1,0xc0,0xc0,0x0f,1,0xc0}));
|
||||
// Backpressure preserves every escaped output byte across partial sends.
|
||||
sockets.at(first)->output.clear();sockets.at(first)->write_budget=3;
|
||||
uint8_t packet[255];memset(packet,0xc0,sizeof(packet));
|
||||
hostkiss::receive(packet,sizeof(packet),1,radio.p.generation[1],0,0);
|
||||
for(unsigned i=0;i<200;++i)hostkiss::loop();
|
||||
const auto& bytes=sockets.at(first)->output;
|
||||
assert(bytes.size()==513&&bytes[0]==0xc0&&bytes[1]==0&&bytes.back()==0xc0);
|
||||
for(unsigned i=2;i<512;i+=2)assert(bytes[i]==0xdb&&bytes[i+1]==0xdc);
|
||||
// The profile change drops both the active stream and pending accepts.
|
||||
const int stale=newSocket();accepts.push_back(stale);input(stale,{0xc0,0,99,0xc0});
|
||||
++radio.p.generation[1];hostkiss::loop();
|
||||
assert(!sockets.at(first)->connected&&!sockets.at(stale)->connected&&!dispatcher.pending);
|
||||
assert(dispatcher.submitted==1&&!hostkiss::radioActive());
|
||||
// A new explicit runtime profile can start a fresh stream; old RX cannot.
|
||||
profiles.overlay=false;radio.p.secondary=profiles.saved;++radio.p.generation[1];
|
||||
const int second=newSocket();accepts.push_back(second);hostkiss::loop();
|
||||
assert(hostkiss::radioActive()&&sockets.at(second)->connected);
|
||||
const unsigned before=dispatcher.submitted;
|
||||
input(second,{0xc0,0xff,0xc0});hostkiss::loop();
|
||||
assert(!sockets.at(second)->connected&&!hostkiss::radioActive()&&dispatcher.submitted==before);
|
||||
// Losing station WiFi tears down the raw radio lease and listener.
|
||||
const int third=newSocket();accepts.push_back(third);hostkiss::loop();assert(hostkiss::radioActive());
|
||||
WiFi.mode=WIFI_AP;hostkiss::loop();assert(!hostkiss::radioActive()&&!sockets.at(third)->connected);
|
||||
command("set kiss off");assert(strstr(reply,"OK"));
|
||||
}
|
||||
'''
|
||||
|
||||
RUNTIME = r'''
|
||||
#include <helpers/HostKiss.h>
|
||||
#include <helpers/HostKissSettings.h>
|
||||
#include <Dispatcher.h>
|
||||
#include <RadioProfileCLI.h>
|
||||
#include <UsbLogging.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
using namespace mesh;
|
||||
void input(std::initializer_list<uint8_t> bytes){serial.input.insert(serial.input.end(),bytes);}
|
||||
void poll(){hostkiss::loop();hostkiss::serviceUsb();}
|
||||
char reply[240];
|
||||
void command(const char* text,bool local=true){assert(hostkiss::handleCommand(text,reply,sizeof(reply),local));}
|
||||
int main() {
|
||||
GuestMemoryFS fs;Radio radio;RadioProfileCLI profiles(radio);Dispatcher dispatcher;
|
||||
hostkiss::begin(&fs,radio,profiles,dispatcher);
|
||||
command("get kiss");assert(strstr(reply,"> off"));
|
||||
input({0xc0,0,1,0xc0});assert(!hostkiss::serviceUsb()&&serial.available()==4);
|
||||
serial.input.clear();
|
||||
command("set kiss on",false);assert(strstr(reply,"local connection"));
|
||||
command("set kiss on");assert(strstr(reply,"OK"));
|
||||
hostkiss::Settings saved;assert(saved.begin(&fs)&&saved.selection().enabled);
|
||||
input({'g','e','t',' '});assert(!hostkiss::serviceUsb()&&serial.available()==4);
|
||||
serial.input.clear();
|
||||
profiles.manual=false;input({0xc0});assert(hostkiss::serviceUsb()&&hostkiss::usbActive()&&binary_owned);
|
||||
assert(!hostkiss::radioActive());input({0xff,0xc0});assert(hostkiss::serviceUsb()&&!hostkiss::usbActive());
|
||||
profiles.manual=true;input({0xc0});binary_ready=false;
|
||||
const size_t before_ready=serial.output.size();
|
||||
assert(hostkiss::serviceUsb()&&hostkiss::usbActive()&&binary_owned);
|
||||
assert(serial.available()==1&&serial.output.size()==before_ready);
|
||||
binary_ready=true;assert(hostkiss::serviceUsb()&&serial.available()==0);
|
||||
assert(hostkiss::radioActive()&&radio.p.secondary.params.protocol==RadioProfileProtocol::Kiss);
|
||||
assert(profiles.saved.params.protocol==RadioProfileProtocol::MeshCore);
|
||||
input({0,7,0xdb,0xdc,0xdb,0xdd,0xc0});poll();
|
||||
assert(dispatcher.submitted==1&&dispatcher.packet==std::vector<uint8_t>({7,0xc0,0xdb}));
|
||||
input({0,8,0xc0});poll();assert(dispatcher.submitted==1);
|
||||
dispatcher.pending=false;hostkiss::sendComplete(true);poll();assert(dispatcher.submitted==2);
|
||||
// Cross-profile or old-generation RX can never leak into the host stream.
|
||||
poll();poll();
|
||||
serial.output.clear();uint8_t rx[]={9};
|
||||
hostkiss::receive(rx,1,0,radio.p.generation[1],0,0);
|
||||
hostkiss::receive(rx,1,1,radio.p.generation[1]-1,0,0);poll();
|
||||
assert(serial.output.empty());
|
||||
hostkiss::receive(rx,1,1,radio.p.generation[1],0,0);poll();
|
||||
assert(serial.output==std::vector<uint8_t>({0xc0,0,9,0xc0}));
|
||||
// A failed settings write preserves the active session and lease.
|
||||
fs.fail_open_write=true;command("set kiss off");
|
||||
assert(strstr(reply,"save failed")&&hostkiss::usbActive()&&hostkiss::radioActive());
|
||||
fs.fail_open_write=false;
|
||||
// Profile invalidation cancels stale TX but quarantines USB binary input.
|
||||
radio.p.secondary.params.protocol=RadioProfileProtocol::Meshtext;
|
||||
++radio.p.generation[1];const unsigned cleared=profiles.clear;
|
||||
poll();assert(hostkiss::usbActive()&&!hostkiss::radioActive());
|
||||
assert(profiles.clear==cleared&&radio.p.secondary.params.protocol==RadioProfileProtocol::Meshtext);
|
||||
const unsigned submitted=dispatcher.submitted;
|
||||
input({0xc0,0,99,0xc0});poll();assert(dispatcher.submitted==submitted);
|
||||
input({0xc0,0xff,0xc0});assert(hostkiss::serviceUsb());
|
||||
assert(!hostkiss::usbActive()&&!binary_owned&&!dispatcher.pending);
|
||||
hostkiss::sendComplete(true); // late completion cannot enter a new session
|
||||
profiles.overlay=false;radio.p.secondary=profiles.saved;++radio.p.generation[1];
|
||||
input({0xc0,0,17,0xc0});poll();assert(dispatcher.submitted==submitted+1);
|
||||
hostkiss::resetUsbSession();assert(!hostkiss::usbActive()&&!binary_owned&&!dispatcher.pending);
|
||||
// Off must be durable, releases the overlay and cannot consume binary input.
|
||||
command("set kiss off");assert(strstr(reply,"OK")&&!hostkiss::radioActive());
|
||||
assert(saved.begin(&fs)&&!saved.selection().enabled);
|
||||
input({0xc0,0,8,0xc0});assert(!hostkiss::serviceUsb());serial.input.clear();
|
||||
hostkiss::begin(&fs,radio,profiles,dispatcher);command("get kiss");assert(strstr(reply,"> off"));
|
||||
command("set kiss.transport tcp");assert(strstr(reply,"requires ESP32"));
|
||||
assert(!hostkiss::handleCommand("get kissing",reply,sizeof(reply),true));
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class HostKissTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if not cls.compiler:
|
||||
raise unittest.SkipTest("host C++ compiler required")
|
||||
cls.work = tempfile.TemporaryDirectory(prefix="mesh-host-kiss-")
|
||||
cls.path = Path(cls.work.name)
|
||||
cls.mocks = cls.path / "mocks"
|
||||
shutil.copytree(ROOT / "test/fixtures/guest_login/mocks", cls.mocks)
|
||||
for path in cls.mocks.rglob("*"):
|
||||
if path.is_file():
|
||||
path.write_text(path.read_text().replace("/guest_login", "/host_kiss"), encoding="ascii")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.work.cleanup()
|
||||
|
||||
def build_run(self, name, source, backend=None, runtime=False):
|
||||
path = self.path / (name + ".cpp")
|
||||
path.write_text(source, encoding="ascii")
|
||||
binary = self.path / name
|
||||
flags = ["-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-misleading-indentation", "-Wno-sign-compare"]
|
||||
if os.name != "nt":
|
||||
flags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-no-pie"]
|
||||
sources = [str(path)]
|
||||
includes = ["-I", str(self.mocks), "-I", str(ROOT / "src")]
|
||||
if backend:
|
||||
flags += ["-DMESH_ENABLE_HOST_KISS=1", "-D" + backend + "_PLATFORM"]
|
||||
if backend == "ESP32":
|
||||
flags += ["-DESP32"]
|
||||
sources.append(str(ROOT / "src/helpers/HostKissSettings.cpp"))
|
||||
if backend in ("NRF52", "STM32"):
|
||||
sources.append(str(ROOT / "src/helpers/AtomicFileWriter.cpp"))
|
||||
if runtime:
|
||||
runtime_path = self.path / "HostKiss.cpp"
|
||||
shutil.copyfile(ROOT / "src/helpers/HostKiss.cpp", runtime_path)
|
||||
sources.append(str(runtime_path))
|
||||
includes += ["-I", str(ROOT / "src/helpers")]
|
||||
(self.mocks / "Dispatcher.h").write_text(RADIO_STUB, encoding="ascii")
|
||||
(self.mocks / "RadioProfileCLI.h").write_text(PROFILE_STUB, encoding="ascii")
|
||||
(self.mocks / "UsbLogging.h").write_text(USB_STUB, encoding="ascii")
|
||||
if backend == "ESP32":
|
||||
(self.mocks / "WiFi.h").write_text(NETWORK_STUB, encoding="ascii")
|
||||
(self.mocks / "lwip").mkdir(exist_ok=True)
|
||||
(self.mocks / "lwip/sockets.h").write_text('#include <WiFi.h>\n', encoding="ascii")
|
||||
result = subprocess.run([self.compiler, *flags, *includes, *sources, "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_framing_limits_escaping_flow_and_malformed_input(self):
|
||||
self.build_run("codec", CODEC)
|
||||
|
||||
def test_saved_settings_transaction_and_reboot_boundaries(self):
|
||||
for backend in ("ESP32", "RP2040", "NRF52", "STM32"):
|
||||
with self.subTest(backend=backend):
|
||||
self.build_run("settings_" + backend, SETTINGS, backend)
|
||||
|
||||
def test_usb_session_ownership_stale_frames_and_toggle(self):
|
||||
self.build_run("runtime", RUNTIME, "NRF52", runtime=True)
|
||||
|
||||
def test_tcp_station_binding_backpressure_sessions_and_toggle(self):
|
||||
self.build_run("tcp_runtime", TCP_RUNTIME, "ESP32", runtime=True)
|
||||
|
||||
def test_disabled_header_needs_no_platform_types(self):
|
||||
self.build_run("disabled", r'''
|
||||
#include <helpers/HostKiss.h>
|
||||
int main(){int a=0;mesh::hostkiss::begin(&a,a,a,a);
|
||||
return mesh::hostkiss::serviceUsb()||mesh::hostkiss::radioActive();}
|
||||
''')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise Full infrastructure console ownership without stopping RF/network service."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
FIXTURE = r'''
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
static std::deque<unsigned char> input;
|
||||
static bool kiss_active = false, reset_pending = false, network_pending = false;
|
||||
static unsigned reset_reads = 0, kiss_resets = 0, ascii_commands = 0;
|
||||
static unsigned board_loops = 0, ethernet_loops = 0, network_commands = 0;
|
||||
static unsigned serial_cancels = 0, terminal_drains = 0;
|
||||
struct Stream {
|
||||
int available() const { return int(input.size()); }
|
||||
int read() {
|
||||
assert(!input.empty() && input.front() != 0xc0);
|
||||
int value = input.front(); input.pop_front(); return value;
|
||||
}
|
||||
void print(char) { assert(!kiss_active); }
|
||||
void println(const char*) { assert(!kiss_active); }
|
||||
void printf(const char*, ...) { assert(!kiss_active); }
|
||||
} console;
|
||||
namespace mesh {
|
||||
void serviceUsbLoggingPort() {}
|
||||
void serviceUsbTerminalPort() { assert(!kiss_active); ++terminal_drains; }
|
||||
bool takeUsbTerminalSessionReset() { ++reset_reads; bool value=reset_pending; reset_pending=false; return value; }
|
||||
bool tryCompleteUsbTerminalSessionReset() { return true; }
|
||||
bool canAcceptUsbConsoleCommand() { return true; }
|
||||
Stream& usbConsolePort() { return console; }
|
||||
void noteUsbLoggingStatsCommand(const char*) {}
|
||||
namespace power { void serviceRegulatorSettings() {} }
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
namespace hostkiss {
|
||||
void resetUsbSession() { ++kiss_resets; kiss_active=false; }
|
||||
bool serviceUsb() {
|
||||
if (!input.empty() && input.front()==0xc0) {
|
||||
input.pop_front(); kiss_active=true;
|
||||
}
|
||||
bool owned=kiss_active;
|
||||
if (kiss_active) {
|
||||
while (!input.empty()) {
|
||||
unsigned char value=input.front(); input.pop_front();
|
||||
if (value==0xff) { kiss_active=false; break; }
|
||||
}
|
||||
}
|
||||
return owned;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
struct Mesh {
|
||||
bool hasPendingSerialOutput() { return false; }
|
||||
void cancelPendingSerialOutput() { ++serial_cancels; }
|
||||
void handleUsbCommand(const char* text, char* reply) {
|
||||
assert(!kiss_active && !strcmp(text,"ver")); ++ascii_commands; strcpy(reply,"version");
|
||||
}
|
||||
void cancelLocalOutput(Stream&) {}
|
||||
bool hasPendingLocalOutput() { return false; }
|
||||
void handleLocalCommand(const char* text, char* reply, Stream&) {
|
||||
assert(!strcmp(text,"get kiss")); ++network_commands; strcpy(reply,"status");
|
||||
}
|
||||
} the_mesh;
|
||||
struct Board { void loop() { ++board_loops; } } board;
|
||||
struct Ethernet : Stream { bool connected() { return true; } } ethernet_client;
|
||||
static char ethernet_command[160];
|
||||
void ethernet_loop_maintain() { ++ethernet_loops; }
|
||||
bool ethernet_take_session_reset() { return false; }
|
||||
bool ethernet_read_line(char* dest, size_t) {
|
||||
if (!network_pending) return false;
|
||||
network_pending=false; strcpy(dest,"get kiss"); return true;
|
||||
}
|
||||
bool ethernet_handle_command(const char*, char*) { return false; }
|
||||
void ethernet_send_reply(const char* text) { assert(!strcmp(text,"status")); }
|
||||
static constexpr size_t LOCAL_SERIAL_COMMAND_MAX=159;
|
||||
static char command[LOCAL_SERIAL_COMMAND_MAX+2] = {};
|
||||
static bool command_overflow=false;
|
||||
@PUMP@
|
||||
static void ascii() { for (unsigned char c : std::string("ver\r")) input.push_back(c); }
|
||||
int main() {
|
||||
ascii(); serviceCommandInterfaces();
|
||||
assert(ascii_commands==1 && board_loops==1 && ethernet_loops==1);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
strcpy(command,"old prefix"); command_overflow=true;
|
||||
input={0xc0,0x00,0x41,0x42}; network_pending=true;
|
||||
unsigned old_drains=terminal_drains;
|
||||
serviceCommandInterfaces();
|
||||
assert(kiss_active && input.empty());
|
||||
assert(command[0]==0 && !command_overflow && ascii_commands==1);
|
||||
assert(board_loops==2 && ethernet_loops==2 && network_commands==1);
|
||||
assert(terminal_drains==old_drains);
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
assert(serial_cancels>0);
|
||||
#endif
|
||||
// A binary session with no new USB bytes must keep ownership and service
|
||||
// unrelated network commands. The owner must not return out of the pump.
|
||||
network_pending=true; serviceCommandInterfaces();
|
||||
assert(kiss_active && board_loops==3 && ethernet_loops==3 && network_commands==2);
|
||||
assert(terminal_drains==old_drains);
|
||||
// EXIT returns ownership but its poll still bypasses the ASCII parser.
|
||||
input={0xff}; serviceCommandInterfaces();
|
||||
assert(!kiss_active && ascii_commands==1);
|
||||
ascii(); serviceCommandInterfaces(); assert(ascii_commands==2);
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
input={0xc0,0x00,0x43}; serviceCommandInterfaces(); assert(kiss_active);
|
||||
reset_pending=true;
|
||||
unsigned previous_resets=reset_reads;
|
||||
strcpy(command,"stale"); command_overflow=true;
|
||||
serviceCommandInterfaces();
|
||||
assert(!kiss_active && kiss_resets==1 && reset_reads==previous_resets+1);
|
||||
assert(command[0]==0 && !command_overflow);
|
||||
ascii(); serviceCommandInterfaces(); assert(ascii_commands==3);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class HostKissInfrastructureTest(unittest.TestCase):
|
||||
def test_release_builder_selects_only_full_supported_roles(self):
|
||||
builder = (ROOT / "build_legacy.sh").read_text()
|
||||
promotion_start = builder.index(' if [ "$ESP32_FULL_BUILD" = "1" ] \\\n || is_companion_radio_full_target "$env_name"; then')
|
||||
promotion_end = builder.index('\n fi', promotion_start) + len('\n fi')
|
||||
kiss_start = builder.index(' # Full LoRa images retain their normal role')
|
||||
kiss_end = builder.index(' declare_build_capability_contract ', kiss_start)
|
||||
policy = builder[promotion_start:promotion_end] + '\n' + builder[kiss_start:kiss_end]
|
||||
# Execute the release builder's actual promotion and feature-selection
|
||||
# blocks, using its target classifiers. PlatformIO must never run here.
|
||||
script = r'''
|
||||
source "$1/build_legacy.sh"
|
||||
pio() { printf 'Unexpected PlatformIO invocation\n' >&2; return 99; }
|
||||
apply_policy() {
|
||||
local env_name=$1 env_platform=$2 BUILD_PROFILE_FOR_TARGET=$3
|
||||
local NRF52_OTA_SENSOR_PROFILE=$4 ESP32_FULL_BUILD=$5
|
||||
local PLATFORMIO_BUILD_FLAGS='-DEXISTING_FLAG=1'
|
||||
PIO_ENV_PLATFORM_BY_NAME[$env_name]=$env_platform
|
||||
@POLICY@
|
||||
case "$PLATFORMIO_BUILD_FLAGS" in
|
||||
*-DMESH_ENABLE_HOST_KISS=1*) actual=1 ;;
|
||||
*) actual=0 ;;
|
||||
esac
|
||||
if [ "$actual" != "$6" ]; then
|
||||
printf 'Incorrect KISS build policy: %s/%s/%s/%s -> %s, expected %s\n' \
|
||||
"$env_name" "$env_platform" "$BUILD_PROFILE_FOR_TARGET" \
|
||||
"$NRF52_OTA_SENSOR_PROFILE" "$actual" "$6" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
'''.replace("@POLICY@", policy)
|
||||
cases = (
|
||||
("heltec_v3_repeater", "ESP32_PLATFORM", "full", "", "1", "1"),
|
||||
("heltec_v4_companion_radio_full", "ESP32_PLATFORM", "standard", "", "0", "1"),
|
||||
("heltec_v3_repeater", "ESP32_PLATFORM", "standard", "", "0", "0"),
|
||||
("heltec_v3_sensor", "ESP32_PLATFORM", "reduced", "", "0", "0"),
|
||||
("RAK_4631_companion_radio_full", "NRF52_PLATFORM", "standard", "", "0", "1"),
|
||||
("RAK_4631_sensor_lora_ota_no_external_sensors", "NRF52_PLATFORM", "auto", "full", "0", "1"),
|
||||
("RAK_4631_sensor_lora_ota_no_external_sensors", "NRF52_PLATFORM", "auto", "reduced", "0", "0"),
|
||||
("RAK_3401_repeater_lora_ota_no_external_sensors", "NRF52_PLATFORM", "auto", "full", "0", "1"),
|
||||
("RAK_3401_room_server_lora_ota_no_external_sensors", "NRF52_PLATFORM", "auto", "reduced", "0", "0"),
|
||||
("RAK_4631_repeater_rak15002_sd_seeder_only", "NRF52_PLATFORM", "auto", "full", "0", "0"),
|
||||
("heltec_v3_kiss_modem", "ESP32_PLATFORM", "full", "", "1", "0"),
|
||||
("wio_e5_repeater", "STM32_PLATFORM", "full", "", "0", "0"),
|
||||
("xiao_rp2040_repeater", "RP2040_PLATFORM", "full", "", "0", "0"),
|
||||
)
|
||||
for case in cases:
|
||||
script += "apply_policy " + shlex.join(case) + " || exit $?\n"
|
||||
result = subprocess.run(["bash", "-c", script, "host-kiss-policy", str(ROOT)],
|
||||
text=True, capture_output=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertNotIn("Unexpected PlatformIO invocation", result.stdout + result.stderr)
|
||||
|
||||
def test_actual_repeater_console_preserves_network_and_housekeeping(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler)
|
||||
pump = body((ROOT / "examples/simple_repeater/main.cpp").read_text(),
|
||||
"static void __attribute__((noinline)) serviceCommandInterfaces()")
|
||||
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
|
||||
for enabled, cooperative, lose_owner in ((0, 0, False), (0, 1, False),
|
||||
(1, 0, False), (1, 1, False),
|
||||
(1, 1, True)):
|
||||
with self.subTest(enabled=enabled, cooperative=cooperative, lose_owner=lose_owner):
|
||||
candidate = pump
|
||||
if lose_owner:
|
||||
candidate = candidate.replace("if (mesh::hostkiss::serviceUsb())",
|
||||
"if (false && mesh::hostkiss::serviceUsb())")
|
||||
self.assertNotEqual(candidate, pump)
|
||||
with tempfile.TemporaryDirectory(prefix="host-kiss-infra-") as directory:
|
||||
directory = Path(directory)
|
||||
(directory / "test.cpp").write_text(FIXTURE.replace("@PUMP@", candidate), encoding="ascii")
|
||||
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", *sanitizers,
|
||||
"-DMESH_ENABLE_HOST_KISS=" + str(enabled),
|
||||
"-DMESH_USB_CONSOLE_COOPERATIVE=" + str(cooperative), "-DETHERNET_ENABLED=1",
|
||||
str(directory / "test.cpp"), "-o", str(directory / "test")],
|
||||
text=True, capture_output=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(directory / "test")], text=True,
|
||||
capture_output=True, timeout=10)
|
||||
if lose_owner:
|
||||
self.assertNotEqual(result.returncode, 0,
|
||||
"Negative control did not lose KISS ownership")
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_all_infrastructure_roles_claim_before_terminal_output_and_keep_rf_loop(self):
|
||||
for role in ("simple_repeater", "simple_room_server", "simple_sensor"):
|
||||
with self.subTest(role=role):
|
||||
source = (ROOT / "examples" / role / "main.cpp").read_text()
|
||||
signature = ("static void __attribute__((noinline)) serviceCommandInterfaces()"
|
||||
if role == "simple_repeater" else "void loop()")
|
||||
pump = body(source, signature)
|
||||
self.assertEqual(pump.count("takeUsbTerminalSessionReset()"), 1)
|
||||
self.assertLess(pump.index("mesh::hostkiss::resetUsbSession()"),
|
||||
pump.index("mesh::hostkiss::serviceUsb()"))
|
||||
self.assertLess(pump.index("mesh::hostkiss::serviceUsb()"),
|
||||
pump.index("mesh::serviceUsbTerminalPort()"))
|
||||
self.assertIn("usb_ready = false;", pump)
|
||||
loop = body(source, "void loop()")
|
||||
self.assertIn("the_mesh.loop();", loop)
|
||||
self.assertIn("if (!mesh::hostkiss::usbActive())", loop)
|
||||
self.assertIn("sleep_guard(!mesh::hostkiss::usbActive()", loop)
|
||||
recovery = body(source, "static bool usbLoggingRecoverySafe(")
|
||||
self.assertIn("if (mesh::hostkiss::usbActive()) return false;", recovery)
|
||||
|
||||
def test_common_binding_routing_and_loop_use_loaded_radio_profiles(self):
|
||||
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
management = (ROOT / "src/helpers/CommonCLI_Management.cpp").read_text()
|
||||
begin = body(management, "void CommonCLI::beginManagement(")
|
||||
self.assertIn("mesh::hostkiss::begin(fs, *radio, _radio_profiles, mesh);", begin)
|
||||
command = body(common, "void CommonCLI::handleCommand(")
|
||||
self.assertIn("mesh::hostkiss::handleCommand(command, reply, 157, sender_timestamp == 0)", command)
|
||||
loop = body(common, "void CommonCLI::loop()")
|
||||
guarded = body(loop, "if (!mesh::morseTimingActive(")
|
||||
self.assertIn("mesh::hostkiss::loop();", guarded)
|
||||
self.assertLess(guarded.index("_radio_profiles.loop();"),
|
||||
guarded.index("mesh::hostkiss::loop();"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Exercise raw radio2 host KISS isolation and concrete modem protocol selection."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
from test_meshtext_transport import HARNESS as FOREIGN_HARNESS
|
||||
import test_meshtext_config as feature_config
|
||||
import test_ethernet_radio as ethernet_test
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class HostKissRadioTest(unittest.TestCase):
|
||||
def run_cpp(self, source, *, dispatcher=False):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler)
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-host-kiss-") as folder:
|
||||
cpp, exe = Path(folder) / "test.cpp", Path(folder) / "test"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
args = [compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
"-DMESH_ENABLE_HOST_KISS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=0",
|
||||
"-DMESH_ENABLE_MESHTEXT_WIRE=0", "-DMESH_ENABLE_ETHERNET_WIRE=0",
|
||||
"-DMESH_ENABLE_TX_BEACONS=0", "-I", str(ROOT / "test/mocks"),
|
||||
"-I", str(ROOT / "src")]
|
||||
if os.name != "nt":
|
||||
args += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
|
||||
if dispatcher:
|
||||
args += [str(ROOT / "src" / name) for name in (
|
||||
"Dispatcher.cpp", "Packet.cpp", "helpers/StaticPoolPacketManager.cpp")]
|
||||
result = subprocess.run(args + [str(cpp), "-o", str(exe)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr[-16000:])
|
||||
env = dict(os.environ)
|
||||
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
|
||||
result = subprocess.run([str(exe)], capture_output=True, text=True, env=env)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_actual_dispatcher_host_only_routing_and_scheduler_admission(self):
|
||||
fixture = FOREIGN_HARNESS.split("int main()", 1)[0]
|
||||
fixture = fixture.replace("RadioProfileProtocol::Meshtext", "RadioProfileProtocol::Kiss")
|
||||
fixture = fixture.replace("bool supportsMeshtext() const override", "bool supportsHostKiss() const override")
|
||||
fixture = fixture.replace("return queueForeignRadioFrame(b,n,1,_radio->profiles()->generation[1]);",
|
||||
"return submitHostKissFrame(b,n,_radio->profiles()->generation[1]);")
|
||||
hooks = r'''
|
||||
namespace mesh { namespace hostkiss {
|
||||
unsigned rx=0; std::vector<bool> completions;
|
||||
void receive(const uint8_t* b,size_t n,uint8_t p,uint32_t g,float,float) {
|
||||
assert(n==3 && b[0]==0x11 && p==1 && g); ++rx;
|
||||
}
|
||||
void sendComplete(bool success) { completions.push_back(success); }
|
||||
} }
|
||||
int main() {
|
||||
using namespace mesh::hostkiss;
|
||||
{ Fixture f;const auto done=completions.size();assert(f.d.queue(bytes));
|
||||
assert(f.d.hostKissFramePending() && !f.d.queue(bytes));f.tick();
|
||||
assert(f.radio.sends==1 && f.radio.active==1 && f.radio.sent==std::vector<uint8_t>(bytes,bytes+3));
|
||||
assert(!f.d.cancelHostKissFrame());f.radio.complete=true;f.tick(20);
|
||||
assert(completions.size()==done+1 && completions.back() && f.d.done.empty());
|
||||
assert(!f.d.hostKissFramePending() && f.radio.p.tx_packets[1]==1);
|
||||
assert(!f.d.getNumSentFlood() && !f.d.getNumSentDirect() && f.d.getTotalAirTime()==20);
|
||||
}
|
||||
{ Fixture f;auto before=rx;f.radio.active=1;f.radio.incoming=true;f.tick();
|
||||
assert(rx==before+1 && !f.d.foreign_rx && !f.d.mc_rx && !f.d.raw_logs);
|
||||
assert(!f.d.getNumRecvFlood() && !f.d.getNumRecvDirect() && !f.d.getLastMeshCoreRecvMillis());
|
||||
assert(f.radio.processed==1 && f.radio.p.rx_packets[1]==1);
|
||||
f.radio.p.secondary.mode=RadioProfileMode::Off;f.radio.incoming=true;f.tick();assert(rx==before+1);
|
||||
f.radio.active=0;f.radio.p.primary.protocol=RadioProfileProtocol::Kiss;
|
||||
f.radio.incoming=true;f.tick();assert(rx==before+1 && !f.d.foreign_rx && !f.d.raw_logs);
|
||||
}
|
||||
{ Fixture f;auto done=completions.size();assert(f.d.queue(bytes));assert(f.d.cancelHostKissFrame());
|
||||
assert(completions.size()==done+1 && !completions.back() && !f.d.hostKissFramePending());
|
||||
f.tick();assert(!f.radio.sends && f.d.done.empty());
|
||||
}
|
||||
{ Fixture f;auto done=completions.size();assert(f.d.queue(bytes));++f.radio.p.generation[1];f.tick();
|
||||
assert(!f.radio.sends && !f.d.hostKissFramePending() && completions.size()==done+1 && !completions.back());
|
||||
}
|
||||
{ Fixture f;auto done=completions.size();assert(f.d.queue(bytes));f.tick();++f.radio.p.generation[1];
|
||||
f.radio.complete=true;f.tick();assert(completions.size()==done+1 && !completions.back() && f.radio.finishes==1);
|
||||
}
|
||||
{ Fixture f;assert(!f.d.submitHostKissFrame(bytes,3,f.radio.p.generation[1]+1));
|
||||
f.radio.supported=false;assert(!f.d.queue(bytes));f.radio.supported=true;
|
||||
f.radio.p.secondary.mode=RadioProfileMode::Rx;assert(!f.d.queue(bytes));
|
||||
f.radio.p.secondary.mode=RadioProfileMode::Off;assert(!f.d.queue(bytes));
|
||||
f.radio.p.secondary.mode=RadioProfileMode::RxTx;f.radio.p.secondary.params.protocol=RadioProfileProtocol::MeshCore;
|
||||
assert(!f.d.queue(bytes));f.radio.p.secondary.params.protocol=RadioProfileProtocol::Kiss;
|
||||
assert(!f.d.queue(nullptr));assert(!f.d.queue(bytes,0));uint8_t full[256]={};assert(!f.d.queue(full,256));
|
||||
f.radio.carrier=true;assert(!f.d.queue(bytes));f.radio.carrier=false;
|
||||
f.d.allow_foreign=false;assert(!f.d.queue(bytes));
|
||||
}
|
||||
{ Fixture f;auto done=completions.size();f.radio.prepare=RadioParamApplyResult::BUSY;
|
||||
assert(f.d.queue(bytes));f.tick();assert(!f.radio.sends && f.d.hostKissFramePending());
|
||||
f.radio.prepare=RadioParamApplyResult::APPLIED;f.tick(11);assert(f.radio.sends==1 && f.radio.active==1);
|
||||
f.d.setRadioAvailable(false);assert(completions.size()==done+1 && !completions.back());
|
||||
}
|
||||
{ Fixture f;auto done=completions.size();f.radio.start_ok=false;assert(f.d.queue(bytes));f.tick();f.tick(2000);
|
||||
assert(f.radio.sends==1 && !f.d.hostKissFramePending() && completions.size()==done+1 && !completions.back());
|
||||
}
|
||||
{ Fixture f;auto done=completions.size();assert(f.d.queue(bytes));f.tick();f.tick(1151);
|
||||
assert(f.radio.sends==1 && f.radio.finishes==1 && f.radio.recovery==1);
|
||||
assert(completions.size()==done+1 && !completions.back());
|
||||
}
|
||||
{ Fixture f;f.radio.receiving=true;assert(f.d.queue(bytes));f.tick();f.tick(201);
|
||||
assert(!f.radio.sends && f.d.hostKissFramePending());f.radio.receiving=false;f.tick(201);
|
||||
assert(f.radio.sends==1 && f.radio.active==1);
|
||||
}
|
||||
{ Fixture f;f.d.duty_window=200;f.d.begin();assert(f.d.queue(bytes));f.tick();
|
||||
f.clock.now+=100;f.radio.complete=true;f.d.loop();assert(f.d.getRemainingTxBudget()==0);
|
||||
assert(f.d.queue(bytes));f.tick(100);assert(f.radio.sends==1);f.tick(101);assert(f.radio.sends==2);
|
||||
}
|
||||
{ Fixture f;assert(f.d.queue(bytes));f.tick();f.radio.complete=true;f.tick();assert(f.d.queue(bytes));
|
||||
auto* p=newPacket(f.d,RADIO_TX_PRIMARY);assert(f.d.sendPacket(p,0));f.tick();
|
||||
assert(f.radio.sends==2 && f.radio.active==0);f.radio.complete=true;f.tick();f.tick();
|
||||
assert(f.radio.sends==3 && f.radio.active==1);
|
||||
}
|
||||
{ Fixture f;auto* p=newPacket(f.d);f.radio.p.cross=RadioCrossMode::On;
|
||||
assert(!f.radio.p.canCross() && f.d.getTransmitProfileMask(p)==1);
|
||||
assert(f.d.sendPacket(p,0));f.tick();assert(f.radio.sends==1 && f.radio.active==0);
|
||||
auto* denied=newPacket(f.d,RADIO_TX_SECONDARY);assert(!f.d.sendPacket(denied,0));
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.run_cpp(fixture + hooks, dispatcher=True)
|
||||
|
||||
def test_actual_protocol_selection_on_every_supported_wrapper(self):
|
||||
for family in ("SX1262", "SX1268", "SX1276", "LLCC68", "STM32WLx", "LR1110", "LR1121", "LR2021"):
|
||||
with self.subTest(family=family):
|
||||
source = (ROOT / "src/helpers/radiolib" / ("Custom" + family + "Wrapper.h")).read_text()
|
||||
select = body(source, "bool applyProtocol(mesh::RadioProfileProtocol protocol)")
|
||||
select = select.replace(" override", "")
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <RadioProfiles.h>
|
||||
#include <helpers/radiolib/LR2021ApplyStatus.h>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
struct Driver {
|
||||
unsigned calls=0; uint8_t sync=0; int status=0;
|
||||
int setProfileProtocol(uint8_t s) { ++calls;sync=s;return status; }
|
||||
};
|
||||
using @CLASS@=Driver;
|
||||
struct TestWrapper {
|
||||
Driver driver; Driver* _radio=&driver;
|
||||
bool supportsMeshtext() const { return false; }
|
||||
bool supportsHostKiss() const { return true; }
|
||||
int recordApply(mesh::lr2021::ApplyStatus::Stage,int code) { return code; }
|
||||
@SELECT@
|
||||
};
|
||||
int main() {
|
||||
using P=mesh::RadioProfileProtocol;
|
||||
TestWrapper w;assert(w.applyProtocol(P::Kiss));assert(w.driver.sync==0x12 && w.driver.calls==1);
|
||||
assert(w.applyProtocol(P::MeshCore));assert(w.driver.sync==0x12 && w.driver.calls==2);
|
||||
assert(!w.applyProtocol(static_cast<P>(255)) && w.driver.calls==2);
|
||||
w.driver.status=-17;assert(!w.applyProtocol(P::Kiss));
|
||||
}
|
||||
'''
|
||||
self.run_cpp(harness.replace("@CLASS@", "Custom" + family).replace("@SELECT@", select))
|
||||
|
||||
def test_actual_cli_protocol_lease_preserves_saved_intent_and_other_owners(self):
|
||||
code = r'''
|
||||
#define main existing_cli_main
|
||||
#include "test/fixtures/radio_profiles/cli_test.cpp"
|
||||
#undef main
|
||||
int main() {
|
||||
using P=mesh::RadioProfileProtocol;
|
||||
Fixture f;
|
||||
f.cmd("set radio2 910.5,500,8,5,rxtx,80");f.advance(2000);
|
||||
const auto saved=f.fs.files["/radio_profiles"];
|
||||
const auto tuple=f.radio.p.secondary;
|
||||
const auto intent=f.cli.secondaryIntentGeneration();
|
||||
const auto generation=f.radio.p.generation[1];
|
||||
assert(f.cli.manualSecondaryConfigured() && f.cli.secondaryRuntimeAvailable());
|
||||
assert(f.cli.useSecondaryProtocol(P::Kiss));
|
||||
assert(f.radio.p.secondary.params.protocol==P::Kiss && f.radio.p.generation[1]!=generation);
|
||||
assert(f.radio.profilePreamble(1)==80 && f.fs.files["/radio_profiles"]==saved);
|
||||
assert(f.cli.secondaryIntentGeneration()==intent && f.cli.savedSecondaryConfig().params==tuple.params);
|
||||
const auto kiss_generation=f.radio.p.generation[1];
|
||||
assert(f.cli.useSecondaryProtocol(P::Kiss) && f.radio.p.generation[1]==kiss_generation);
|
||||
// A running host lease is not replaceable by another foreign service.
|
||||
assert(!f.cli.useSecondaryProtocol(P::Meshtext));
|
||||
assert(!f.cli.useSecondaryProtocol(P::Ethernet));
|
||||
f.cli.clearSecondaryProtocol();
|
||||
assert(f.radio.p.secondary.params==tuple.params && f.radio.p.secondary.mode==tuple.mode);
|
||||
assert(f.fs.files["/radio_profiles"]==saved);
|
||||
// Saved manual changes retire the host intent immediately, while preserving
|
||||
// the two-second reply window before publishing the new running tuple.
|
||||
assert(f.cli.useSecondaryProtocol(P::Kiss));
|
||||
f.cmd("set radio2 912.5,500,9,5,rxtx,88");
|
||||
assert(f.cli.secondaryIntentGeneration()!=intent && !f.cli.secondaryRuntimeAvailable());
|
||||
assert(f.radio.p.secondary.params.protocol==P::Kiss && f.radio.p.secondary.params.freq==910.5f);
|
||||
f.cli.clearSecondaryProtocol();f.advance(1999);
|
||||
assert(f.radio.p.secondary.params.protocol==P::Kiss && f.radio.p.secondary.params.freq==910.5f);
|
||||
f.advance(1);
|
||||
assert(f.radio.p.secondary.params.protocol==P::MeshCore && f.radio.p.secondary.params.freq==912.5f);
|
||||
assert(f.radio.profilePreamble(1)==88 && f.cli.secondaryRuntimeAvailable());
|
||||
assert(f.cli.useSecondaryProtocol(P::Kiss));
|
||||
// A direct outside owner wins even if the old host later releases its
|
||||
// overlay. Old host cleanup must not restore the saved tuple over it.
|
||||
auto foreign=f.radio.p.secondary;foreign.params.protocol=P::Meshtext;foreign.params.freq=913.5f;
|
||||
f.radio.p.setSecondary(foreign,false);
|
||||
f.cli.clearSecondaryProtocol();f.advance(1);
|
||||
assert(f.radio.p.secondary.params==foreign.params && !f.cli.secondaryRuntimeAvailable());
|
||||
f.cmd("set radio2 914.5,500,9,5,rxtx,88");f.advance(2000);
|
||||
assert(f.cli.secondaryRuntimeAvailable() && f.cli.useSecondaryProtocol(P::Kiss));
|
||||
#if MESH_ENABLE_MESHTEXT_CHAT
|
||||
f.cli.clearSecondaryProtocol();assert(f.cli.useSecondaryProtocol(P::Meshtext));
|
||||
const auto mt=f.radio.p.secondary;
|
||||
assert(!f.cli.useSecondaryProtocol(P::Kiss) && f.radio.p.secondary.params==mt.params);
|
||||
#endif
|
||||
#if MESH_ENABLE_ETHERNET_TUNNEL
|
||||
f.cli.clearSecondaryProtocol();assert(f.cli.useSecondaryProtocol(P::Ethernet));
|
||||
const auto eth=f.radio.p.secondary;
|
||||
assert(!f.cli.useSecondaryProtocol(P::Kiss) && f.radio.p.secondary.params==eth.params);
|
||||
#endif
|
||||
Fixture absent;assert(!absent.cli.useSecondaryProtocol(P::Kiss));
|
||||
absent.cmd("set radio2 off");absent.advance(2000);
|
||||
assert(!absent.cli.useSecondaryProtocol(P::Kiss));
|
||||
}
|
||||
'''
|
||||
runner = ethernet_test.EthernetRadioTest()
|
||||
runner.setUpClass()
|
||||
for foreign in (0, 1):
|
||||
with self.subTest(other_foreign_services=foreign):
|
||||
runner.compile_run(code, ["-DMESH_ENABLE_HOST_KISS=1",
|
||||
"-DMESH_ENABLE_MESHTEXT_CHAT=" + str(foreign),
|
||||
"-DMESH_ENABLE_ETHERNET_TUNNEL=" + str(foreign)],
|
||||
["src/helpers/RadioProfileCLI.cpp"], ["test/fixtures/radio_profiles/mocks"])
|
||||
|
||||
def test_feature_layout_is_consistent_with_platform_and_explicit_overrides(self):
|
||||
checker = feature_config.MeshtextConfigTest()
|
||||
checker.setUp()
|
||||
for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"):
|
||||
checker.assert_orders([platform, "COMPANION_RADIO_FULL=1"], 1, feature="MESH_ENABLE_HOST_KISS")
|
||||
checker.assert_orders([platform, "COMPANION_RADIO_FULL=0"], 0, feature="MESH_ENABLE_HOST_KISS")
|
||||
checker.assert_orders([platform, "COMPANION_RADIO_FULL=1", "MESH_ENABLE_HOST_KISS=0"], 0,
|
||||
feature="MESH_ENABLE_HOST_KISS")
|
||||
checker.assert_orders([platform, "MESH_ENABLE_HOST_KISS=1"], 1, feature="MESH_ENABLE_FOREIGN_RADIO")
|
||||
checker.assert_orders([], 0, feature="MESH_ENABLE_HOST_KISS")
|
||||
checker.assert_orders(["RP2040_PLATFORM", "COMPANION_RADIO_FULL=1"], 0, feature="MESH_ENABLE_HOST_KISS")
|
||||
checker.assert_orders(["MESH_PRIMARY_ESPNOW=1", "MESH_ENABLE_HOST_KISS=1"], 0,
|
||||
feature="MESH_ENABLE_HOST_KISS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute cached USB text guards and Full Companion raw-KISS handoff code."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
class Stream {
|
||||
public:
|
||||
virtual ~Stream() = default;
|
||||
virtual int available() = 0;
|
||||
virtual int read() = 0;
|
||||
virtual int peek() = 0;
|
||||
virtual void flush() = 0;
|
||||
virtual int availableForWrite() { return 0; }
|
||||
virtual size_t write(uint8_t) = 0;
|
||||
virtual size_t write(const uint8_t*, size_t) = 0;
|
||||
};
|
||||
'''
|
||||
|
||||
TEXT_FIXTURE = r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <helpers/UsbBinaryTextGuard.h>
|
||||
static std::atomic<bool> usb_binary_stream_owned{false};
|
||||
static std::atomic<uint32_t> usb_text_writers{0};
|
||||
static bool logging = true, debug = true;
|
||||
struct Port : Stream {
|
||||
unsigned reads=0, peeks=0, available_calls=0, flushes=0, capacity_calls=0;
|
||||
std::string output;
|
||||
int available() override { ++available_calls; return 3; }
|
||||
int read() override { ++reads; return 65; }
|
||||
int peek() override { ++peeks; return 65; }
|
||||
void flush() override { ++flushes; }
|
||||
int availableForWrite() override { ++capacity_calls; return 99; }
|
||||
size_t write(uint8_t value) override { return write(&value,1); }
|
||||
size_t write(const uint8_t* data,size_t length) override {
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
if (!usb_binary_stream_owned.load()) assert(usb_text_writers.load()==1);
|
||||
#endif
|
||||
output.append(reinterpret_cast<const char*>(data),length); return length;
|
||||
}
|
||||
} raw, null_port;
|
||||
namespace mesh {
|
||||
bool isUsbBinaryStreamOwned() { return usb_binary_stream_owned.load(); }
|
||||
bool isUsbDebugLoggingEnabled() { return !isUsbBinaryStreamOwned() && logging && debug; }
|
||||
@COUNTERS@
|
||||
static Stream& null_usb_logging_stream = null_port;
|
||||
static Stream& rawUsbLoggingPort() { return logging ? static_cast<Stream&>(raw) : null_port; }
|
||||
static Stream& rawUsbTerminalPort(bool enabled) { return enabled ? static_cast<Stream&>(raw) : null_port; }
|
||||
static Stream& rawUsbConsolePort() { return raw; }
|
||||
@PORTS@
|
||||
}
|
||||
int main() {
|
||||
using namespace mesh;
|
||||
auto& cached_log=usbLoggingPort();
|
||||
auto& cached_debug=usbDebugPort();
|
||||
auto& cached_terminal=usbTerminalPort(true);
|
||||
auto& cached_console=usbConsolePort();
|
||||
const uint8_t text[]={'a','b','c'};
|
||||
assert(cached_log.write(text,3)==3 && raw.output=="abc");
|
||||
usb_binary_stream_owned.store(true);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
for (auto* cached: {&cached_log,&cached_debug,&cached_terminal,&cached_console}) {
|
||||
assert(cached->write(text,3)==3 && cached->write(uint8_t('!'))==1);
|
||||
assert(!cached->available() && cached->read()==-1 && cached->peek()==-1);
|
||||
assert(!cached->availableForWrite());cached->flush();
|
||||
}
|
||||
assert(raw.output=="abc" && !raw.reads && !raw.peeks && !raw.available_calls);
|
||||
assert(!raw.flushes && !raw.capacity_calls && !canAcceptUsbConsoleCommand());
|
||||
// New port lookups must be guarded too, without retargeting cached writers
|
||||
// to a binary facade. Binary output uses its separate raw transport.
|
||||
assert(usbLoggingPort().write(text,3)==3);
|
||||
assert(usbTerminalPort(true).write(text,3)==3);
|
||||
assert(usbConsolePort().write(text,3)==3 && raw.output=="abc");
|
||||
assert(raw.write(reinterpret_cast<const uint8_t*>("KISS"),4)==4 && raw.output=="abcKISS");
|
||||
usb_binary_stream_owned.store(false);
|
||||
assert(cached_terminal.write(text,3)==3 && raw.output=="abcKISSabc");
|
||||
assert(cached_log.available()==3 && cached_log.read()==65 && cached_log.peek()==65);
|
||||
cached_console.flush();assert(cached_console.availableForWrite()==99 && canAcceptUsbConsoleCommand());
|
||||
assert(raw.flushes==1 && raw.capacity_calls==1);
|
||||
#else
|
||||
// Feature-disabled images retain the prior raw facade and layouts.
|
||||
assert(&cached_log==&raw && &cached_debug==&raw && &cached_terminal==&raw && &cached_console==&raw);
|
||||
assert(cached_console.write(text,3)==3 && raw.output=="abcabc");
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
|
||||
COMPANION_FIXTURE = r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
static bool kiss_active=false, terminal_mode=false, usb_mota_mode=false, binary_owned=false;
|
||||
static bool usb_logging_reply_pending=true;
|
||||
static bool host_kiss_usb_parked=false,host_kiss_usb_restore_enabled=false;
|
||||
static bool usb_terminal_discard_line=false,usb_logging_terminal_mode=false;
|
||||
static unsigned serial_cancels=0,route_cancels=0,forgotten_routes=0;
|
||||
static unsigned input_resets=0,text_discards=0,line_clears=0,reader_cancels=0,mota_resets=0;
|
||||
static unsigned ascii_calls=0,usb_binary_calls=0,network_calls=0,kiss_resets=0;
|
||||
static std::deque<uint8_t> input;
|
||||
struct Port {} terminal_port;
|
||||
struct Usb {
|
||||
bool enabled=true,passthrough=false;
|
||||
unsigned resets=0,frames=19,enables=0,disables=0;
|
||||
bool isEnabled() const { return enabled; }
|
||||
void enable(){enabled=true;++enables;}
|
||||
void disable(){enabled=false;++disables;}
|
||||
void resetSessionState(){++resets;}
|
||||
unsigned getCompletedFrameCount()const{return frames;}
|
||||
void setPassthroughMode(bool value){passthrough=value;}
|
||||
} usb_serial_interface;
|
||||
static bool ble_stream_pending=true,ble_operations_pending=true;
|
||||
struct Mesh {
|
||||
bool usb_stream_pending=true,usb_operations_pending=true;
|
||||
void cancelSerialResponseStream(Usb* p){assert(p==&usb_serial_interface);++serial_cancels;usb_stream_pending=false;}
|
||||
void cancelSerialOperationsForRoute(Usb* p){assert(p==&usb_serial_interface);++route_cancels;usb_operations_pending=false;}
|
||||
void resetUsbHostSessionInput(){++input_resets;}
|
||||
bool isTerminalMode()const{return terminal_mode;}
|
||||
} the_mesh;
|
||||
struct Manager {
|
||||
bool enabled=true;
|
||||
bool isEnabled()const{return enabled;}
|
||||
void forgetReplyRouteForDisconnected(Usb* p){assert(p==&usb_serial_interface);++forgotten_routes;}
|
||||
void loop(){++network_calls;if(usb_serial_interface.enabled && !input.empty())++usb_binary_calls;}
|
||||
} interface_manager;
|
||||
struct SessionProbe {
|
||||
unsigned cancels=0,requests=0,last_frame=0;
|
||||
void cancel(){++cancels;}
|
||||
void request(unsigned frame){++requests;last_frame=frame;}
|
||||
} usb_binary_startup_probe,usb_ascii_session_default;
|
||||
void clearUsbTerminalLine(){++line_clears;}
|
||||
void leaveUsbTerminalMode(bool banner){assert(!banner);terminal_mode=false;}
|
||||
void leaveUsbMotaMode(bool resume){assert(!resume);usb_mota_mode=false;}
|
||||
namespace mesh {
|
||||
Port& usbTerminalPort(){return terminal_port;}
|
||||
void cancelReaderCommand(Port* p){assert(p==&terminal_port);++reader_cancels;}
|
||||
void discardUsbTerminalOutput(){++text_discards;}
|
||||
void setUsbBinaryStreamOwned(bool value){binary_owned=value;}
|
||||
namespace ota {
|
||||
struct Source {void resetSessionState(){++mota_resets;}} source;
|
||||
struct OtaContext {static Source& serialFolderSource(){return source;}};
|
||||
}
|
||||
namespace hostkiss {
|
||||
bool usbActive(){return kiss_active;}
|
||||
void resetUsbSession(){++kiss_resets;if(kiss_active){kiss_active=false;binary_owned=false;}}
|
||||
bool serviceUsb(){
|
||||
if(!kiss_active && !input.empty() && input.front()==0xc0){input.pop_front();kiss_active=true;binary_owned=true;}
|
||||
bool claimed=kiss_active;
|
||||
while(kiss_active && !input.empty()){
|
||||
auto byte=input.front();input.pop_front();
|
||||
if(byte==0xff){kiss_active=false;binary_owned=false;break;}
|
||||
}
|
||||
return claimed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@CANCEL@
|
||||
@RESET@
|
||||
@SERVICE@
|
||||
static bool poll(){
|
||||
bool claimed=serviceCompanionHostKissUsb();
|
||||
if(!claimed && !input.empty()){++ascii_calls;input.clear();}
|
||||
interface_manager.loop();
|
||||
return claimed;
|
||||
}
|
||||
int main(){
|
||||
input={'v','e','r','\r'};assert(!poll() && ascii_calls==1 && !serial_cancels);
|
||||
#if MESH_ENABLE_HOST_KISS
|
||||
terminal_mode=true;usb_mota_mode=true;usb_terminal_discard_line=usb_logging_terminal_mode=true;
|
||||
input={0xc0,0x00,0x41};assert(poll());
|
||||
assert(kiss_active && host_kiss_usb_parked && host_kiss_usb_restore_enabled && !usb_serial_interface.enabled);
|
||||
assert(serial_cancels==1 && route_cancels==1 && forgotten_routes==1);
|
||||
assert(!the_mesh.usb_stream_pending && !the_mesh.usb_operations_pending);
|
||||
assert(ble_stream_pending && ble_operations_pending && network_calls==2);
|
||||
assert(!terminal_mode && !usb_terminal_discard_line && !usb_logging_terminal_mode);
|
||||
assert(input_resets==1 && line_clears==1 && text_discards==1);
|
||||
assert(!usb_logging_reply_pending && usb_binary_startup_probe.cancels==1 && usb_ascii_session_default.cancels==1);
|
||||
#if COMPANION_FEATURE_READER
|
||||
assert(reader_cancels==1);
|
||||
#endif
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
assert(!usb_mota_mode && mota_resets==1);
|
||||
#endif
|
||||
assert(poll() && network_calls==3 && !usb_binary_calls && ascii_calls==1);
|
||||
// RETURN ends binary ownership, but does not dispatch a trailing ASCII or
|
||||
// Companion frame during this same poll. Restore is the next poll's work.
|
||||
input={0xff,'v','e','r','\r'};assert(poll());
|
||||
assert(!kiss_active && host_kiss_usb_parked && !usb_serial_interface.enabled && binary_owned);
|
||||
assert(ascii_calls==1 && !usb_binary_calls && !input.empty());
|
||||
assert(!poll());
|
||||
assert(!host_kiss_usb_parked && !host_kiss_usb_restore_enabled && usb_serial_interface.enabled && !binary_owned);
|
||||
assert(ascii_calls==2 && !usb_binary_calls && usb_serial_interface.resets==1);
|
||||
assert(usb_ascii_session_default.requests==1 && usb_ascii_session_default.last_frame==19);
|
||||
// A transport already disabled by its manager must not be resurrected.
|
||||
usb_serial_interface.enabled=false;input={0xc0};assert(poll());
|
||||
assert(!host_kiss_usb_restore_enabled);input={0xff};assert(poll());assert(!poll());
|
||||
assert(!usb_serial_interface.enabled && !host_kiss_usb_parked);
|
||||
// Disabling the manager during the KISS lease also vetoes restoration.
|
||||
usb_serial_interface.enabled=true;input={0xc0};assert(poll());interface_manager.enabled=false;
|
||||
input={0xff};assert(poll());assert(!poll());assert(!usb_serial_interface.enabled);
|
||||
interface_manager.enabled=true;
|
||||
// A disconnect between RETURN and the next poll also releases the saved
|
||||
// one-poll quarantine even though the runtime KISS session already ended.
|
||||
usb_serial_interface.enabled=true;input={0xc0};assert(poll());
|
||||
input={0xff};assert(poll());assert(binary_owned && !kiss_active);
|
||||
resetUsbTerminalHostSession();
|
||||
assert(!binary_owned && !host_kiss_usb_parked && usb_serial_interface.enabled && kiss_resets==1);
|
||||
// An actual host-session reset cancels only USB work and retires KISS.
|
||||
usb_serial_interface.enabled=true;input={0xc0};assert(poll());
|
||||
resetUsbTerminalHostSession();
|
||||
assert(!kiss_active && kiss_resets==2 && !host_kiss_usb_parked && !host_kiss_usb_restore_enabled);
|
||||
assert(usb_serial_interface.enabled && !usb_serial_interface.passthrough);
|
||||
assert(ble_stream_pending && ble_operations_pending && !usb_terminal_discard_line && !usb_logging_terminal_mode);
|
||||
#else
|
||||
assert(!serviceCompanionHostKissUsb() && usb_serial_interface.enabled);
|
||||
assert(!serial_cancels && !route_cancels && !forgotten_routes && !input_resets);
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class HostKissUsbTest(unittest.TestCase):
|
||||
def run_cpp(self, source, *, enabled, reader=0, mota=0, fail=False, extra_flags=()):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler)
|
||||
with tempfile.TemporaryDirectory(prefix="host-kiss-usb-") as directory:
|
||||
directory = Path(directory)
|
||||
(directory / "Arduino.h").write_text(ARDUINO, encoding="ascii")
|
||||
(directory / "test.cpp").write_text(source, encoding="ascii")
|
||||
flags = ["-std=c++17", "-Wall", "-Wextra", "-DMESH_ENABLE_HOST_KISS=" + str(enabled),
|
||||
"-DMESH_USB_LOGGING_AVAILABLE=1", "-DMESH_ESP32_TINYUSB_NONBLOCKING=0",
|
||||
"-DMESH_ESP32_HWCDC_SESSION_GUARD=0", "-DMESH_NRF52_USB_CONSOLE_COOPERATIVE=0",
|
||||
"-DCOMPANION_FEATURE_READER=" + str(reader),
|
||||
"-DCOMPANION_FEATURE_USB_MOTA_SOURCE=" + str(mota),
|
||||
"-I", str(directory), "-I", str(ROOT / "src")]
|
||||
if sys.platform.startswith("linux"):
|
||||
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
result = subprocess.run([compiler, *flags, *extra_flags, str(directory / "test.cpp"), "-o", str(directory / "test")],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(directory / "test")], capture_output=True, text=True, timeout=10)
|
||||
if fail:
|
||||
self.assertNotEqual(result.returncode, 0, "Negative control failed to lose USB ownership")
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_actual_cached_text_facades_suppress_binary_session_writes_and_reads(self):
|
||||
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
||||
ports = "\n".join(body(source, signature) for signature in (
|
||||
"Stream& usbLoggingPort()", "Stream& usbDebugPort()", "Stream& usbTerminalPort(bool enabled)",
|
||||
"Stream& usbConsolePort()", "bool canAcceptUsbConsoleCommand()"))
|
||||
for enabled in (0, 1):
|
||||
with self.subTest(enabled=enabled):
|
||||
counters = "\n".join(body(source, signature) for signature in (
|
||||
"static bool beginUsbTextWrite()", "static void endUsbTextWrite()"))
|
||||
self.run_cpp(TEXT_FIXTURE.replace("@PORTS@", ports).replace("@COUNTERS@", counters), enabled=enabled)
|
||||
|
||||
def test_actual_companion_claim_return_reset_and_usb_only_cancellation(self):
|
||||
source = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
cancel = body(source, "static void cancelUsbSerialOperations()")
|
||||
reset = body(source, "static void resetUsbTerminalHostSession()")
|
||||
service = body(source, "static bool serviceCompanionHostKissUsb()")
|
||||
for enabled, reader, mota, negative in ((0, 0, 0, False), (1, 0, 0, False),
|
||||
(1, 1, 1, False), (1, 1, 1, True)):
|
||||
with self.subTest(enabled=enabled, reader=reader, mota=mota, negative=negative):
|
||||
candidate = service
|
||||
if negative:
|
||||
candidate = candidate.replace("host_kiss_usb_parked && !claimed && !mesh::hostkiss::usbActive()",
|
||||
"host_kiss_usb_parked && !mesh::hostkiss::usbActive()")
|
||||
self.assertNotEqual(candidate, service)
|
||||
fixture = COMPANION_FIXTURE.replace("@CANCEL@", cancel).replace("@RESET@", reset).replace("@SERVICE@", candidate)
|
||||
self.run_cpp(fixture, enabled=enabled, reader=reader, mota=mota, fail=negative)
|
||||
|
||||
def test_actual_nrf52_text_queue_discards_backlog_without_draining_binary_stream(self):
|
||||
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
static std::atomic<bool> usb_binary_stream_owned{false};
|
||||
static std::atomic<uint32_t> usb_text_writers{0};
|
||||
static std::atomic<bool> primary_usb_terminal_discard_pending{false};
|
||||
static uint32_t primary_usb_terminal_seen_reset_generation=0, reset_generation=0;
|
||||
static bool platform_debug=false, saved_logging=true, saved_debug=true;
|
||||
uint32_t primaryUsbSessionGeneration(){return reset_generation;}
|
||||
bool isUsbDebugLoggingEnabled(){return !usb_binary_stream_owned.load() && saved_logging && saved_debug;}
|
||||
void setPlatformDebugOutputEnabled(bool enabled){platform_debug=enabled;}
|
||||
struct Queue {
|
||||
unsigned pending=3,discards=0,services=0; bool busy=false;
|
||||
bool tryDiscardPending(){++discards;if(busy)return false;pending=0;return true;}
|
||||
void service(){++services;pending=0;}
|
||||
} buffered_primary_usb_terminal_port;
|
||||
void serviceUsbTerminalPort();
|
||||
void discardUsbTerminalOutput();
|
||||
bool hasPendingUsbTerminalOutput(){return buffered_primary_usb_terminal_port.pending || primary_usb_terminal_discard_pending.load();}
|
||||
@BEGIN@
|
||||
@END@
|
||||
@OWNED@
|
||||
@SET@
|
||||
@READY@
|
||||
@SERVICE@
|
||||
@DISCARD@
|
||||
int main(){
|
||||
// An admitted writer can outlive the ownership store. Even with no queued
|
||||
// bytes or busy queue lock, READY must wait for that producer to leave.
|
||||
assert(beginUsbTextWrite() && usb_text_writers.load()==1);
|
||||
setUsbBinaryStreamOwned(true);
|
||||
assert(!isUsbBinaryStreamReady() && !beginUsbTextWrite() && usb_text_writers.load()==1);
|
||||
endUsbTextWrite();
|
||||
assert(usb_text_writers.load()==0 && isUsbBinaryStreamReady());
|
||||
setUsbBinaryStreamOwned(false);
|
||||
buffered_primary_usb_terminal_port.pending=3;
|
||||
buffered_primary_usb_terminal_port.busy=true;
|
||||
setUsbBinaryStreamOwned(true);
|
||||
assert(!isUsbBinaryStreamReady());
|
||||
assert(isUsbBinaryStreamOwned() && !platform_debug && saved_logging && saved_debug);
|
||||
assert(primary_usb_terminal_discard_pending.load() && !buffered_primary_usb_terminal_port.services);
|
||||
assert(buffered_primary_usb_terminal_port.pending==3);
|
||||
serviceUsbTerminalPort();assert(!buffered_primary_usb_terminal_port.services);
|
||||
buffered_primary_usb_terminal_port.busy=false;
|
||||
serviceUsbTerminalPort();
|
||||
assert(!buffered_primary_usb_terminal_port.pending && !buffered_primary_usb_terminal_port.services);
|
||||
assert(!primary_usb_terminal_discard_pending.load() && isUsbBinaryStreamReady());
|
||||
// New host/reset generations discard rather than expose a queued old line.
|
||||
buffered_primary_usb_terminal_port.pending=4;++reset_generation;
|
||||
serviceUsbTerminalPort();
|
||||
assert(primary_usb_terminal_seen_reset_generation==reset_generation);
|
||||
assert(!buffered_primary_usb_terminal_port.pending && !buffered_primary_usb_terminal_port.services);
|
||||
setUsbBinaryStreamOwned(false);assert(!isUsbBinaryStreamOwned() && platform_debug);
|
||||
serviceUsbTerminalPort();assert(buffered_primary_usb_terminal_port.services==1);
|
||||
}
|
||||
'''
|
||||
for token, signature in (("@BEGIN@", "static bool beginUsbTextWrite()"),
|
||||
("@END@", "static void endUsbTextWrite()"),
|
||||
("@READY@", "bool isUsbBinaryStreamReady()"),
|
||||
("@OWNED@", "bool isUsbBinaryStreamOwned()"),
|
||||
("@SET@", "void setUsbBinaryStreamOwned(bool owned)"),
|
||||
("@SERVICE@", "void serviceUsbTerminalPort()"),
|
||||
("@DISCARD@", "void discardUsbTerminalOutput()")):
|
||||
fixture = fixture.replace(token, body(source, signature))
|
||||
self.run_cpp(fixture, enabled=1, extra_flags=("-DNRF52_PLATFORM=1", "-DUSE_TINYUSB=1"))
|
||||
negative = fixture.replace(" || usb_text_writers.load() != 0", "")
|
||||
self.assertNotEqual(negative, fixture)
|
||||
self.run_cpp(negative, enabled=1, fail=True,
|
||||
extra_flags=("-DNRF52_PLATFORM=1", "-DUSE_TINYUSB=1"))
|
||||
|
||||
def test_actual_loop_holds_ascii_and_binary_parser_gates_through_return_poll(self):
|
||||
source = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
loop = body(source, "\nvoid loop()")
|
||||
self.assertLess(loop.index("serviceUsbTerminalHostSessionReset();"),
|
||||
loop.index("serviceCompanionHostKissUsb();"))
|
||||
self.assertLess(loop.index("serviceCompanionHostKissUsb();"), loop.index("mesh::serviceUsbLoggingPort();"))
|
||||
for call in ("serviceUsbAsciiSessionDefault();", "serviceUsbLoggingReplyBarrier();",
|
||||
"expireUsbBinaryStartupProbeBeforeDispatch();", "serviceUsbMotaResponseBoundary();",
|
||||
"serviceUsbTerminal();"):
|
||||
prefix = loop[:loop.index(call)]
|
||||
self.assertIn("if (!host_kiss_usb_claimed)", prefix[prefix.rfind("#if defined(ENABLE_USB_INTERFACE)"):])
|
||||
self.assertIn("the_mesh.loop();", loop)
|
||||
self.assertIn("interface_manager.loop();", loop)
|
||||
self.assertIn("if (mesh::hostkiss::usbActive()) return false;",
|
||||
body(source, "static bool usbLoggingRecoverySafe("))
|
||||
rescue = body((ROOT / "examples/companion_radio/MyMesh.cpp").read_text(), "void MyMesh::checkCLIRescueCmd()")
|
||||
self.assertLess(rescue.index("if (mesh::isUsbBinaryStreamOwned()) return;"),
|
||||
rescue.index("mesh::usbCompanionPort()"))
|
||||
mymesh = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
|
||||
admission = body(mymesh, "bool allowForeignRadioTransmit() const")
|
||||
self.assertIn("_radio_available", admission)
|
||||
self.assertIn("_scheduled_reboot_at == 0", admission)
|
||||
self.assertIn("mesh::hostkiss::radioActive()", admission)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -64,6 +64,7 @@ class Stream {
|
||||
"""
|
||||
|
||||
RUNTIME = r'''
|
||||
#define MESH_ENABLE_HOST_KISS 0
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
@@ -202,8 +203,10 @@ class Nrf52FrameworkLoggingTest(unittest.TestCase):
|
||||
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
||||
header = (ROOT / "src/helpers/UsbLogging.h").read_text()
|
||||
parts = {
|
||||
"DEBUG_GATE": extract_braced(header, "inline bool isUsbDebugLoggingEnabled("),
|
||||
"LOGGING_PORT": extract_braced(source, "Stream& usbLoggingPort("),
|
||||
"DEBUG_GATE": extract_braced(header, "inline bool isUsbBinaryStreamOwned(") + "\n"
|
||||
+ extract_braced(header, "inline bool isUsbDebugLoggingEnabled("),
|
||||
"LOGGING_PORT": "\n".join(extract_braced(source, signature) for signature in (
|
||||
"static Stream& rawUsbLoggingPort(", "Stream& usbLoggingPort(")),
|
||||
"DEBUG_PORT": extract_braced(source, "Stream& usbDebugPort("),
|
||||
}
|
||||
hook = extract_braced(source, 'extern "C" size_t meshNrf52FrameworkDebugWrite(')
|
||||
|
||||
@@ -330,7 +330,8 @@ int main() {
|
||||
}
|
||||
'''
|
||||
source = source.replace('@NULL@', function(USB, 'class NullUsbLoggingStream') + ';')
|
||||
source = source.replace('@PORT@', function(USB, 'Stream& usbTerminalPort('))
|
||||
source = source.replace('@PORT@', '\n'.join(function(USB, signature) for signature in (
|
||||
'static Stream& rawUsbTerminalPort(', 'Stream& usbTerminalPort(')))
|
||||
source = source.replace('@WAITING@', function(header, 'bool isTerminalWaitingForInput('))
|
||||
source = source.replace('@METHODS@', '\n'.join(function(mesh, signature) for signature in (
|
||||
'Stream& MyMesh::terminalOutput(', 'void MyMesh::printTerminalBanner(',
|
||||
|
||||
Reference in new issue
Block a user