Keep BLE contact sync from blocking other companion transports

This commit is contained in:
mikecarper committed 2026-10-04 02:11:18 -07:00
1 parent c316e9fb3c
commit 638e8cf3b7
11 files changed
+347 -53

No files matched your search

+29 -17
View File
@@ -369,7 +369,8 @@ void MyMesh::writeDisabledFrame() {
_serial->writeFrame(buf, 1);
}
bool MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
bool MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact,
BaseSerialInterface* route) {
int i = 0;
out_frame[i++] = code;
memcpy(&out_frame[i], contact.id.pub_key, PUB_KEY_SIZE);
@@ -389,7 +390,8 @@ bool MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) {
i += 4;
memcpy(&out_frame[i], &contact.lastmod, 4);
i += 4;
return _serial->writeFrame(out_frame, i) == (size_t)i;
return (route != NULL ? _serial->writeFrameToRoute(route, out_frame, i)
: _serial->writeFrame(out_frame, i)) == (size_t)i;
}
void MyMesh::stopContactsIterator() {
@@ -399,7 +401,7 @@ void MyMesh::stopContactsIterator() {
_iter_start_pending = false;
_iter_contact_pending = false;
_iter_next_frame_at = 0;
if (_serial != NULL) _serial->unlockReplyRoute();
_iter_reply_route = NULL;
}
static constexpr int CONTACT_UPDATE_FRAME_MIN_LEN =
@@ -2271,6 +2273,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_serial(NULL), _mota_source_control(NULL),
telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_iter_started = false;
_iter_reply_route = NULL;
_iter_start_pending = false;
_iter_contact_pending = false;
_iter_next_frame_at = 0;
@@ -5181,8 +5184,8 @@ void MyMesh::serviceSigningSession() {
}
}
void MyMesh::cancelSerialResponseStream() {
stopContactsIterator();
void MyMesh::cancelSerialResponseStream(BaseSerialInterface* route) {
if (route == NULL || _iter_reply_route == route) stopContactsIterator();
}
void MyMesh::cancelSerialOperationsForRoute(BaseSerialInterface* route) {
@@ -5268,7 +5271,7 @@ void MyMesh::handleCmdFrame(size_t len) {
MESH_DEBUG_PRINTLN("App %s connected", app_name);
BaseSerialInterface* app_route = _serial->captureReplyRoute();
stopContactsIterator(); // stop any left-over ContactsIterator
cancelSerialResponseStream(app_route);
cancelSerialOperationsForRoute(app_route);
int i = 0;
out_frame[i++] = RESP_CODE_SELF_INFO;
@@ -5535,9 +5538,10 @@ void MyMesh::handleCmdFrame(size_t len) {
_iter_filter_since = 0;
}
// CONTACTS_START, every CONTACT, and END_OF_CONTACTS are one response
// transaction. Keep them on the transport which requested the list.
_serial->lockReplyRoute();
// Retain this stream's destination independently of later commands.
// BLE's paced contact sync must not hold USB/WiFi command polling until
// the entire list finishes, or redirect their replies to the BLE app.
_iter_reply_route = _serial->captureReplyRoute();
_iter = startContactsIterator();
_iter_started = true;
_iter_start_pending = true;
@@ -5550,7 +5554,7 @@ void MyMesh::handleCmdFrame(size_t len) {
uint8_t reply[5];
reply[0] = RESP_CODE_CONTACTS_START;
memcpy(&reply[1], &_iter_total_count, 4); // total, NOT filtered count
if (_serial->writeFrame(reply, 5) == 5) {
if (_serial->writeFrameToRoute(_iter_reply_route, reply, 5) == 5) {
_iter_start_pending = false;
_iter_next_frame_at = futureMillis(CONTACT_STREAM_FRAME_INTERVAL_MS);
}
@@ -10123,6 +10127,9 @@ void MyMesh::checkCLIRescueCmd() {
void MyMesh::checkSerialInterface() {
size_t len = _serial->checkRecvFrame(cmd_frame);
if (_iter_started && !_serial->isReplyRouteAvailable(_iter_reply_route)) {
stopContactsIterator();
}
if (!_serial->isConnected()) {
stopContactsIterator();
return;
@@ -10130,11 +10137,12 @@ void MyMesh::checkSerialInterface() {
if (len > 0) {
handleCmdFrame(len);
} else if (_iter_started // check if our ContactsIterator is 'running'
}
if (_iter_started // check if our ContactsIterator is 'running'
&& (_iter_next_frame_at == 0
|| _iter_next_frame_at == _ms->getMillis()
|| millisHasNowPassed(_iter_next_frame_at))
&& !_serial->isWriteBusy() // don't spam the Serial Interface too quickly!
&& !_serial->isReplyRouteWriteBusy(_iter_reply_route)
) {
if (_iter_table_revision != getContactTableRevision()) {
// The paced stream walks the live compacting table. If an advert, app
@@ -10154,7 +10162,8 @@ void MyMesh::checkSerialInterface() {
uint8_t reply[5];
reply[0] = RESP_CODE_CONTACTS_START;
memcpy(&reply[1], &_iter_total_count, 4);
if (_serial->writeFrame(reply, sizeof(reply)) == sizeof(reply)) {
if (_serial->writeFrameToRoute(_iter_reply_route, reply, sizeof(reply))
== sizeof(reply)) {
_iter_start_pending = false;
_iter_next_frame_at = futureMillis(CONTACT_STREAM_FRAME_INTERVAL_MS);
}
@@ -10171,7 +10180,7 @@ void MyMesh::checkSerialInterface() {
out_frame[0] = RESP_CODE_END_OF_CONTACTS;
memcpy(&out_frame[1], &_most_recent_lastmod,
4); // include the most recent lastmod, so app can update their 'since'
if (_serial->writeFrame(out_frame, 5) == 5) {
if (_serial->writeFrameToRoute(_iter_reply_route, out_frame, 5) == 5) {
stopContactsIterator();
}
return;
@@ -10181,7 +10190,8 @@ void MyMesh::checkSerialInterface() {
// Do not advance past a contact until its complete frame has been accepted
// by the selected transport. A transiently full/lost route must not turn a
// successful CONTACTS_START/END transaction into a silently short list.
if (writeContactRespFrame(RESP_CODE_CONTACT, _iter_pending_contact)) {
if (writeContactRespFrame(RESP_CODE_CONTACT, _iter_pending_contact,
_iter_reply_route)) {
if (_iter_pending_contact.lastmod > _most_recent_lastmod) {
_most_recent_lastmod = _iter_pending_contact.lastmod;
}
@@ -10346,7 +10356,8 @@ bool MyMesh::canRecoverUsbLogging() const {
&& !_terminal_trace_pending
#endif
&& !(_serial && _serial->hasPendingIO())
&& !(_iter_started && _serial && _serial->isConnected());
&& !(_iter_started && _serial
&& _serial->isReplyRouteAvailable(_iter_reply_route));
}
bool MyMesh::hasPendingWork() const {
@@ -10370,7 +10381,8 @@ bool MyMesh::hasPendingWork() const {
const bool contact_write_needs_polling = isContactWriteDue();
#endif
return (_serial != NULL && _serial->hasPendingIO())
|| (_iter_started && _serial != NULL && _serial->isConnected())
|| (_iter_started && _serial != NULL
&& _serial->isReplyRouteAvailable(_iter_reply_route))
|| hasPendingOtaApply()
|| command_radio_apply_pending
|| _scheduled_reboot_at != 0
+4 -2
View File
@@ -188,7 +188,7 @@ public:
void activateRadio();
bool isRadioReady() const { return _radio_available; }
void startInterface(BaseSerialInterface &serial);
void cancelSerialResponseStream();
void cancelSerialResponseStream(BaseSerialInterface* route = NULL);
void cancelSerialOperationsForRoute(BaseSerialInterface* route);
bool hasFiniteDelayedReplyForRoute(BaseSerialInterface* route) const;
void resetUsbHostSessionInput();
@@ -481,7 +481,8 @@ private:
BaseSerialInterface* route = nullptr);
size_t writePendingSerialFrame(const uint8_t frame[], size_t len, uint32_t now);
void writeDisabledFrame();
bool writeContactRespFrame(uint8_t code, const ContactInfo &contact);
bool writeContactRespFrame(uint8_t code, const ContactInfo &contact,
BaseSerialInterface* route = NULL);
void stopContactsIterator();
bool updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len);
bool addToOfflineQueue(const uint8_t frame[], int len);
@@ -650,6 +651,7 @@ private:
AbstractUITask* _ui;
ContactsIterator _iter;
BaseSerialInterface* _iter_reply_route;
ContactInfo _iter_pending_contact;
uint32_t _iter_filter_since;
uint32_t _iter_next_frame_at;
+7 -15
View File
@@ -628,9 +628,7 @@ static void serviceCompanionPowerSaving(bool force = false) {
#if defined(ETHERNET_ENABLED)
static void cancelCompanionEthernetSession(void*) {
if (interface_manager.isReplyRouteFor(&ethernet_interface)) {
the_mesh.cancelSerialResponseStream();
}
the_mesh.cancelSerialResponseStream(&ethernet_interface);
the_mesh.cancelSerialOperationsForRoute(&ethernet_interface);
interface_manager.forgetReplyRouteForDisconnected(&ethernet_interface);
}
@@ -777,12 +775,9 @@ static void cancelUsbSerialOperations() {
// A cancelled reply must not enable diagnostics for the next USB host.
usb_logging_reply_pending = false;
#endif
// Contact enumeration uses the manager's pinned streaming route; delayed
// single replies capture their own route inside MyMesh. Cancel only USB's
// ownership so a simultaneous BLE/WiFi operation keeps running.
if (interface_manager.isReplyRouteFor(&usb_serial_interface)) {
the_mesh.cancelSerialResponseStream();
}
// Contact streams and delayed replies capture their own destinations.
// Cancel only USB's ownership so simultaneous BLE/WiFi work keeps running.
the_mesh.cancelSerialResponseStream(&usb_serial_interface);
the_mesh.cancelSerialOperationsForRoute(&usb_serial_interface);
interface_manager.forgetReplyRouteForDisconnected(&usb_serial_interface);
}
@@ -2308,9 +2303,7 @@ void halt() {
}
static void cancelCompanionWiFiSession(void*) {
if (interface_manager.isReplyRouteFor(&wifi_interface)) {
the_mesh.cancelSerialResponseStream();
}
the_mesh.cancelSerialResponseStream(&wifi_interface);
the_mesh.cancelSerialOperationsForRoute(&wifi_interface);
interface_manager.forgetReplyRouteForDisconnected(&wifi_interface);
}
@@ -2753,6 +2746,7 @@ static bool hasCompanionNonBluetoothClient() {
static void disableCompanionBluetoothForCli() {
companion_bluetooth_off_at = 0;
the_mesh.cancelSerialResponseStream(&bluetooth_interface);
the_mesh.cancelSerialOperationsForRoute(&bluetooth_interface);
interface_manager.disableBluetooth();
interface_manager.forgetReplyRouteForDisconnected(&bluetooth_interface);
@@ -3422,9 +3416,7 @@ void setup() {
// add usb interface
#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)
wifi_interface.setSessionChangedCallback([](void*) {
if (interface_manager.isReplyRouteFor(&wifi_interface)) {
the_mesh.cancelSerialResponseStream();
}
the_mesh.cancelSerialResponseStream(&wifi_interface);
the_mesh.cancelSerialOperationsForRoute(&wifi_interface);
interface_manager.forgetReplyRouteForDisconnected(&wifi_interface);
}, nullptr);
+1
View File
@@ -51,6 +51,7 @@ struct {
bluetooth_preference=durable_bluetooth_preference=enabled;return true;
}
bool isAnyNetworkTerminalMode(){return network_terminal;}
void cancelSerialResponseStream(BaseSerialInterface*) {}
void cancelSerialOperationsForRoute(BaseSerialInterface* route){
assert(route==&bluetooth_interface);++cancelled;
}
@@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""Run production contact streaming with interleaved USB and paced BLE clients."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <deque>
#include <limits>
#include <vector>
#include "helpers/ArduinoSerialInterface.h"
#include "helpers/MultiSerialInterface.h"
#define PUB_KEY_SIZE 32
#define MAX_PATH_SIZE 64
template<class... Args> static void ignoreDebug(Args&&...) {}
#define MESH_DEBUG_PRINTLN(...) ignoreDebug(__VA_ARGS__)
namespace StrHelper {
static void strzcpy(void* out,const char* in,size_t size) {
memset(out,0,size);memcpy(out,in,std::min(size,strlen(in)));
}
}
@CONSTANTS@
@BUFFER@
struct ContactInfo {
struct { uint8_t pub_key[32]{}; } id;
uint8_t type=1, flags=0, out_path_len=0;
char name[32]{};
uint32_t last_advert_timestamp=0, gps_lat=0, gps_lon=0, lastmod=1;
bool copyPathTo(uint8_t* out) const { memset(out,0,MAX_PATH_SIZE); return true; }
};
struct ContactsIterator {
unsigned next=0, count=0;
template<class T> bool hasNext(T*,ContactInfo& c) {
if(next==count) return false;
c=ContactInfo(); c.id.pub_key[0]=static_cast<uint8_t>(++next);
snprintf(c.name,sizeof(c.name),"HIL-%u",next);
c.lastmod=next; return true;
}
};
struct Ble : BaseSerialInterface {
bool enabled=false, connected=true;
unsigned last_write=0, capacity=4;
std::deque<std::vector<uint8_t>> input, pending;
std::vector<std::vector<uint8_t>> delivered;
void enable() override { enabled=true; }
void disable() override { enabled=false; }
bool isEnabled() const override { return enabled; }
bool isConnected() const override { return connected; }
bool isReadBusy() const override { return !input.empty(); }
bool isWriteBusy() const override { return millis()-last_write<60; }
size_t writeFrame(const uint8_t* src,size_t n) override {
if(!enabled||!connected||pending.size()>=capacity) return 0;
pending.emplace_back(src,src+n); return n;
}
size_t checkRecvFrame(uint8_t* dest) override {
if(connected&&!pending.empty()&&!isWriteBusy()) {
delivered.push_back(pending.front()); pending.pop_front(); last_write=millis();
}
if(!connected||input.empty()) return 0;
auto frame=input.front(); input.pop_front();
memcpy(dest,frame.data(),frame.size()); return frame.size();
}
};
struct MyMesh {
BaseSerialInterface* _serial=nullptr;
struct Clock { unsigned getMillis() const {return millis();} } clock, *_ms=&clock;
struct Store { bool hasIncompleteContactLoad() const {return false;} } store, *_store=&store;
ContactsIterator _iter;
BaseSerialInterface* _iter_reply_route=nullptr;
ContactInfo _iter_pending_contact;
uint32_t _iter_filter_since=0, _iter_next_frame_at=0, _iter_total_count=0;
uint32_t _iter_table_revision=0, _most_recent_lastmod=0;
bool _iter_started=false, _iter_start_pending=false, _iter_contact_pending=false;
unsigned contacts=54, revision=1;
uint8_t out_frame[MAX_FRAME_SIZE+4]{},cmd_frame[MAX_FRAME_SIZE+1]{};
bool millisHasNowPassed(uint32_t time) const {return static_cast<int32_t>(time-millis())<0;}
uint32_t futureMillis(uint32_t delta) const {return millis()+delta;}
uint32_t getContactTableRevision() const {return revision;}
unsigned getNumContacts() const {return contacts;}
ContactsIterator startContactsIterator() {return {0,contacts};}
void writeErrFrame(uint8_t error) {
uint8_t reply[]={RESP_CODE_ERR,error}; assert(_serial->writeFrame(reply,2)==2);
}
void cancelSerialOperationsForRoute(BaseSerialInterface*) {}
void stopContactsIterator();
void cancelSerialResponseStream(BaseSerialInterface* route=nullptr);
bool writeContactRespFrame(uint8_t,const ContactInfo&,BaseSerialInterface* route=nullptr);
void checkSerialInterface();
void handleCmdFrame(size_t len) {
@GET_CONTACTS@
else if(cmd_frame[0]==CMD_APP_START&&len>=8) {
@APP_BEGIN@
memset(out_frame,0xA5,70);out_frame[0]=RESP_CODE_SELF_INFO;
assert(_serial->writeFrame(out_frame,70)==70);
} else {
assert(cmd_frame[0]==CMD_GET_BATT_AND_STORAGE);
memset(out_frame,0x5A,11);out_frame[0]=RESP_CODE_BATT_AND_STORAGE;
assert(_serial->writeFrame(out_frame,11)==11);
}
}
};
@METHODS@
struct Fixture {
BufferStream stream;
ArduinoSerialInterface usb;
Ble ble;
MultiSerialInterface manager;
MyMesh mesh;
bool drain_usb=true;
Fixture() {
g_mock_millis=100;
usb.begin(stream); usb.enableFlowControl(true);
assert(manager.addInterface(InterfaceType::Bluetooth,&ble));
assert(manager.addInterface(InterfaceType::USB,&usb)); manager.enable();
mesh._serial=&manager;
}
void step() {
if(drain_usb) stream.write_capacity=4096;
mesh.checkSerialInterface(); manager.loop(); ++g_mock_millis;
}
void run(unsigned count) {while(count--) step();}
void sendUsb(std::initializer_list<uint8_t> payload) {
const uint8_t header[]={'<',static_cast<uint8_t>(payload.size()),0};
stream.push(header,3); stream.push(payload.begin(),payload.size());
}
std::vector<std::vector<uint8_t>> usbFrames() const {
std::vector<std::vector<uint8_t>> frames;
for(size_t pos=0;pos<stream.output.size();) {
assert(stream.output[pos]=='>'&&pos+3<=stream.output.size());
size_t n=stream.output[pos+1]|(stream.output[pos+2]<<8);
assert(n>0&&pos+n+3<=stream.output.size());
frames.emplace_back(stream.output.begin()+pos+3,stream.output.begin()+pos+3+n);
pos+=n+3;
}
return frames;
}
void startBle() {ble.input.push_back({CMD_GET_CONTACTS});step();assert(mesh._iter_started);}
void finish() {run(6000);assert(!mesh._iter_started);}
};
static unsigned count(const std::vector<std::vector<uint8_t>>& frames,uint8_t type) {
return std::count_if(frames.begin(),frames.end(),[type](const auto& f){return f[0]==type;});
}
static void assertBleSnapshot(const Fixture& f,unsigned contacts=54) {
assert(count(f.ble.delivered,RESP_CODE_CONTACTS_START)==1);
assert(count(f.ble.delivered,RESP_CODE_CONTACT)==contacts);
assert(count(f.ble.delivered,RESP_CODE_END_OF_CONTACTS)==1);
for(unsigned i=1;i<=contacts;++i) {
auto at=std::find_if(f.ble.delivered.begin(),f.ble.delivered.end(),[i](const auto& v){return v[0]==RESP_CODE_CONTACT&&v[1]==i;});
assert(at!=f.ble.delivered.end());
}
}
int main() {
// Fifty-four BLE contacts need over three seconds, but each independent USB
// request is handled immediately, even when it arrives on every loop pass.
{
Fixture f;f.startBle();unsigned queries=0,apps=0;
for(unsigned now=0;now<4000;++now) {
const bool app=now%97==96;
if(app) {
f.mesh.cancelSerialResponseStream(&f.usb);
f.manager.forgetReplyRouteForDisconnected(&f.usb);f.usb.resetSessionState();
f.sendUsb({CMD_APP_START,0,0,0,0,0,0,0});++apps;
} else {f.sendUsb({CMD_GET_BATT_AND_STORAGE});++queries;}
f.step();assert(f.usbFrames().size()==queries+apps);
}
f.finish();assertBleSnapshot(f);
assert(count(f.usbFrames(),RESP_CODE_CONTACT)==0);
assert(count(f.usbFrames(),RESP_CODE_CONTACTS_START)==0);
assert(count(f.usbFrames(),RESP_CODE_SELF_INFO)==apps);
assert(count(f.usbFrames(),RESP_CODE_BATT_AND_STORAGE)==queries);
for(const auto& response:f.usbFrames()) {
assert(response.size()==(response[0]==RESP_CODE_SELF_INFO?70u:11u));
assert(response.back()==(response[0]==RESP_CODE_SELF_INFO?0xA5:0x5A));
}
}
// A second client cannot replace the sole contact iterator; its ERR remains
// on that client's route, while the BLE list keeps its original destination.
{
Fixture f;f.startBle();f.sendUsb({CMD_GET_CONTACTS,0xFF,0xFF,0xFF,0xFF});f.step();
assert(f.usbFrames().size()==1&&f.usbFrames()[0][0]==RESP_CODE_ERR);
assert(f.usbFrames()[0][1]==ERR_CODE_BAD_STATE);
f.finish();assertBleSnapshot(f);
}
// Session loss cancels only its contact stream, irrespective of whichever
// interface supplied the newest command. No remaining contacts leak to USB.
{
Fixture f;f.startBle();f.sendUsb({CMD_GET_BATT_AND_STORAGE});f.step();
f.mesh.cancelSerialResponseStream(&f.usb);assert(f.mesh._iter_started);
f.mesh.cancelSerialResponseStream(&f.ble);assert(!f.mesh._iter_started);
f.run(300);assert(count(f.usbFrames(),RESP_CODE_CONTACT)==0);
}
{
Fixture f;f.startBle();f.ble.connected=false;f.step();assert(!f.mesh._iter_started);
f.sendUsb({CMD_GET_BATT_AND_STORAGE});f.step();assert(f.usbFrames()[0][0]==RESP_CODE_BATT_AND_STORAGE);
}
// The cached contact survives a failed BLE admission and an intervening
// command which overwrites both shared command/output scratch buffers.
{
Fixture f;f.mesh.contacts=3;f.startBle();f.ble.capacity=0;f.run(70);
assert(f.mesh._iter_contact_pending&&f.mesh._iter_pending_contact.id.pub_key[0]==1);
f.sendUsb({CMD_GET_BATT_AND_STORAGE});f.step();
assert(f.usbFrames()[0].back()==0x5A&&f.mesh._iter_contact_pending);
f.ble.capacity=4;f.finish();assertBleSnapshot(f,3);
}
{
Fixture f;f.startBle();f.ble.input.push_back({CMD_APP_START,0,0,0,0,0,0,0});
f.step();assert(!f.mesh._iter_started);f.run(300);
assert(count(f.ble.delivered,RESP_CODE_SELF_INFO)==1);
assert(count(f.usbFrames(),RESP_CODE_SELF_INFO)==0);
}
// A backed-up USB contact stream also leaves BLE queries responsive; once
// capacity returns, every START/contact/END is retained on the USB route.
{
Fixture f;f.drain_usb=false;f.stream.write_capacity=0;
f.sendUsb({CMD_GET_CONTACTS});f.step();assert(f.mesh._iter_started);
f.mesh.cancelSerialResponseStream(&f.ble);assert(f.mesh._iter_started);
f.ble.input.push_back({CMD_GET_CONTACTS});f.step();
f.ble.input.push_back({CMD_GET_BATT_AND_STORAGE});f.step();f.run(100);
assert(count(f.ble.delivered,RESP_CODE_ERR)==1);
assert(count(f.ble.delivered,RESP_CODE_BATT_AND_STORAGE)==1);
f.drain_usb=true;f.finish();
assert(count(f.usbFrames(),RESP_CODE_CONTACTS_START)==1);
assert(count(f.usbFrames(),RESP_CODE_CONTACT)==54);
assert(count(f.usbFrames(),RESP_CODE_END_OF_CONTACTS)==1);
assert(count(f.ble.delivered,RESP_CODE_CONTACT)==0);
}
}
'''
class ContactStreamConcurrencyTests(unittest.TestCase):
def test_paced_stream_keeps_routes_without_blocking_other_clients(self):
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
if not compiler:
self.skipTest('a host C++17 compiler is required')
mesh = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
native = (ROOT / 'test/test_serial_mode_switch/test_serial_mode_switch.cpp').read_text()
constants = '\n'.join(re.findall(
r'^#define (?:CONTACT_STREAM_FRAME_INTERVAL_MS|CMD_GET_CONTACTS|CMD_APP_START|CMD_GET_BATT_AND_STORAGE|RESP_CODE_ERR|RESP_CODE_CONTACTS_START|RESP_CODE_CONTACT|RESP_CODE_END_OF_CONTACTS|RESP_CODE_SELF_INFO|RESP_CODE_BATT_AND_STORAGE|ERR_CODE_BAD_STATE|ERR_CODE_FILE_IO_ERROR)\s+.+$',
mesh, re.MULTILINE))
contacts = extract_braced(mesh, '} else if (cmd_frame[0] == CMD_GET_CONTACTS').replace('} else if', 'if', 1)
app = extract_braced(mesh, '} else if (cmd_frame[0] == CMD_APP_START')
app = app[app.index('{') + 1:app.index(' int i = 0;')]
methods = '\n'.join(extract_braced(mesh, sig) for sig in (
'bool MyMesh::writeContactRespFrame(', 'void MyMesh::stopContactsIterator(',
'void MyMesh::cancelSerialResponseStream(', 'void MyMesh::checkSerialInterface()',
))
program = HARNESS.replace('@CONSTANTS@', constants).replace('@BUFFER@', extract_braced(native, 'class BufferStream') + ';')
program = program.replace('@GET_CONTACTS@', contacts).replace('@APP_BEGIN@', app).replace('@METHODS@', methods)
sanitizers = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else []
with tempfile.TemporaryDirectory(prefix='meshcore-contact-concurrency-') as directory:
binary = Path(directory) / 'concurrency'
result = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter', '-Wno-sign-compare',
*sanitizers,
f'-I{ROOT / "test/mocks"}', f'-I{ROOT / "src"}', '-x', 'c++', '-',
str(ROOT / 'src/helpers/ArduinoSerialInterface.cpp'), '-o', str(binary),
], input=program, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == '__main__':
unittest.main()
+11 -12
View File
@@ -30,18 +30,16 @@ class ContactStreamContractTests(unittest.TestCase):
def test_contact_frame_write_result_is_observable(self):
header = source("examples/companion_radio/MyMesh.h")
impl = source("examples/companion_radio/MyMesh.cpp")
self.assertIn(
"bool writeContactRespFrame(uint8_t code, const ContactInfo &contact);",
header,
)
self.assertRegex(header, r"bool writeContactRespFrame\(uint8_t code,\s*"
r"const ContactInfo &contact,\s*"
r"BaseSerialInterface\* route = NULL\);")
body = function_body(
impl,
"bool MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact)",
)
self.assertRegex(
body,
r"return\s+_serial->writeFrame\(out_frame,\s*i\)\s*==\s*\(size_t\)i\s*;",
"bool MyMesh::writeContactRespFrame(",
)
self.assertIn("_serial->writeFrameToRoute(route, out_frame, i)", body)
self.assertIn("_serial->writeFrame(out_frame, i)", body)
self.assertRegex(body, r"\)\s*==\s*\(size_t\)i\s*;")
def test_iterator_retries_start_contact_and_end_frames(self):
impl = source("examples/companion_radio/MyMesh.cpp")
@@ -52,17 +50,18 @@ class ContactStreamContractTests(unittest.TestCase):
self.assertRegex(
body,
r"if\s*\(writeContactRespFrame\(RESP_CODE_CONTACT,\s*"
r"_iter_pending_contact\)\)",
r"_iter_pending_contact,\s*_iter_reply_route\)\)",
)
self.assertRegex(
body,
r"if\s*\(_serial->writeFrame\(out_frame,\s*5\)\s*==\s*5\)\s*\{\s*"
r"if\s*\(_serial->writeFrameToRoute\(_iter_reply_route,\s*"
r"out_frame,\s*5\)\s*==\s*5\)\s*\{\s*"
r"stopContactsIterator\(\)\s*;",
)
# The cached contact is cleared only inside the successful-write arm.
success = body.index(
"if (writeContactRespFrame(RESP_CODE_CONTACT, _iter_pending_contact))"
"if (writeContactRespFrame(RESP_CODE_CONTACT, _iter_pending_contact,"
)
clear = body.index("_iter_contact_pending = false;", success)
self.assertGreater(clear, success)
@@ -238,6 +238,9 @@ static uint32_t now_ms=100;
struct Serial {
bool hasPendingIO() const { return false; }
bool isConnected() const { return false; }
bool isReplyRouteAvailable(const Serial* route) const {
return route==this&&isConnected();
}
};
struct {
bool isWatchdogObserving() const { return false; }
@@ -253,6 +256,7 @@ struct MyMesh {
const void *sign_data=nullptr;
const void *emergency_client_repeat_packet=nullptr;
Serial* _serial=nullptr;
Serial* _iter_reply_route=nullptr;
#if COMPANION_FEATURE_TEMP_RADIO
bool _temp_radio_applied=false;
uint32_t _temp_radio_set_at=0, _temp_radio_revert_at=0, _temp_radio_retry_at=0;
+3 -1
View File
@@ -96,7 +96,9 @@ struct MyMesh {
uint8_t cmd_frame[MAX_FRAME_SIZE+1]{}, out_frame[MAX_FRAME_SIZE+4]{};
char reply_buf[166]{};
bool isTerminalMode() const { return terminal; }
void cancelSerialResponseStream() { ++cancellations; }
void cancelSerialResponseStream(BaseSerialInterface* route) {
if (interface_manager.isReplyRouteFor(route)) ++cancellations;
}
void cancelSerialOperationsForRoute(BaseSerialInterface*) {}
void enterTerminalMode(bool show_banner) {
terminal=true;
+2 -1
View File
@@ -58,7 +58,8 @@ struct MyMesh {
uint8_t private_key_backup_sender[6] = {};
struct Ack { BaseSerialInterface* reply_route; bool radio_retry; };
Ack expected_ack_table[EXPECTED_ACK_TABLE_SIZE] = {};
void cancelSerialResponseStream() {
void cancelSerialResponseStream(BaseSerialInterface* route) {
if (!interface_manager.isReplyRouteFor(route)) return;
++stream_cancels;
if (streaming) { streaming = false; interface_manager.unlockReplyRoute(); }
}
+1 -5
View File
@@ -520,11 +520,8 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
main.index("static void cancelUsbSerialOperations()"):
main.index("static void enterUsbTerminalMode(")
]
route_check = usb_cancel.index(
"interface_manager.isReplyRouteFor(&usb_serial_interface)"
)
route_cancel = usb_cancel.index(
"the_mesh.cancelSerialResponseStream()"
"the_mesh.cancelSerialResponseStream(&usb_serial_interface)"
)
delayed_cancel = usb_cancel.index(
"the_mesh.cancelSerialOperationsForRoute(&usb_serial_interface)"
@@ -532,7 +529,6 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
route_forget = usb_cancel.index(
"interface_manager.forgetReplyRouteForDisconnected("
)
self.assertLess(route_check, route_cancel)
self.assertLess(route_cancel, delayed_cancel)
self.assertLess(delayed_cancel, route_forget)
self.assertIn("the_mesh.resetUsbHostSessionInput();", reset_helper)
+5
View File
@@ -39,6 +39,9 @@ struct SerialInterface {
bool pending=false, connected=false, passthrough=true;
bool hasPendingIO() const{return pending;}
bool isConnected() const{return connected;}
bool isReplyRouteAvailable(const SerialInterface* route) const {
return route==this&&connected;
}
bool isPassthroughMode() const{return passthrough;}
};
struct RoleMesh {
@@ -54,6 +57,7 @@ struct RoleMesh {
Store* _store=&store;
SerialInterface serial;
SerialInterface* _serial=&serial;
SerialInterface* _iter_reply_route=&serial;
bool hasOutbound() const{return outbound;}
bool isAnyTempRadioActive() const{return temporary;}
bool hasPendingOtaApply() const{return ota_apply;}
@@ -84,6 +88,7 @@ void reset(){
// Assignment copies the fixture's dependency pointers; point them back at
// the live fixture objects, not the temporary used to reset scalar state.
the_mesh._store=&the_mesh.store;the_mesh._serial=&the_mesh.serial;
the_mesh._iter_reply_route=&the_mesh.serial;
command[0]=0;command_overflow=false;
#if defined(ENABLE_USB_INTERFACE)
usb_serial_interface=SerialInterface{};usb_connected=false;