351 lines
19 KiB
Python
351 lines
19 KiB
Python
"""Exercise real UART session parking, inbox delivery and serial sleep vetoes."""
|
|
from pathlib import Path
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
from test_esp32_usb_sleep import board_method
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PRELUDE = r'''
|
|
#include <helpers/ArduinoSerialInterface.h>
|
|
#include <helpers/MultiSerialInterface.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/UsbAsciiBinarySwitch.h>
|
|
#include <helpers/esp32/PowerTestControls.h>
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <deque>
|
|
#include <limits>
|
|
#include <vector>
|
|
#define COMPANION_FEATURE_USB_MOTA_SOURCE 1
|
|
#define COMPANION_FEATURE_NETWORK_TERMINAL 1
|
|
#define COMPANION_FEATURE_READER 1
|
|
#define MESH_USB_LOGGING_AVAILABLE 0
|
|
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
|
#define OFFLINE_QUEUE_SIZE 64
|
|
@CONSTANTS@
|
|
@BUFFER@
|
|
struct Uart : BufferStream {
|
|
bool driver=true;
|
|
unsigned begins=0,ends=0,flushes=0,fail_begins=0;
|
|
explicit operator bool() const { return driver; }
|
|
int availableForWrite() override { return driver ? BufferStream::availableForWrite() : 0; }
|
|
void begin(unsigned baud) {
|
|
assert(baud==115200);++begins;
|
|
driver=fail_begins==0;if(fail_begins)--fail_begins;
|
|
}
|
|
void end() { assert(driver);++ends;driver=false;input.clear(); }
|
|
void flush() override { assert(driver);++flushes; }
|
|
} Serial;
|
|
static uint32_t micros() { return millis()*1000; }
|
|
static bool terminal_output_pending=false;
|
|
namespace mesh {
|
|
bool hasPendingUsbTerminalOutput() { return terminal_output_pending; }
|
|
bool isUsbLoggingEnabled() { return false; }
|
|
bool hasDedicatedUsbLoggingPort() { return false; }
|
|
void discardUsbTerminalOutput() { terminal_output_pending=false; }
|
|
Stream& usbTerminalPort() { return Serial; }
|
|
static struct { const Stream* output=nullptr; } print_job;
|
|
static const Stream*& reader_owner=print_job.output;
|
|
@READER_CANCEL@
|
|
struct GroupChannel {};
|
|
struct Packet {
|
|
uint8_t path_len=0;
|
|
float getSNR() const { return 4; }
|
|
bool isRouteFlood() const { return false; }
|
|
};
|
|
namespace ota {
|
|
struct Source { bool pending=false;bool hasPendingResponse() const { return pending; } };
|
|
struct OtaContext { static Source& serialFolderSource() { static Source source;return source; } };
|
|
}
|
|
}
|
|
ArduinoSerialInterface usb_serial_interface,other_interface;
|
|
MultiSerialInterface interface_manager;
|
|
BufferStream other_stream;
|
|
struct MyMesh {
|
|
@FRAME@
|
|
Frame offline_queue[OFFLINE_QUEUE_SIZE]{};
|
|
int offline_queue_len=0,offline_queue_head=0;
|
|
uint8_t out_frame[MAX_FRAME_SIZE]{};
|
|
BaseSerialInterface* _serial=&interface_manager;
|
|
bool terminal=false,network=false,stale_input=true;
|
|
unsigned ascii_starts=0;
|
|
bool last_banner=true;
|
|
BaseSerialInterface* stream_route=nullptr;
|
|
BaseSerialInterface* pending_route=nullptr;
|
|
unsigned cancelled_usb=0,cancelled_other=0;
|
|
bool isTerminalMode() const { return terminal; }
|
|
void enterTerminalMode(bool banner) { assert(!network);terminal=true;++ascii_starts;last_banner=banner; }
|
|
void exitTerminalMode() { terminal=false; }
|
|
void resetUsbHostSessionInput() { stale_input=false; }
|
|
void cancelSerialResponseStream(BaseSerialInterface* route) {
|
|
if(stream_route==route)stream_route=nullptr;
|
|
}
|
|
void cancelSerialOperationsForRoute(BaseSerialInterface* route) {
|
|
if(route==&usb_serial_interface)++cancelled_usb;else++cancelled_other;
|
|
if(pending_route==route)pending_route=nullptr;
|
|
}
|
|
int findChannelIdxForPacket(const mesh::GroupChannel&,mesh::Packet*) const { return 0; }
|
|
int getOfflineQueueCapacity() const;
|
|
Frame& offlineQueueFrameAt(int);
|
|
bool addToOfflineQueue(const uint8_t*,int);
|
|
int peekOfflineQueue(uint8_t*);
|
|
void popOfflineQueue();
|
|
void syncNextOfflineMessage();
|
|
void onChannelDataRecv(const mesh::GroupChannel&,mesh::Packet*,uint16_t,const uint8_t*,size_t);
|
|
mesh::powerdiag::Mask serialVeto() const {
|
|
using mesh::powerdiag::Reason;using mesh::powerdiag::mask;
|
|
@SERIAL_GUARD@
|
|
return 0;
|
|
}
|
|
} the_mesh;
|
|
@INBOX@
|
|
static bool usb_protocol_initialized=true,usb_mota_mode=false;
|
|
static bool usb_logging_terminal_mode=false,usb_terminal_discard_line=false;
|
|
static bool usb_terminal_host_reset_completion_pending=false;
|
|
static char usb_terminal_line[64]{};
|
|
static unsigned usb_terminal_line_len=0;
|
|
static mesh::UsbBinaryStartupProbe usb_binary_startup_probe;
|
|
static mesh::UsbAsciiSessionDefault usb_ascii_session_default;
|
|
static bool usb_uart_quiet_parked=false,usb_uart_quiet_restore_enabled=false;
|
|
static bool isNetworkTerminalActive() { return the_mesh.network; }
|
|
static void queueUsbTerminalControlReply(const char*) { assert(false); }
|
|
static void drainUsbTerminalOutputBeforeProtocolSwitch() { assert(false); }
|
|
static void clearUsbTerminalLine() { memset(usb_terminal_line,0,sizeof(usb_terminal_line));usb_terminal_line_len=0; }
|
|
@SESSION@
|
|
struct Board {
|
|
bool inhibit_sleep=false,ota=false,radio_test=false;
|
|
uint32_t uart_quiet_start_at=0,uart_quiet_restore_at=0;
|
|
uint16_t uart_quiet_seconds=0;
|
|
bool isOTAUpdateRunning() const { return ota; }
|
|
bool isRadioTestActive() const { return radio_test; }
|
|
@BOARD@
|
|
};
|
|
using Bytes=std::vector<uint8_t>;
|
|
static void select(ArduinoSerialInterface& route,BufferStream& stream) {
|
|
const uint8_t input[]={'<',1,0,10};uint8_t command[MAX_FRAME_SIZE]{};
|
|
stream.push(input,sizeof(input));assert(interface_manager.checkRecvFrame(command)==1);
|
|
assert(interface_manager.captureReplyRoute()==&route);
|
|
}
|
|
static void receive(unsigned sequence) {
|
|
mesh::GroupChannel channel;mesh::Packet packet;
|
|
const uint8_t data[]={uint8_t(sequence),0xa5,0x5a,0xff};
|
|
the_mesh.onChannelDataRecv(channel,&packet,0xff00,data,sizeof(data));
|
|
}
|
|
static void setup() {
|
|
resetArduinoMock();g_mock_millis=1000;
|
|
usb_serial_interface.begin(Serial,"START");usb_serial_interface.enableFlowControl(true);
|
|
other_interface.begin(other_stream);other_interface.enableFlowControl(true);
|
|
assert(interface_manager.addInterface(InterfaceType::USB,&usb_serial_interface));
|
|
assert(interface_manager.addInterface(InterfaceType::WiFi,&other_interface));
|
|
interface_manager.enable();other_interface.disable();
|
|
mesh::esp32::registerUartQuietSessionControl(nullptr,transitionCompanionUartQuietSession);
|
|
}
|
|
'''
|
|
|
|
SCENARIOS = r'''
|
|
int main(int argc,char**) {
|
|
setup();Board board;char reply[160]{};
|
|
using mesh::esp32::UartQuietAction;
|
|
using mesh::esp32::UartQuietSessionAction;
|
|
select(usb_serial_interface,Serial);
|
|
const uint32_t original_count=usb_serial_interface.getCompletedFrameCount();
|
|
the_mesh.stream_route=the_mesh.pending_route=&usb_serial_interface;
|
|
interface_manager.lockReplyRoute();
|
|
mesh::reader_owner=&Serial;
|
|
// Refused readiness cannot partly retire the live host or its producers.
|
|
usb_protocol_initialized=false;
|
|
assert(!transitionCompanionUartQuietSession(nullptr,UartQuietSessionAction::Park));
|
|
usb_protocol_initialized=true;usb_mota_mode=true;
|
|
assert(!transitionCompanionUartQuietSession(nullptr,UartQuietSessionAction::Park));
|
|
usb_mota_mode=false;mesh::ota::OtaContext::serialFolderSource().pending=true;
|
|
assert(!transitionCompanionUartQuietSession(nullptr,UartQuietSessionAction::Park));
|
|
mesh::ota::OtaContext::serialFolderSource().pending=false;
|
|
terminal_output_pending=true;
|
|
assert(!transitionCompanionUartQuietSession(nullptr,UartQuietSessionAction::Park));
|
|
terminal_output_pending=false;
|
|
assert(the_mesh.cancelled_usb==0&&mesh::reader_owner==&Serial&&usb_serial_interface.isEnabled());
|
|
// Cancelling an armed request is not a host-session disconnect.
|
|
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
|
|
assert(board.handleUartQuiet(UartQuietAction::Cancel,0,reply,sizeof(reply)));
|
|
assert(the_mesh.cancelled_usb==0&&usb_serial_interface.isEnabled()&&Serial.ends==0);
|
|
// A queued ACK must drain before either the session or UART is parked.
|
|
Serial.write_capacity=0;const uint8_t ack=0;
|
|
assert(interface_manager.writeFrameToRoute(&usb_serial_interface,&ack,1)==1);
|
|
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
|
|
g_mock_millis+=100;board.servicePowerTestControls(!interface_manager.hasPendingIO());
|
|
assert(Serial.ends==0&&the_mesh.cancelled_usb==0&&interface_manager.hasPendingIO());
|
|
Serial.write_capacity=4096;interface_manager.loop();
|
|
assert(!interface_manager.hasPendingIO()&&Serial.output==Bytes({'>',1,0,0}));
|
|
board.servicePowerTestControls(!interface_manager.hasPendingIO());
|
|
assert(!Serial&&Serial.ends==1&&Serial.flushes==1);
|
|
if(argc>1) {
|
|
// Negative control: the old board ended the driver without parking its
|
|
// still-assumed-connected interface. A real RX queues MSG_WAITING forever.
|
|
assert(usb_serial_interface.isEnabled());receive(0);
|
|
assert(the_mesh.offline_queue_len==1&&interface_manager.hasPendingIO());
|
|
assert(the_mesh.serialVeto()==mesh::powerdiag::mask(mesh::powerdiag::Reason::Serial));
|
|
puts("old UART quiet retained MSG_WAITING serial veto");return 42;
|
|
}
|
|
assert(!usb_serial_interface.isEnabled()&&!interface_manager.isConnected());
|
|
assert(the_mesh.cancelled_usb==1&&the_mesh.cancelled_other==0);
|
|
assert(mesh::reader_owner==nullptr);
|
|
assert(!the_mesh.stream_route&&!the_mesh.pending_route&&!interface_manager.captureReplyRoute());
|
|
assert(!the_mesh.stale_input&&!usb_binary_startup_probe.isActive());
|
|
assert(!interface_manager.isReplyRouteAvailable(&usb_serial_interface));
|
|
// The application's actual RX/inbox method retains every packet while no
|
|
// transport is listening, with no unserviceable MSG_WAITING frame queued.
|
|
for(unsigned i=0;i<40;++i) {
|
|
receive(i);interface_manager.loop();
|
|
assert(the_mesh.offline_queue_len==int(i+1));
|
|
assert(!interface_manager.hasPendingIO()&&the_mesh.serialVeto()==0);
|
|
}
|
|
const Bytes quiet_output=Serial.output;
|
|
the_mesh.syncNextOfflineMessage();assert(the_mesh.offline_queue_len==40);
|
|
// Another transport's real queued work keeps the original serial veto.
|
|
// Nothing about quiet mode grants it an exception or clears its session.
|
|
other_interface.enable();other_stream.write_capacity=0;
|
|
the_mesh.pending_route=the_mesh.stream_route=&other_interface;
|
|
assert(interface_manager.writeFrameToRoute(&other_interface,&ack,1)==1);
|
|
assert(interface_manager.hasPendingIO());
|
|
assert(the_mesh.serialVeto()==mesh::powerdiag::mask(mesh::powerdiag::Reason::Serial));
|
|
assert(!transitionCompanionUartQuietSession(nullptr,UartQuietSessionAction::Resume));
|
|
assert(!usb_serial_interface.isEnabled()&&the_mesh.pending_route==&other_interface);
|
|
other_stream.write_capacity=4096;interface_manager.loop();
|
|
assert(!interface_manager.hasPendingIO()&&the_mesh.serialVeto()==0);
|
|
other_interface.disable();
|
|
// A failed driver restart retains the parked session and all inbox data;
|
|
// the timer retries even when manual CPU sleep has been turned off.
|
|
mesh::esp32::powerTestControls().sleep_enabled=false;Serial.fail_begins=1;
|
|
g_mock_millis+=5000;board.servicePowerTestControls();
|
|
assert(!Serial&&!usb_serial_interface.isEnabled()&&board.uart_quiet_restore_at);
|
|
assert(the_mesh.offline_queue_len==40&&the_mesh.ascii_starts==0);
|
|
g_mock_millis+=999;board.servicePowerTestControls();assert(!Serial);
|
|
++g_mock_millis;board.servicePowerTestControls();
|
|
assert(Serial&&usb_serial_interface.isEnabled()&&!board.uart_quiet_restore_at);
|
|
assert(the_mesh.ascii_starts==1&&!the_mesh.last_banner&&the_mesh.terminal);
|
|
assert(usb_serial_interface.getCompletedFrameCount()==original_count);
|
|
assert(Serial.output==quiet_output&&the_mesh.offline_queue_len==40);
|
|
assert(the_mesh.pending_route==&other_interface&&the_mesh.stream_route==&other_interface);
|
|
assert(!other_interface.isEnabled()&&the_mesh.cancelled_other==0);
|
|
// Fresh Binary ownership can download the original inbox once, in order.
|
|
leaveUsbTerminalMode(false);select(usb_serial_interface,Serial);
|
|
for(unsigned i=0;i<40;++i)the_mesh.syncNextOfflineMessage();
|
|
assert(the_mesh.offline_queue_len==0);
|
|
size_t at=quiet_output.size();
|
|
for(unsigned i=0;i<40;++i) {
|
|
assert(Serial.output.at(at)=='>');size_t length=Serial.output.at(at+1)|(size_t(Serial.output.at(at+2))<<8);
|
|
at+=3;assert(length==13&&Serial.output.at(at)==27&&Serial.output.at(at+9)==i);at+=length;
|
|
}
|
|
assert(at==Serial.output.size());
|
|
// A pre-disabled interface must stay disabled after the timed recovery.
|
|
usb_serial_interface.disable();const unsigned starts=the_mesh.ascii_starts;
|
|
mesh::reader_owner=&other_stream;
|
|
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
|
|
g_mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
|
|
g_mock_millis+=5000;board.servicePowerTestControls();
|
|
assert(Serial&&!usb_serial_interface.isEnabled()&&the_mesh.ascii_starts==starts);
|
|
assert(mesh::reader_owner==&other_stream);
|
|
// A live network terminal/Reader retains its owner when USB comes back.
|
|
usb_serial_interface.enable();the_mesh.network=true;
|
|
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
|
|
g_mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
|
|
g_mock_millis+=5000;board.servicePowerTestControls();
|
|
assert(Serial&&usb_serial_interface.isEnabled()&&the_mesh.ascii_starts==starts);
|
|
assert(mesh::reader_owner==&other_stream&&the_mesh.network);
|
|
// Active USB ASCII ownership is retired too. A subsequent global manager
|
|
// shutdown must not be reversed merely because the UART recovery is due.
|
|
the_mesh.network=false;serviceUsbAsciiSessionDefault();
|
|
assert(the_mesh.terminal);mesh::reader_owner=&Serial;
|
|
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
|
|
g_mock_millis+=100;board.servicePowerTestControls();
|
|
assert(!Serial&&!the_mesh.terminal&&!usb_serial_interface.isEnabled()&&mesh::reader_owner==nullptr);
|
|
interface_manager.disable();g_mock_millis+=5000;board.servicePowerTestControls();
|
|
assert(Serial&&!interface_manager.isEnabled()&&!usb_serial_interface.isEnabled());
|
|
puts("parked UART retained forty real RX inbox frames without bypassing serial guards");
|
|
}
|
|
'''
|
|
|
|
|
|
def harness(legacy=False):
|
|
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
|
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
|
|
native = (ROOT / "test/test_serial_mode_switch/test_serial_mode_switch.cpp").read_text()
|
|
reader = (ROOT / "src/helpers/CompanionReader.cpp").read_text()
|
|
constants = "\n".join(re.findall(
|
|
r"^#define (?:RESP_CODE_(?:CHANNEL_MSG_RECV|CHANNEL_MSG_RECV_V3|CHANNEL_DATA_RECV|NO_MORE_MESSAGES)|PUSH_CODE_MSG_WAITING|MAX_CHANNEL_DATA_LENGTH)\s+.+$",
|
|
source, re.MULTILINE))
|
|
inbox = "\n".join(extract_braced(source, signature) for signature in (
|
|
"bool MyMesh::Frame::isChannelMsg() const", "int MyMesh::getOfflineQueueCapacity() const",
|
|
"MyMesh::Frame& MyMesh::offlineQueueFrameAt(", "bool MyMesh::addToOfflineQueue(",
|
|
"int MyMesh::peekOfflineQueue(", "void MyMesh::popOfflineQueue(",
|
|
"void MyMesh::syncNextOfflineMessage(", "void MyMesh::onChannelDataRecv("))
|
|
session = "\n".join(extract_braced(main, signature) for signature in (
|
|
"static void cancelUsbSerialOperations()", "static void enterUsbTerminalMode(",
|
|
"static void leaveUsbTerminalMode(", "static void serviceUsbAsciiSessionDefault()",
|
|
"static void beginUsbDefaultSession()", "static bool transitionCompanionUartQuietSession("))
|
|
board = "\n".join(board_method(signature) for signature in (
|
|
"bool handleUartQuiet(", "void servicePowerTestControls("))
|
|
if legacy:
|
|
park = " if (!mesh::esp32::transitionUartQuietSession(\n mesh::esp32::UartQuietSessionAction::Park)) return;\n"
|
|
assert board.count(park) == 1
|
|
board = board.replace(park, "", 1)
|
|
guard = re.search(r" if \(_serial != NULL && _serial->hasPendingIO\(\)\) return mask\(Reason::Serial\);", source)
|
|
assert guard is not None
|
|
result = PRELUDE
|
|
for marker, value in {"@CONSTANTS@": constants, "@BUFFER@": extract_braced(native, "class BufferStream") + ";",
|
|
"@FRAME@": extract_braced(header, "struct Frame {") + ";", "@INBOX@": inbox,
|
|
"@SERIAL_GUARD@": guard.group(), "@SESSION@": session, "@BOARD@": board,
|
|
"@READER_CANCEL@": extract_braced(reader, "void cancelReaderCommand(")}.items():
|
|
result = result.replace(marker, value)
|
|
return result + SCENARIOS
|
|
|
|
|
|
class UartQuietSessionTests(unittest.TestCase):
|
|
def test_actual_rx_queue_and_transport_lifecycle_with_old_negative_control(self):
|
|
with tempfile.TemporaryDirectory(prefix="mesh-uart-quiet-session-") as directory:
|
|
folder = Path(directory)
|
|
for legacy in (True, False):
|
|
with self.subTest(legacy=legacy):
|
|
cpp, binary = folder / "test.cpp", folder / "test"
|
|
cpp.write_text(harness(legacy), encoding="ascii")
|
|
built = subprocess.run([os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
|
|
"-Werror", "-Wno-unused-parameter", "-Wno-unused-function", "-Wno-sign-compare",
|
|
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
|
"-DESP32_PLATFORM=1", "-DARDUINO_USB_CDC_ON_BOOT=0",
|
|
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"), str(cpp),
|
|
str(ROOT / "src/helpers/ArduinoSerialInterface.cpp"), "-o", str(binary)],
|
|
text=True, capture_output=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
ran = subprocess.run([str(binary)] + (["legacy"] if legacy else []),
|
|
text=True, capture_output=True, timeout=20)
|
|
self.assertEqual(ran.returncode, 42 if legacy else 0, ran.stdout + ran.stderr)
|
|
self.assertIn("MSG_WAITING serial veto" if legacy else "forty real RX inbox frames", ran.stdout)
|
|
|
|
def test_hook_is_registered_only_for_default_esp32_uart(self):
|
|
source = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
|
pattern = r"#if defined\(ESP32_PLATFORM\) \\\n && \(!defined\(ARDUINO_USB_CDC_ON_BOOT\) \|\| !ARDUINO_USB_CDC_ON_BOOT\) \\\n && !defined\(SERIAL_RX\) && !defined\(SERIAL_TX\)\n"
|
|
self.assertEqual(len(re.findall(pattern, source)), 2)
|
|
start = source.index(" mesh::esp32::registerUartQuietSessionControl(")
|
|
prefix = source[:start]
|
|
block = prefix[prefix.rfind("#if defined(ESP32_PLATFORM)"):] + source[start:source.index("#endif", start) + 6]
|
|
for defines, expected in ((["ESP32_PLATFORM=1"], True), (["NRF52_PLATFORM=1"], False),
|
|
(["ESP32_PLATFORM=1", "ARDUINO_USB_CDC_ON_BOOT=1"], False),
|
|
(["ESP32_PLATFORM=1", "SERIAL_RX=1"], False), (["ESP32_PLATFORM=1", "SERIAL_TX=1"], False)):
|
|
with self.subTest(defines=defines):
|
|
result = subprocess.run([os.environ.get("CXX", "c++"), "-E", "-P", "-x", "c++",
|
|
*["-D" + value for value in defines], "-"], input=block, text=True, capture_output=True, timeout=20)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual("registerUartQuietSessionControl" in result.stdout, expected)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|