mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 08:43:48 +00:00
Add guarded Bluetooth CLI on/off with force override
Add get bluetooth and set bluetooth on|off, with ble aliases. Require another active management connection before disabling Bluetooth unless off has a trailing force argument. Keep the toggle local to the current boot. Drain Bluetooth replies before disconnecting and recheck the remaining connection before applying a normal shutdown. Enabling Bluetooth cancels a pending shutdown. Document the commands and add compiled regression coverage to CI for native USB, HWCDC, UART, network clients, force, and timer rollover. Validation: CLI, Bluetooth, and USB regression tests passed. XIAO ESP32-S3 and RAK4631 Full Companion builds passed RAM, flash, and capability checks.
This commit is contained in:
1 parent
170c2c44b1
commit
975167938d
6 files changed
+428
-2
No files matched your search
@@ -125,6 +125,9 @@ jobs:
|
||||
- name: Verify Companion contact persistence and OTA sleep guards
|
||||
run: python3 -B test/test_companion_contact_sleep_contract.py -v
|
||||
|
||||
- name: Verify Companion Bluetooth CLI connection protection
|
||||
run: python3 -B test/test_companion_bluetooth_control.py -v
|
||||
|
||||
- name: Verify message reader buttons, touch targets, and footer layouts
|
||||
working-directory: test
|
||||
run: >-
|
||||
|
||||
@@ -1410,6 +1410,39 @@ Station G2/G3 targets default to `off`.
|
||||
|
||||
---
|
||||
|
||||
#### Turn Bluetooth on or off (Companion)
|
||||
|
||||
```text
|
||||
get bluetooth
|
||||
set bluetooth off
|
||||
set bluetooth off force
|
||||
set bluetooth on
|
||||
```
|
||||
|
||||
`get ble` and `set ble ...` are aliases. This controls the running Bluetooth
|
||||
service on ESP32 and nRF52 Companions: `off` stops advertising and disconnects
|
||||
Bluetooth clients; `on` enables it again. The change lasts until reboot.
|
||||
|
||||
Plain `off` requires another active management connection. A connected USB
|
||||
terminal or binary client, TCP client, or browser terminal can provide that
|
||||
connection. WiFi association, a listening server, and USB power alone do not.
|
||||
On USB hardware without an observable open-host session (HWCDC or a UART
|
||||
bridge), issue the command through that USB client so the request itself proves
|
||||
it is active.
|
||||
|
||||
Append `force` to `off` to permit disconnecting the only active connection.
|
||||
Use USB, another management transport, or reboot to regain access afterward.
|
||||
`force` is accepted only as the final argument to `off`.
|
||||
|
||||
Commands sent through Bluetooth allow its reply to drain before shutdown
|
||||
(normally 250 ms, at most two seconds). `get bluetooth` shows `off pending`
|
||||
during that wait. A normal shutdown is cancelled if the other management
|
||||
connection disappears before it applies; `force` bypasses that check too.
|
||||
|
||||
Bluetooth must be available in the current build and boot transport mode.
|
||||
An exclusive-WiFi boot still requires `set companion.transport ble` and a
|
||||
reboot before Bluetooth can be enabled.
|
||||
|
||||
#### View or change the independent Bluetooth name (Companion)
|
||||
|
||||
**Usage:**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
enum class CompanionBluetoothCommandSource { Other, Terminal, Framed };
|
||||
enum class CompanionBluetoothCommand { None, Get, On, Off, ForceOff, Invalid };
|
||||
|
||||
inline const char* skipBluetoothCommandSpace(const char* text) {
|
||||
while (*text == ' ' || *text == '\t') ++text;
|
||||
return text;
|
||||
}
|
||||
|
||||
inline bool bluetoothCommandWord(const char* text, const char* word) {
|
||||
const size_t length = strlen(word);
|
||||
return strncmp(text, word, length) == 0
|
||||
&& *skipBluetoothCommandSpace(text + length) == 0;
|
||||
}
|
||||
|
||||
inline CompanionBluetoothCommand parseCompanionBluetoothCommand(const char* text) {
|
||||
if (!text) return CompanionBluetoothCommand::None;
|
||||
text = skipBluetoothCommandSpace(text);
|
||||
if (bluetoothCommandWord(text, "get bluetooth")
|
||||
|| bluetoothCommandWord(text, "get ble")) return CompanionBluetoothCommand::Get;
|
||||
const char* value = nullptr;
|
||||
const char* const prefixes[] = {"set bluetooth", "set ble"};
|
||||
for (const char* prefix : prefixes) {
|
||||
const size_t length = strlen(prefix);
|
||||
if (strncmp(text, prefix, length) == 0
|
||||
&& (text[length] == 0 || text[length] == ' ' || text[length] == '\t')) {
|
||||
value = skipBluetoothCommandSpace(text + length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!value) return CompanionBluetoothCommand::None;
|
||||
if (bluetoothCommandWord(value, "on")) return CompanionBluetoothCommand::On;
|
||||
if (bluetoothCommandWord(value, "off")) return CompanionBluetoothCommand::Off;
|
||||
if (strncmp(value, "off", 3) == 0 && (value[3] == ' ' || value[3] == '\t')
|
||||
&& bluetoothCommandWord(skipBluetoothCommandSpace(value + 3), "force")) {
|
||||
return CompanionBluetoothCommand::ForceOff;
|
||||
}
|
||||
return CompanionBluetoothCommand::Invalid;
|
||||
}
|
||||
|
||||
bool handleCompanionBluetoothCommand(
|
||||
const char* command, char* reply, size_t reply_size,
|
||||
CompanionBluetoothCommandSource source = CompanionBluetoothCommandSource::Other);
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include "MyMesh.h"
|
||||
#include "CompanionBluetooth.h"
|
||||
#if defined(MESH_SOAK_DIAGNOSTICS)
|
||||
#include "../../tools/hil/S3SoakDiagnostics.h"
|
||||
#endif
|
||||
@@ -2301,6 +2302,8 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
if (!command || !reply || reply_size == 0) return false;
|
||||
while (*command == ' ') command++;
|
||||
|
||||
if (handleCompanionBluetoothCommand(command, reply, reply_size)) return true;
|
||||
|
||||
if (strcmp(command, "get powersaving") == 0 || strcmp(command, "powersaving") == 0) {
|
||||
snprintf(reply, reply_size, "> %s", _prefs.powersaving_enabled ? "on" : "off");
|
||||
return true;
|
||||
@@ -5331,7 +5334,9 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
char command[MAX_FRAME_SIZE] = {0};
|
||||
memcpy(command, &cmd_frame[1], command_len);
|
||||
char reply[MAX_FRAME_SIZE] = {0};
|
||||
if (!handleLocalControlCommand(command, reply, sizeof(reply))) {
|
||||
if (!handleCompanionBluetoothCommand(command, reply, sizeof(reply),
|
||||
CompanionBluetoothCommandSource::Framed)
|
||||
&& !handleLocalControlCommand(command, reply, sizeof(reply))) {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
} else {
|
||||
out_frame[0] = RESP_CODE_OK;
|
||||
@@ -7492,6 +7497,11 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
#endif
|
||||
|
||||
char local_reply[160];
|
||||
if (handleCompanionBluetoothCommand(command, local_reply, sizeof(local_reply),
|
||||
CompanionBluetoothCommandSource::Terminal)) {
|
||||
terminalOutput().printf(" %s\r\n", local_reply);
|
||||
return;
|
||||
}
|
||||
if (handleDirectCommand(command, local_reply, sizeof(local_reply))) {
|
||||
terminalOutput().printf(" %s\r\n", local_reply);
|
||||
return;
|
||||
@@ -7983,6 +7993,8 @@ void MyMesh::handleTerminalCommand(char* command) {
|
||||
terminalOutput().print(" get bluetooth.name\r\n");
|
||||
terminalOutput().print(" set bluetooth.name <name|default>\r\n");
|
||||
#if defined(BLE_PIN_CODE)
|
||||
terminalOutput().print(" get bluetooth (alias: get ble)\r\n");
|
||||
terminalOutput().print(" set bluetooth <on|off> [force] (this boot; force only with off)\r\n");
|
||||
terminalOutput().print(" get bluetooth.mac\r\n");
|
||||
terminalOutput().print(
|
||||
" set bluetooth.mac <address|random|random-every-boot|random-after-connect|default>\r\n");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <Mesh.h>
|
||||
#include <helpers/BluetoothMac.h>
|
||||
#include "MyMesh.h"
|
||||
#include "CompanionBluetooth.h"
|
||||
#include "CompanionWiFi.h"
|
||||
#if MESH_PACKET_LOGGING
|
||||
#include <helpers/SerialPacketLog.h>
|
||||
@@ -1797,7 +1798,9 @@ void halt() {
|
||||
ota_console_client.print("> ");
|
||||
#else
|
||||
char reply[160]; reply[0] = 0;
|
||||
if (!the_mesh.handleLocalCommand(ota_console_line, reply)
|
||||
if (!handleCompanionBluetoothCommand(ota_console_line, reply, sizeof(reply),
|
||||
CompanionBluetoothCommandSource::Terminal)
|
||||
&& !the_mesh.handleLocalCommand(ota_console_line, reply)
|
||||
&& !mesh::ota::handle_ota_command(ota_console_line, reply, board))
|
||||
strcpy(reply, "Unknown local or OTA command");
|
||||
ota_console_client.print(" -> "); ota_console_client.print(reply); ota_console_client.print("\r\n> ");
|
||||
@@ -2249,6 +2252,118 @@ void halt() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(BLE_PIN_CODE)
|
||||
static uint32_t companion_bluetooth_off_at = 0;
|
||||
static bool companion_bluetooth_force_off = false;
|
||||
|
||||
static bool hasCompanionNonBluetoothClient() {
|
||||
if (!interface_manager.isEnabled()) return false;
|
||||
if (interface_manager.isInterfaceConnected(InterfaceType::WiFi)
|
||||
|| interface_manager.isInterfaceConnected(InterfaceType::Ethernet)) return true;
|
||||
#if COMPANION_FEATURE_TEXT_TERMINAL \
|
||||
&& (COMPANION_FEATURE_NETWORK_TERMINAL || defined(WITH_WEBCONFIG))
|
||||
if (the_mesh.isAnyNetworkTerminalMode()) return true;
|
||||
#endif
|
||||
#if defined(ENABLE_USB_INTERFACE) \
|
||||
&& (defined(NRF52_PLATFORM) || defined(RP2040_PLATFORM) \
|
||||
|| (defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
|
||||
&& (!defined(ARDUINO_USB_MODE) || ARDUINO_USB_MODE == 0)))
|
||||
// Native CDC reports an open host session (including nRF52's DTR-low
|
||||
// MeshCLI session). HWCDC SOFs and UART bridges cannot prove an open app;
|
||||
// those clients count only when they issue this command themselves.
|
||||
#if COMPANION_FEATURE_USB_MOTA_SOURCE
|
||||
if (usb_mota_mode) return false;
|
||||
#endif
|
||||
if (usb_serial_interface.isEnabled() && isUsbTerminalDataConnected()) return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
static void disableCompanionBluetoothForCli() {
|
||||
companion_bluetooth_off_at = 0;
|
||||
the_mesh.cancelSerialOperationsForRoute(&bluetooth_interface);
|
||||
interface_manager.disableBluetooth();
|
||||
interface_manager.forgetReplyRouteForDisconnected(&bluetooth_interface);
|
||||
}
|
||||
|
||||
static void serviceCompanionBluetoothControl() {
|
||||
if (companion_bluetooth_off_at == 0) return;
|
||||
const int32_t elapsed = (int32_t)(millis() - companion_bluetooth_off_at);
|
||||
if (elapsed < 0) return;
|
||||
// The other host may have disconnected while the BLE reply was draining.
|
||||
if (!companion_bluetooth_force_off && !hasCompanionNonBluetoothClient()) {
|
||||
companion_bluetooth_off_at = 0;
|
||||
mesh::usbLoggingPort().println(
|
||||
"Bluetooth off cancelled: no other active management connection");
|
||||
return;
|
||||
}
|
||||
// Give the requesting BLE client its reply, with a bounded wait even when
|
||||
// its notification queue is stuck. The command itself reports a request.
|
||||
if (bluetooth_interface.hasPendingIO() && elapsed < 1750) return;
|
||||
disableCompanionBluetoothForCli();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool handleCompanionBluetoothCommand(const char* command, char* reply,
|
||||
size_t reply_size,
|
||||
CompanionBluetoothCommandSource source) {
|
||||
if (!reply || reply_size == 0) return false;
|
||||
const auto action = parseCompanionBluetoothCommand(command);
|
||||
if (action == CompanionBluetoothCommand::None) return false;
|
||||
if (action == CompanionBluetoothCommand::Invalid) {
|
||||
snprintf(reply, reply_size, "Error: use set bluetooth on|off [force]; force only with off");
|
||||
return true;
|
||||
}
|
||||
#if defined(BLE_PIN_CODE)
|
||||
if (!companion_bluetooth_initialized) {
|
||||
snprintf(reply, reply_size, "Error: Bluetooth unavailable in this boot; check transport mode/startup");
|
||||
return true;
|
||||
}
|
||||
if (action == CompanionBluetoothCommand::Get) {
|
||||
snprintf(reply, reply_size, "bluetooth %s%s",
|
||||
interface_manager.isBluetoothEnabled() ? "on" : "off",
|
||||
companion_bluetooth_off_at ? " (off pending)" : "");
|
||||
return true;
|
||||
}
|
||||
if (action == CompanionBluetoothCommand::On) {
|
||||
companion_bluetooth_off_at = 0;
|
||||
interface_manager.enableBluetooth();
|
||||
snprintf(reply, reply_size, "%s", interface_manager.isBluetoothEnabled()
|
||||
? "OK - Bluetooth on (this boot)" : "Error: Bluetooth enable failed");
|
||||
return true;
|
||||
}
|
||||
if (!interface_manager.isBluetoothEnabled()) {
|
||||
companion_bluetooth_off_at = 0;
|
||||
snprintf(reply, reply_size, "OK - Bluetooth already off (this boot)");
|
||||
return true;
|
||||
}
|
||||
const bool force = action == CompanionBluetoothCommand::ForceOff;
|
||||
BaseSerialInterface* route = interface_manager.captureReplyRoute();
|
||||
const bool non_bluetooth_requester = source == CompanionBluetoothCommandSource::Terminal
|
||||
|| (source == CompanionBluetoothCommandSource::Framed
|
||||
&& route != &bluetooth_interface
|
||||
&& interface_manager.isReplyRouteAvailable(route));
|
||||
if (!force && !non_bluetooth_requester && !hasCompanionNonBluetoothClient()) {
|
||||
snprintf(reply, reply_size,
|
||||
"Error: no other active management connection; use USB/TCP or set bluetooth off force");
|
||||
return true;
|
||||
}
|
||||
if (non_bluetooth_requester) {
|
||||
disableCompanionBluetoothForCli();
|
||||
snprintf(reply, reply_size, "OK - Bluetooth off (this boot)");
|
||||
} else {
|
||||
companion_bluetooth_force_off = force;
|
||||
companion_bluetooth_off_at = millis() + 250;
|
||||
if (companion_bluetooth_off_at == 0) companion_bluetooth_off_at = 1;
|
||||
snprintf(reply, reply_size, "OK - Bluetooth off requested (this boot)");
|
||||
}
|
||||
#else
|
||||
(void)source;
|
||||
snprintf(reply, reply_size, "Error: Bluetooth is not supported in this build");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(ESP32_PLATFORM) && COMPANION_FEATURE_MEMORY_DIAGNOSTICS
|
||||
static void logFullCompanionMemory(const char* stage) {
|
||||
mesh::usbLoggingPort().printf(
|
||||
@@ -2920,5 +3035,6 @@ void loop() {
|
||||
serviceDeferredCompanionBluetooth();
|
||||
#endif
|
||||
serviceCompanionBluetoothIdentity();
|
||||
serviceCompanionBluetoothControl();
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Execute Bluetooth CLI parsing, real transport routing, and shutdown guards."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MAIN = (ROOT / 'examples/companion_radio/main.cpp').read_text()
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <helpers/MultiSerialInterface.h>
|
||||
#include "examples/companion_radio/CompanionBluetooth.h"
|
||||
uint32_t now_ms=100;
|
||||
uint32_t millis(){return now_ms;}
|
||||
bool usb_open=false, usb_power=true, network_terminal=false;
|
||||
bool companion_bluetooth_initialized=true, usb_mota_mode=false;
|
||||
uint32_t companion_bluetooth_off_at=0;
|
||||
bool companion_bluetooth_force_off=false;
|
||||
struct Transport : BaseSerialInterface {
|
||||
bool enabled=false,connected=false,busy=false,frame=false,enable_fails=false;
|
||||
unsigned disabled=0;
|
||||
void enable() override {enabled=!enable_fails;}
|
||||
void disable() override {enabled=false;connected=false;++disabled;}
|
||||
bool isEnabled() const override{return enabled;}
|
||||
bool isConnected() const override{return connected;}
|
||||
bool isReadBusy() const override{return false;}
|
||||
bool isWriteBusy() const override{return busy;}
|
||||
bool hasPendingIO() const override{return busy;}
|
||||
size_t writeFrame(const uint8_t*,size_t n) override{return n;}
|
||||
size_t checkRecvFrame(uint8_t* out) override{
|
||||
if(!frame)return 0; frame=false;out[0]=0x42;return 1;
|
||||
}
|
||||
} bluetooth_interface,usb_serial_interface,wifi_interface,ethernet_interface;
|
||||
MultiSerialInterface interface_manager;
|
||||
bool isUsbTerminalDataConnected(){return usb_open;}
|
||||
unsigned cancelled=0,logs=0;
|
||||
struct {
|
||||
bool isAnyNetworkTerminalMode(){return network_terminal;}
|
||||
void cancelSerialOperationsForRoute(BaseSerialInterface* route){
|
||||
assert(route==&bluetooth_interface);++cancelled;
|
||||
}
|
||||
} the_mesh;
|
||||
namespace mesh {
|
||||
struct Logger {void println(const char*){++logs;}} logger;
|
||||
Logger& usbLoggingPort(){return logger;}
|
||||
}
|
||||
@METHODS@
|
||||
char reply[160];
|
||||
bool command(const char* text,CompanionBluetoothCommandSource source=CompanionBluetoothCommandSource::Framed){
|
||||
memset(reply,0,sizeof(reply));
|
||||
return handleCompanionBluetoothCommand(text,reply,sizeof(reply),source);
|
||||
}
|
||||
void route(Transport& target){
|
||||
target.connected=true;target.frame=true;
|
||||
uint8_t frame[176];assert(interface_manager.checkRecvFrame(frame)==1);
|
||||
assert(interface_manager.captureReplyRoute()==&target);
|
||||
}
|
||||
int main(){
|
||||
#if defined(BLE_PIN_CODE)
|
||||
interface_manager.addInterface(InterfaceType::Bluetooth,&bluetooth_interface);
|
||||
interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface);
|
||||
interface_manager.addInterface(InterfaceType::WiFi,&wifi_interface);
|
||||
interface_manager.addInterface(InterfaceType::Ethernet,ðernet_interface);
|
||||
interface_manager.enable();route(bluetooth_interface);
|
||||
assert(command("get bluetooth")&&strstr(reply,"bluetooth on"));
|
||||
for(const char* invalid : {"set ble","set bluetooth on force","set ble off force extra",
|
||||
"set ble off forced","set bluetooth off junk","set ble yes"}){
|
||||
assert(command(invalid)&&strstr(reply,"Error:"));
|
||||
assert(bluetooth_interface.disabled==0&&companion_bluetooth_off_at==0);
|
||||
}
|
||||
assert(!command("get bluetooth.mac"));
|
||||
assert(!command("set ble.stealth on"));
|
||||
assert(command("set bluetooth off")&&strstr(reply,"no other active"));
|
||||
assert(usb_power&&bluetooth_interface.enabled&&!companion_bluetooth_off_at);
|
||||
// A disabled TCP interface and a socket that is merely listening don't count.
|
||||
wifi_interface.connected=true;wifi_interface.enabled=false;
|
||||
assert(command("set ble off")&&strstr(reply,"Error:"));
|
||||
wifi_interface.enabled=true;wifi_interface.connected=false;
|
||||
assert(command("set ble off")&&strstr(reply,"Error:"));
|
||||
// A real TCP client allows a delayed BLE response. Losing it cancels shutdown.
|
||||
wifi_interface.connected=true;
|
||||
assert(command("set ble off")&&strstr(reply,"requested"));
|
||||
assert(command("get ble")&&strstr(reply,"off pending"));
|
||||
now_ms+=249;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
|
||||
wifi_interface.connected=false;++now_ms;serviceCompanionBluetoothControl();
|
||||
assert(bluetooth_interface.enabled&&!companion_bluetooth_off_at&&logs==1);
|
||||
// Force may disconnect the sole client, but waits for queued notifications.
|
||||
assert(command(" set ble\t off\tforce \t")&&strstr(reply,"requested"));
|
||||
bluetooth_interface.busy=true;now_ms+=250;serviceCompanionBluetoothControl();
|
||||
assert(bluetooth_interface.enabled);
|
||||
now_ms+=1749;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
|
||||
++now_ms;serviceCompanionBluetoothControl();
|
||||
assert(!bluetooth_interface.enabled&&cancelled==1);
|
||||
assert(interface_manager.captureReplyRoute()==nullptr);
|
||||
assert(command("set ble off")&&strstr(reply,"already off"));
|
||||
assert(command("get ble")&&strcmp(reply,"bluetooth off")==0);
|
||||
bluetooth_interface.busy=false;
|
||||
assert(command("set bluetooth on")&&strstr(reply,"OK"));route(bluetooth_interface);
|
||||
// Another native CDC session counts; HWCDC/bridge presence alone doesn't.
|
||||
usb_open=true;usb_serial_interface.connected=true;
|
||||
assert(command("set ble off"));
|
||||
#if defined(NRF52_PLATFORM) || (defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE == 0)
|
||||
assert(strstr(reply,"requested"));
|
||||
usb_open=false;usb_serial_interface.connected=false;
|
||||
now_ms+=250;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
|
||||
#else
|
||||
assert(strstr(reply,"Error:"));usb_open=false;usb_serial_interface.connected=false;
|
||||
#endif
|
||||
// USB MOTA ownership cannot count as a separate management terminal.
|
||||
usb_mota_mode=true;usb_open=true;
|
||||
assert(command("set ble off")&&strstr(reply,"Error:"));
|
||||
usb_mota_mode=false;usb_open=false;
|
||||
// An actual USB command proves that requester is present on every backend.
|
||||
route(usb_serial_interface);
|
||||
assert(command("set ble off")&&strstr(reply,"Bluetooth off (this boot)"));
|
||||
assert(!bluetooth_interface.enabled&&interface_manager.captureReplyRoute()==&usb_serial_interface);
|
||||
assert(command("set ble on")&&bluetooth_interface.enabled);
|
||||
// ASCII terminal commands cannot inherit the previous framed BLE route.
|
||||
route(bluetooth_interface);
|
||||
assert(command("set ble off",CompanionBluetoothCommandSource::Terminal));
|
||||
assert(!bluetooth_interface.enabled);
|
||||
assert(command("set ble on"));route(bluetooth_interface);
|
||||
// Active browser/TCP terminal and Ethernet sessions each qualify.
|
||||
network_terminal=true;
|
||||
assert(command("set ble off")&&strstr(reply,"requested"));
|
||||
assert(command("set ble on")&&!companion_bluetooth_off_at);
|
||||
now_ms+=300;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
|
||||
network_terminal=false;ethernet_interface.connected=true;
|
||||
assert(command("set ble off")&&strstr(reply,"requested"));
|
||||
now_ms+=250;serviceCompanionBluetoothControl();assert(!bluetooth_interface.enabled);
|
||||
assert(command("set ble on"));route(bluetooth_interface);ethernet_interface.connected=false;
|
||||
// Pending deadlines work across millis() wrap, including the zero sentinel.
|
||||
for(uint32_t start : {0xffffff00u,0xffffff06u}){
|
||||
now_ms=start;assert(command("set ble off force"));
|
||||
now_ms=start+249;serviceCompanionBluetoothControl();assert(bluetooth_interface.enabled);
|
||||
now_ms=start+251;serviceCompanionBluetoothControl();assert(!bluetooth_interface.enabled);
|
||||
assert(command("set ble on"));route(bluetooth_interface);
|
||||
}
|
||||
companion_bluetooth_initialized=false;
|
||||
assert(command("set ble on")&&strstr(reply,"unavailable"));
|
||||
assert(command("set ble off force")&&strstr(reply,"unavailable"));
|
||||
companion_bluetooth_initialized=true;
|
||||
assert(command("set ble off",CompanionBluetoothCommandSource::Terminal));
|
||||
bluetooth_interface.enable_fails=true;
|
||||
assert(command("set ble on")&&strstr(reply,"enable failed"));
|
||||
#else
|
||||
assert(command("get ble")&&strstr(reply,"not supported"));
|
||||
assert(command("set ble off force")&&strstr(reply,"not supported"));
|
||||
#endif
|
||||
assert(!handleCompanionBluetoothCommand(nullptr,reply,sizeof(reply)));
|
||||
assert(!handleCompanionBluetoothCommand("get ble",nullptr,sizeof(reply)));
|
||||
assert(!handleCompanionBluetoothCommand("set ble off force",reply,0));
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class BluetoothControlTests(unittest.TestCase):
|
||||
def test_actual_commands_and_connection_guards(self):
|
||||
guards = '\n'.join(method(MAIN, signature) for signature in (
|
||||
'static bool hasCompanionNonBluetoothClient(',
|
||||
'static void disableCompanionBluetoothForCli(',
|
||||
'static void serviceCompanionBluetoothControl('))
|
||||
methods = '#if defined(BLE_PIN_CODE)\n' + guards + '\n#endif\n' + method(
|
||||
MAIN, 'bool handleCompanionBluetoothCommand(')
|
||||
configurations = {
|
||||
'no_ble': [],
|
||||
'nrf52': ['BLE_PIN_CODE=123456', 'NRF52_PLATFORM=1'],
|
||||
'esp32_tinyusb': ['BLE_PIN_CODE=123456', 'ESP32=1',
|
||||
'ARDUINO_USB_CDC_ON_BOOT=1', 'ARDUINO_USB_MODE=0'],
|
||||
'esp32_hwcdc': ['BLE_PIN_CODE=123456', 'ESP32=1',
|
||||
'ARDUINO_USB_CDC_ON_BOOT=1', 'ARDUINO_USB_MODE=1'],
|
||||
'esp32_uart': ['BLE_PIN_CODE=123456', 'ESP32=1'],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
folder = Path(directory)
|
||||
(folder/'Arduino.h').write_text(
|
||||
'#pragma once\n#include <cstdint>\n#include <cstddef>\n#include <initializer_list>\n')
|
||||
source = folder/'bluetooth.cpp'
|
||||
source.write_text(HARNESS.replace('@METHODS@', methods))
|
||||
for name, defines in configurations.items():
|
||||
with self.subTest(platform=name):
|
||||
binary = folder/name
|
||||
built = subprocess.run([
|
||||
os.environ.get('CXX','g++'), '-std=c++17', '-Wall', '-Wextra',
|
||||
'-I',str(folder),'-I',str(ROOT),'-I',str(ROOT/'src'),
|
||||
'-DENABLE_USB_INTERFACE=1', '-DCOMPANION_FEATURE_TEXT_TERMINAL=1',
|
||||
'-DCOMPANION_FEATURE_NETWORK_TERMINAL=1',
|
||||
'-DCOMPANION_FEATURE_USB_MOTA_SOURCE=1',
|
||||
*['-D'+flag for flag in defines],str(source),'-o',str(binary)],
|
||||
capture_output=True,text=True)
|
||||
self.assertEqual(built.returncode,0,built.stderr)
|
||||
ran = subprocess.run([str(binary)],capture_output=True,text=True)
|
||||
self.assertEqual(ran.returncode,0,ran.stderr)
|
||||
|
||||
def test_cli_entry_points_pass_the_current_source(self):
|
||||
mesh = (ROOT/'examples/companion_radio/MyMesh.cpp').read_text()
|
||||
terminal = method(mesh,'void MyMesh::handleTerminalCommand(')
|
||||
framed = method(mesh,'void MyMesh::handleCmdFrame(')
|
||||
local = method(mesh,'bool MyMesh::handleLocalControlCommand(')
|
||||
self.assertIn('CompanionBluetoothCommandSource::Terminal',terminal)
|
||||
self.assertIn('CompanionBluetoothCommandSource::Framed',framed)
|
||||
self.assertIn('handleCompanionBluetoothCommand(command, reply, reply_size)',local)
|
||||
loop = method(MAIN,'\nvoid loop()')
|
||||
self.assertIn('serviceCompanionBluetoothControl();',loop)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user