From 8c9bb6c8ba771e47e1101e23b2864001d15ea20d Mon Sep 17 00:00:00 2001 From: Wes Ladd Date: Mon, 13 Jul 2026 01:36:18 -0500 Subject: [PATCH] Send complete XMODEM frames over Wi-Fi --- CHANGELOG.md | 1 + carveracontroller/WIFIStream.py | 3 ++- tests/unit/test_wifi_stream.py | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_wifi_stream.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 603ca7b9..8d83bc2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed: Ensure complete XMODEM packets are written over Wi-Fi - Enhancement: Add multi-select to the remote file browser - Enhancement: Display error message in halt popup. Requires halt errors to start with "ERROR: " in the firmware - Enhancement: Add popup notice when using stock firmware instead of the Community Firmware diff --git a/carveracontroller/WIFIStream.py b/carveracontroller/WIFIStream.py index da0c7bac..8d10ae77 100644 --- a/carveracontroller/WIFIStream.py +++ b/carveracontroller/WIFIStream.py @@ -158,7 +158,8 @@ def getc(self, size, timeout=0.5): return None def putc(self, data, timeout=0.5): - return self.socket.send(data) or None + self.socket.sendall(data) + return len(data) def upload(self, filename, local_md5, callback): # do upload diff --git a/tests/unit/test_wifi_stream.py b/tests/unit/test_wifi_stream.py new file mode 100644 index 00000000..56c9e6cc --- /dev/null +++ b/tests/unit/test_wifi_stream.py @@ -0,0 +1,46 @@ +"""Tests for WIFIStream socket write semantics.""" + +from carveracontroller.WIFIStream import WIFIStream +from carveracontroller.XMODEM import XMODEM + + +class DeterministicShortWriteSocket: + """Socket double that accepts only a prefix from each ``send`` call.""" + + def __init__(self, short_write_size): + self.short_write_size = short_write_size + self.send_calls = 0 + self.sendall_calls = 0 + self.wire = bytearray() + + def send(self, data): + self.send_calls += 1 + accepted = min(self.short_write_size, len(data)) + self.wire.extend(data[:accepted]) + return accepted + + def sendall(self, data): + self.sendall_calls += 1 + self.wire.extend(data) + + +def _xmodem8k_frame(): + modem = XMODEM(lambda _size, _timeout=0.5: None, lambda data, _timeout=0.5: len(data)) + payload = b"x" * 8192 + framed_payload = bytes([len(payload) >> 8, len(payload) & 0xFF]) + payload + return bytes(modem._make_send_header(8192, 1) + framed_payload + modem._make_send_checksum(1, framed_payload)) + + +def test_putc_sends_the_complete_xmodem_frame_and_returns_its_length(): + stream = WIFIStream.__new__(WIFIStream) + stream.socket = DeterministicShortWriteSocket(short_write_size=2048) + frame = _xmodem8k_frame() + assert len(frame) == 8199, "fixture must model a complete xmodem8k frame" + + result = stream.putc(frame) + + assert len(stream.socket.wire) == len(frame) + assert bytes(stream.socket.wire) == frame + assert stream.socket.send_calls == 0 + assert stream.socket.sendall_calls == 1 + assert result == len(frame)