Restore Companion text terminal after USB mOTA sender exits

This commit is contained in:
mikecarper committed 2026-09-25 17:51:38 -07:00
1 parent ce46e60a86
commit 9ba79848ca
2 files changed
+120 -1

No files matched your search

+11 -1
View File
@@ -620,6 +620,8 @@ static mesh::UsbBinaryStartupProbe usb_binary_startup_probe;
static mesh::UsbAsciiSessionDefault usb_ascii_session_default;
#if COMPANION_FEATURE_USB_MOTA_SOURCE
static bool usb_mota_mode = false;
static mesh::UsbMotaEntryOrigin usb_mota_entry_origin =
mesh::UsbMotaEntryOrigin::BINARY;
static char usb_mota_line[32];
static size_t usb_mota_line_len = 0;
static bool usb_mota_disconnect_armed = false;
@@ -779,16 +781,23 @@ static void resetUsbMotaMode() {
}
static void leaveUsbMotaMode(bool acknowledge) {
const bool restore_ascii = acknowledge
&& usb_mota_entry_origin == mesh::UsbMotaEntryOrigin::ASCII;
char reply[160] = {0};
the_mesh.handleLocalControlCommand("ota folder off", reply, sizeof(reply));
if (acknowledge) {
char transition_reply[224];
snprintf(transition_reply, sizeof(transition_reply),
"\r\n%s\r\nOK - Binary mode\r\n", reply);
"\r\n%s\r\nOK - %s mode\r\n", reply,
restore_ascii ? "Terminal" : "Binary");
queueUsbTerminalControlReply(transition_reply);
drainUsbTerminalOutputBeforeProtocolSwitch();
}
resetUsbMotaMode();
// A text-first motatool session must hand CDC0 back to the text terminal.
// Otherwise closing the sender leaves this still-open USB host in Binary
// mode, and its next `ver` or `ota status` appears to get no reply.
if (restore_ascii) enterUsbTerminalMode(false);
}
static bool enterUsbMotaMode(mesh::UsbMotaEntryOrigin origin) {
@@ -796,6 +805,7 @@ static bool enterUsbMotaMode(mesh::UsbMotaEntryOrigin origin) {
mesh::discardUsbTerminalOutput();
usb_serial_interface.setPassthroughMode(true);
usb_mota_mode = true;
usb_mota_entry_origin = origin;
usb_mota_line_len = 0;
usb_mota_line[0] = 0;
usb_mota_disconnect_armed = isUsbTerminalDataConnected();
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Execute the Companion's real USB mOTA exit functions with both entry modes."""
from pathlib import Path
import subprocess
import tempfile
import unittest
MAIN = (Path(__file__).resolve().parents[1] / "examples/companion_radio/main.cpp").read_text()
def function(signature):
start = MAIN.index(signature)
end = MAIN.index("{", start) + 1
depth = 1
while depth:
depth += (MAIN[end] == "{") - (MAIN[end] == "}")
end += 1
return MAIN[start:end]
HARNESS = r'''
#include <cassert>
#include <cstdio>
#include <cstring>
namespace mesh {
enum class UsbMotaEntryOrigin { BINARY, ASCII };
static void discardUsbTerminalOutput() {}
}
static bool usb_mota_mode = true, usb_mota_disconnect_armed = true;
static mesh::UsbMotaEntryOrigin usb_mota_entry_origin = mesh::UsbMotaEntryOrigin::BINARY;
static char usb_mota_line[32] = "old";
static size_t usb_mota_line_len = 3;
static bool ascii = false;
static int drained = 0, folder_off = 0;
static char control_reply[224];
struct SerialInterface {
bool passthrough = true;
void setPassthroughMode(bool enabled) { passthrough = enabled; }
} usb_serial_interface;
struct Mesh {
bool handleLocalControlCommand(const char* command, char* reply, size_t size) {
assert(strcmp(command, "ota folder off") == 0);
++folder_off;
snprintf(reply, size, "OK folder off");
return true;
}
} the_mesh;
static void queueUsbTerminalControlReply(const char* reply) {
snprintf(control_reply, sizeof(control_reply), "%s", reply);
}
static void drainUsbTerminalOutputBeforeProtocolSwitch() { ++drained; }
static void enterUsbTerminalMode(bool show_banner) {
assert(!show_banner);
ascii = true;
usb_serial_interface.setPassthroughMode(true);
}
@FUNCTIONS@
static void prepare(mesh::UsbMotaEntryOrigin origin) {
usb_mota_mode = usb_mota_disconnect_armed = true;
usb_mota_entry_origin = origin;
usb_mota_line_len = 3;
strcpy(usb_mota_line, "old");
usb_serial_interface.passthrough = true;
ascii = false;
drained = folder_off = 0;
control_reply[0] = 0;
}
int main() {
prepare(mesh::UsbMotaEntryOrigin::ASCII);
leaveUsbMotaMode(true);
assert(ascii && usb_serial_interface.passthrough);
assert(!usb_mota_mode && !usb_mota_disconnect_armed && usb_mota_line_len == 0);
assert(folder_off == 1 && drained == 1);
assert(strstr(control_reply, "OK - Terminal mode") != nullptr);
prepare(mesh::UsbMotaEntryOrigin::BINARY);
leaveUsbMotaMode(true);
assert(!ascii && !usb_serial_interface.passthrough);
assert(folder_off == 1 && drained == 1);
assert(strstr(control_reply, "OK - Binary mode") != nullptr);
prepare(mesh::UsbMotaEntryOrigin::ASCII);
leaveUsbMotaMode(false); // Host reset owns the next protocol decision.
assert(!ascii && !usb_serial_interface.passthrough);
assert(folder_off == 1 && drained == 0 && control_reply[0] == 0);
}
'''
class UsbMotaExitTests(unittest.TestCase):
def test_clean_text_sender_restores_text_terminal(self):
source = HARNESS.replace(
"@FUNCTIONS@",
function("static void resetUsbMotaMode()") + "\n"
+ function("static void leaveUsbMotaMode(bool acknowledge)"),
)
with tempfile.TemporaryDirectory() as directory:
cpp = Path(directory) / "test.cpp"
binary = Path(directory) / "test"
cpp.write_text(source)
subprocess.run(["c++", "-std=c++11", str(cpp), "-o", str(binary)],
check=True, capture_output=True)
subprocess.run([str(binary)], check=True, capture_output=True)
if __name__ == "__main__":
unittest.main()