Support stock serial MQTT bridges on Full Companion
This commit is contained in:
1 parent
ea3fbcb277
commit
eb80ebafbc
11 files changed
+369
-65
No files matched your search
@@ -179,7 +179,7 @@ class CompanionIdentityCliTests(unittest.TestCase):
|
||||
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
||||
terminal = method(source, 'void MyMesh::handleTerminalCommand(')
|
||||
self.assertIn('handleLocalControlCommand(command, local_reply, sizeof(local_reply))', terminal)
|
||||
self.assertIn('terminalOutput().printf(" %s\\r\\n", local_reply);', terminal)
|
||||
self.assertIn('terminalOutput().printf(" -> %s\\r\\n", local_reply);', terminal)
|
||||
self.assertIn('terminalOutput().print(" get public.key\\r\\n");', terminal)
|
||||
shared = method(source, 'bool MyMesh::handleCommand(')
|
||||
self.assertIn('if (handleLocalControlCommand(command, reply, reply_capacity)) return true;', shared)
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Execute saved Companion stream-mode selection and its production USB routes.
|
||||
|
||||
Framework endpoints are native boundaries. Preference I/O, the CLI branches,
|
||||
boot selection, and USB routing/descriptor functions are actual source bodies.
|
||||
The complete primary-CDC transport fixture separately exercises real queues.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_companion_preferences_transaction import HARNESS as STORE_HARNESS
|
||||
import test_companion_usb_logging as logging_tests
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
MODE_HARNESS = r'''
|
||||
#include <atomic>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
struct Endpoint : Stream {
|
||||
unsigned begins=0; std::string output;
|
||||
int available() override{return 0;} int read() override{return -1;}
|
||||
int peek() override{return -1;}
|
||||
size_t write(uint8_t b) override{output+=char(b);return 1;}
|
||||
size_t write(const uint8_t* p,size_t n) override{output.append((const char*)p,n);return n;}
|
||||
void begin(unsigned){++begins;}
|
||||
};
|
||||
struct UsbDevice {unsigned detaches=0,attaches=0;
|
||||
void detach(){++detaches;} void attach(){++attaches;}} TinyUSBDevice;
|
||||
template<class T> T constrain(T value,int low,int high){return T(value<low?low:value>high?high:value);}
|
||||
uint32_t futureMillis(uint32_t value){return value+10;}
|
||||
namespace mesh {
|
||||
static std::atomic<bool> usb_logging_preference_known{false};
|
||||
bool logging=false;
|
||||
bool dedicated_usb_logging_port_started=false,dedicated_usb_logging_port_configured=false;
|
||||
std::atomic<bool> dedicated_usb_logging_port_connected{false};
|
||||
Endpoint dedicated_usb_logging_port,nonblocking_dedicated_usb_logging_port;
|
||||
Endpoint nonblocking_primary_usb_logging_port,null_usb_logging_stream;
|
||||
bool isUsbLoggingEnabled(){return logging;}
|
||||
bool isUsbDebugLoggingEnabled(){return false;}
|
||||
void setPlatformDebugOutputEnabled(bool){}
|
||||
void setUsbDebugEnabled(bool){}
|
||||
void setUsbLoggingEnabled(bool enabled){logging=enabled;usb_logging_preference_known=true;}
|
||||
bool saveUsbLoggingBootPreference(bool){return true;}
|
||||
@USB_FUNCTIONS@
|
||||
void freshBoot(){
|
||||
dedicated_usb_logging_port_started=false;dedicated_usb_logging_port_configured=false;
|
||||
dedicated_usb_logging_port_connected=false;dedicated_usb_logging_port.begins=0;
|
||||
TinyUSBDevice.detaches=0;TinyUSBDevice.attaches=0;
|
||||
usb_logging_preference_known=false;logging=false;configureUsbLoggingPacketStream(false);
|
||||
}
|
||||
}
|
||||
struct {double node_lat=47.1,node_lon=-122.2;} sensors;
|
||||
bool usb_logging_reply_hold=false,usb_logging_reply_staged=false,usb_logging_reply_state=false;
|
||||
bool usb_logging_reply_pending=false,usb_protocol_initialized=true;
|
||||
unsigned ownership_checks=0;
|
||||
void serviceUsbLoggingOwnership(bool){++ownership_checks;}
|
||||
struct MyMesh {
|
||||
CompanionNodePrefs _prefs;DataStore store;DataStore* _store=&store;
|
||||
uint32_t _scheduled_reboot_at=0;
|
||||
@SAVE_PREFS@
|
||||
@APPLY@
|
||||
bool command(const char* command,char* reply,size_t reply_size){
|
||||
@COMMANDS@
|
||||
return false;
|
||||
}
|
||||
void boot(){
|
||||
@NORMALIZE@
|
||||
@BOOT@
|
||||
mesh::beginUsbLoggingPort();
|
||||
}
|
||||
};
|
||||
static void bootMode(MyMesh& node,uint8_t expected){
|
||||
DataStore reboot;reboot.fs=node.store.fs;
|
||||
CompanionNodePrefs saved;double lat=0,lon=0;
|
||||
assert(reboot.loadPrefs(saved,lat,lon));assert(saved.usb_logging_enabled==expected);
|
||||
node._prefs=saved;mesh::freshBoot();node.boot();
|
||||
assert(node._prefs.usb_logging_enabled==expected);
|
||||
assert(mesh::isUsbLoggingPacketStream()==(expected==2));
|
||||
assert(mesh::isUsbLoggingEnabled()==(expected!=0));
|
||||
assert(!mesh::usbLoggingInterfaceRestartRequired());
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
assert(mesh::dedicated_usb_logging_port_configured==(expected==1));
|
||||
assert(mesh::dedicated_usb_logging_port.begins==(expected==1));
|
||||
assert(TinyUSBDevice.detaches==(expected==1)&&TinyUSBDevice.attaches==(expected==1));
|
||||
#else
|
||||
assert(!mesh::dedicated_usb_logging_port_configured);
|
||||
#endif
|
||||
if(expected==2){
|
||||
assert(!mesh::hasDedicatedUsbLoggingPort());
|
||||
assert(&mesh::usbLoggingPort()==&mesh::nonblocking_primary_usb_logging_port);
|
||||
assert(strstr(mesh::usbLoggingPortDescription(),"primary"));
|
||||
}else if(expected==0){assert(&mesh::usbLoggingPort()==&mesh::null_usb_logging_stream);}
|
||||
else {
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
assert(mesh::hasDedicatedUsbLoggingPort());
|
||||
assert(&mesh::usbLoggingPort()==&mesh::null_usb_logging_stream);
|
||||
mesh::dedicated_usb_logging_port_connected=true;
|
||||
assert(&mesh::usbLoggingPort()==&mesh::nonblocking_dedicated_usb_logging_port);
|
||||
#else
|
||||
assert(&mesh::usbLoggingPort()==&mesh::nonblocking_primary_usb_logging_port);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
int main(){
|
||||
MyMesh node;strcpy(node._prefs.node_name,"stable owned identity");
|
||||
node._prefs.freq=915.125f;node._prefs.ble_pin=246810;node._prefs.bluetooth_enabled=1;
|
||||
char reply[160]={};
|
||||
for(uint8_t mode : {0,1,2}){
|
||||
node._prefs.usb_logging_enabled=mode;assert(node.savePrefs());
|
||||
assert(node.store.fs.files["/new_prefs"][158]==mode);
|
||||
bootMode(node,mode);
|
||||
assert(!strcmp(node._prefs.node_name,"stable owned identity"));
|
||||
assert(node._prefs.freq==915.125f&&node._prefs.ble_pin==246810);
|
||||
}
|
||||
node._prefs.usb_logging_enabled=1;assert(node.savePrefs());bootMode(node,1);
|
||||
const auto original=node.store.fs.files["/new_prefs"];
|
||||
const unsigned attached=TinyUSBDevice.attaches,detached=TinyUSBDevice.detaches;
|
||||
for(const char* invalid : {"set usb.logging stream extra","set usb.logging STREAM",
|
||||
"set usb.logging stream ","set usb.logging stream reboot ","set usb.loggingstream"}){
|
||||
const auto before=node.store.fs.files["/new_prefs"];
|
||||
const bool handled=node.command(invalid,reply,sizeof(reply));
|
||||
if(handled)assert(strstr(reply,"Error:"));
|
||||
assert(node.store.fs.files["/new_prefs"]==before&&node._prefs.usb_logging_enabled==1);
|
||||
}
|
||||
assert(node.command("set usb.logging stream",reply,sizeof(reply)));
|
||||
assert(node._prefs.usb_logging_enabled==2&&node.store.fs.files["/new_prefs"][158]==2);
|
||||
assert(strstr(reply,"reboot required"));assert(node._scheduled_reboot_at==0);
|
||||
assert(!mesh::isUsbLoggingPacketStream()); // Saved choice cannot flip a live descriptor/route.
|
||||
assert(TinyUSBDevice.attaches==attached&&TinyUSBDevice.detaches==detached);
|
||||
const auto stream=node.store.fs.files["/new_prefs"];
|
||||
for(size_t i=0;i<stream.size();++i)if(i!=158)assert(stream[i]==original[i]);
|
||||
assert(node.command("get usb.logging",reply,sizeof(reply))&&strstr(reply,"reboot required"));
|
||||
assert(node.command("set usb.logging stream reboot",reply,sizeof(reply)));
|
||||
assert(node._scheduled_reboot_at==futureMillis(1000));
|
||||
assert(!mesh::isUsbLoggingPacketStream());
|
||||
bootMode(node,2);node._scheduled_reboot_at=0;
|
||||
assert(node.command("get usb.logging",reply,sizeof(reply)));
|
||||
assert(strstr(reply,"usb.logging stream")&&strstr(reply,"primary")&&!strstr(reply,"reboot required"));
|
||||
assert(node.command("set usb.logging stream reboot",reply,sizeof(reply)));
|
||||
assert(node._scheduled_reboot_at==0); // Identical boot selection does not reboot.
|
||||
assert(node.command("set usb.logging on",reply,sizeof(reply)));
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
assert(strstr(reply,"reboot required"));
|
||||
#endif
|
||||
assert(mesh::isUsbLoggingPacketStream()); // Mode remains latched until boot.
|
||||
bootMode(node,1);node._scheduled_reboot_at=0;
|
||||
assert(node.command("set usb.logging stream",reply,sizeof(reply)));
|
||||
node.store.fs.fail_write=true;
|
||||
assert(node.command("set usb.logging off",reply,sizeof(reply)));
|
||||
assert(strstr(reply,"save failed")&&node.store.fs.files["/new_prefs"][158]==2);
|
||||
assert(!mesh::isUsbLoggingPacketStream());
|
||||
char guarded[17];memset(guarded,'X',sizeof(guarded));
|
||||
assert(node.command("get usb.logging",guarded,16));assert(guarded[15]==0&&guarded[16]=='X');
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class CompanionPacketStreamTests(unittest.TestCase):
|
||||
def test_actual_preferences_cli_boot_descriptor_and_port(self):
|
||||
store = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text()
|
||||
mesh = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
||||
header = (ROOT / 'examples/companion_radio/MyMesh.h').read_text()
|
||||
usb = (ROOT / 'src/helpers/UsbLogging.cpp').read_text()
|
||||
main = (ROOT / 'examples/companion_radio/main.cpp').read_text()
|
||||
methods = '\n'.join(method(store, sig) for sig in (
|
||||
'bool DataStore::loadPrefs(', 'bool DataStore::loadPrefsInt(',
|
||||
'bool DataStore::savePrefs('))
|
||||
prefix = STORE_HARNESS.split('int main(')[0].replace('@METHODS@', methods)
|
||||
start = usb.index('static std::atomic<bool> usb_logging_packet_stream')
|
||||
end = usb.index('\nvoid setUsbCompanionTxBufferCapacity', start)
|
||||
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()'))
|
||||
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;')
|
||||
boot_end = mesh.index('\n#endif', boot_start)
|
||||
normalize = next(line for line in mesh.splitlines()
|
||||
if '_prefs.usb_logging_enabled = constrain(' in line)
|
||||
code = MODE_HARNESS.replace('@USB_FUNCTIONS@', functions)
|
||||
code = code.replace('@COMMANDS@', mesh[start:end])
|
||||
code = code.replace('@SAVE_PREFS@', method(header, ' bool savePrefs()'))
|
||||
code = code.replace('@APPLY@', method(main, 'void MyMesh::applyUsbLoggingState(')
|
||||
.replace('void MyMesh::applyUsbLoggingState(', 'void applyUsbLoggingState('))
|
||||
code = code.replace('@BOOT@', mesh[boot_start:boot_end]).replace('@NORMALIZE@', normalize)
|
||||
helper = logging_tests.CompanionUsbLoggingTests()
|
||||
with tempfile.TemporaryDirectory(prefix='companion-packet-stream-') as directory:
|
||||
work = Path(directory)
|
||||
helper.prepare(work)
|
||||
for dual in (False, True):
|
||||
with self.subTest(dual=dual):
|
||||
helper.compile_and_run(work, prefix + code,
|
||||
['NRF52_PLATFORM=1', 'MESH_USB_LOGGING_AVAILABLE=1']
|
||||
+ (['MESH_DUAL_CDC_LOGGING=1'] if dual else []),
|
||||
('ConfigSerializer.cpp', 'DynamicConfigSerializer.cpp',
|
||||
'CommonRadioPrefs.cpp', 'TxtDataHelpers.cpp'))
|
||||
|
||||
def test_actual_full_dual_compiled_primary_transport(self):
|
||||
fixture = ROOT / 'test/fixtures/nrf52_usb_console'
|
||||
with tempfile.TemporaryDirectory(prefix='companion-stream-transport-') as directory:
|
||||
work = Path(directory)
|
||||
# The full implementation uses existing native endpoint boundaries;
|
||||
# only add the optional second-CDC class needed by this profile.
|
||||
arduino = (fixture / 'mocks/Arduino.h').read_text().replace(
|
||||
'"../../../mocks/Arduino.h"', '"' + str(ROOT / 'test/mocks/Arduino.h') + '"')
|
||||
(work / 'Arduino.h').write_text(arduino)
|
||||
tiny = (fixture / 'mocks/Adafruit_TinyUSB.h').read_text().replace(
|
||||
'#define CFG_TUD_CDC 1', '#define CFG_TUD_CDC 2').replace(
|
||||
'#define CFG_TUD_CDC_TX_BUFSIZE 64', '#define CFG_TUD_CDC_TX_BUFSIZE 128').replace(
|
||||
'uint32_t tud_cdc_n_write_flush(uint8_t);',
|
||||
'extern "C" uint32_t tud_cdc_n_write_flush(uint8_t);')
|
||||
tiny += '''
|
||||
void tud_sof_cb_enable(bool);
|
||||
uint32_t tud_cdc_n_read(uint8_t,void*,uint32_t);
|
||||
class Adafruit_USBD_CDC : public Stream {
|
||||
public:
|
||||
virtual uint16_t getInterfaceDescriptor(uint8_t,uint8_t*,uint16_t){return 0;}
|
||||
void setStringDescriptor(const char*){} void begin(unsigned){} bool dtr(){return false;}
|
||||
int available() override{return 0;} int read() override{return -1;} int peek() override{return -1;}
|
||||
int availableForWrite() override{return 128;}
|
||||
size_t write(uint8_t) override{assert(false);return 0;}
|
||||
size_t write(const uint8_t*,size_t) override{assert(false);return 0;}
|
||||
};
|
||||
'''
|
||||
(work / 'Adafruit_TinyUSB.h').write_text(tiny)
|
||||
source = (fixture / 'test_console.cpp').read_text()
|
||||
source = source.replace('int main() {',
|
||||
'void tud_sof_cb_enable(bool){}\n'
|
||||
'uint32_t tud_cdc_n_read(uint8_t n,void*,uint32_t){assert(n==1);return 0;}\n'
|
||||
'int main() {', 1)
|
||||
# Run the unchanged comprehensive queue/session fixture on the
|
||||
# dual-capable build, selecting primary stream before enable.
|
||||
source = source.replace(' mesh::setUsbLoggingEnabled(true);',
|
||||
' mesh::configureUsbLoggingPacketStream(true);\n'
|
||||
' assert(!mesh::hasDedicatedUsbLoggingPort());\n'
|
||||
' mesh::setUsbLoggingEnabled(true);', 1)
|
||||
(work / 'test.cpp').write_text(source)
|
||||
binary = work / 'test'
|
||||
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie',
|
||||
'-DARDUINO', '-DNRF52_PLATFORM', '-DUSE_TINYUSB', '-DENABLE_USB_INTERFACE',
|
||||
'-DMESH_DEBUG=1', '-DMESH_DUAL_CDC_LOGGING=1',
|
||||
'-DCOMPANION_FEATURE_DEDICATED_USB_LOGGING=1',
|
||||
'-I' + str(work), '-I' + str(fixture / 'mocks'), '-I' + str(ROOT / 'src'),
|
||||
str(work / 'test.cpp'), *[str(ROOT / 'src/helpers' / name) for name in (
|
||||
'UsbLogging.cpp', 'UsbLoggingClientActivity.cpp', 'UsbLoggingLineStateOverride.cpp')],
|
||||
'-o', str(binary)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
|
||||
self.assertEqual(ran.returncode, 0, ran.stderr)
|
||||
self.assertIn('native nRF52 console transport passed', ran.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -83,6 +83,7 @@ void discardUsbTerminalOutput() {}
|
||||
Stream& usbCompanionPort() { return stream; }
|
||||
bool saveUsbLoggingBootPreference(bool) { return true; }
|
||||
bool usbLoggingInterfaceRestartRequired() { return false; }
|
||||
bool isUsbLoggingPacketStream() { return false; }
|
||||
}
|
||||
namespace CompanionMqttSetupPortal {
|
||||
static bool saveEnabled(bool) { return true; }
|
||||
|
||||
Reference in new issue
Block a user