wip: use mcumgr params for serial transports#81
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR refactors the SMPSerialTransport class to use mcumgr parameters for configuring serial transport buffers. It introduces a new fragmentation strategy pattern with Auto() and BufferParams classes, replacing the previous direct parameter approach.
- Replaces direct constructor parameters with a
fragmentation_strategyparameter that accepts eitherAuto()orBufferParams - Adds automatic buffer configuration based on server's MCUMGR_PARAM buffer size
- Updates all test cases and examples to use the new API
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| smpclient/transport/serial.py | Refactors constructor to use fragmentation strategy pattern and adds server buffer size initialization |
| tests/test_smp_serial_transport.py | Updates tests for new constructor API and adds tests for Auto/BufferParams modes |
| tests/test_smp_client.py | Updates test transport creation to use new BufferParams syntax |
| examples/usb/upgrade.py | Updates example code to use new BufferParams constructor pattern |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
fd8d90b to
523d358
Compare
|
Great progress! We now have a native_sim fixture that we can use to test stuff out: https://github.com/intercreate/smp-server-fixtures/actions/runs/18636709577 We can always add QEMU in the future - in fact, that would be required to test image management firmware upgrades with MCUBoot - but the native_sim target made this really easy to start out. In the long run, it probably makes sense to matrix a bunch of configs with twister and then commit the zip to this repo for integration testing. native_sim terminal: smpmgr terminal: |
JPHutchins
left a comment
There was a problem hiding this comment.
I think that more optimized defaults should be added. For example, I think that MCUBoot default is 128 * 8, Zephyr main is 128 * 2, etc. Allows the user to opt out of MCUmgr params negotiation and still get maximum throughput.
| if isinstance(self._fragmentation_strategy, SMPSerialTransport.Auto): | ||
| return self.BufferParams().line_length | ||
| else: | ||
| return self._fragmentation_strategy.line_length |
There was a problem hiding this comment.
I think that we dropped 3.9 support, so this should be replaced with exhaustive match case.
… suite Launch real Zephyr SMP servers from the vendored smp-server-fixtures release (registry built from manifest.json) and drive them with SMPClient. Covers the buffer-size matrix, serial/shell/udp(IPv4+IPv6) transports, fs file round-trip, img DFU, and MCUboot serial recovery via `os reset boot_mode` (smp 4.1.0). Linux-only; excluded from the default `task test` via the `integration` marker. Also sets the serial Auto line_length default to 128. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
523d358 to
cad229e
Compare
| @property | ||
| def _line_buffers(self) -> int: | ||
| """The number of line buffers.""" | ||
| if isinstance(self._fragmentation_strategy, SMPSerialTransport.Auto): | ||
| if self._smp_server_transport_buffer_size is not None: | ||
| return self._smp_server_transport_buffer_size // self.BufferParams().line_length | ||
| return self.BufferParams().line_buffers | ||
| else: | ||
| return self._fragmentation_strategy.line_buffers |
There was a problem hiding this comment.
Good catch on the floor-division inconsistency — fixed in a714fc8, though the empirical result is the opposite of an overflow.
I probed the real servers (native_sim / QEMU / mps2): buf_size is CONFIG_MCUMGR_TRANSPORT_NETBUF_SIZE, which bounds the decoded SMP serial frame uint16 length | message | uint16 CRC16 reassembled into one netbuf — the base64/line framing is stripped before the netbuf. So the limit is simply buf_size - 4 (2-byte length + 2-byte CRC16), and buf_count is irrelevant to a single serial message (one netbuf per message).
Boundary, confirmed at three sizes (client allowed to send past the old cap):
| buf_size | buf_size - 4 |
buf_size - 3 |
|---|---|---|
| 96 | accepted | dropped |
| 256 | accepted | dropped |
| 384 | accepted | dropped |
So the old base64_max(buf_size) - per_line_framing * (buf_size // line_length) was actually ~25-30% conservative (e.g. buf384 → 255 instead of 380), not over-permissive — and the floor-division line count you flagged fed that wrong framing term.
The fix drops the line-count dependency in Auto: max_unencoded_size = buf_size - (FRAME_LENGTH + CRC16). BufferParams mode (the encoded line-buffer model, e.g. MCUboot's 128×8 UART buffer) is unchanged. Locked by tests: a unit assert (buf_size - 4), an integration assert across the buffer matrix, and test_max_payload_roundtrip actually round-tripping a max-size message (including the sub-line-length buf96) against the real fixtures.
… buffer fixtures - Exclude tests/integration via --ignore (not -m 'not integration') so the marker expression's quoting doesn't break the PowerShell-run task on Windows. - QemuSocketSerialTransport accepts an explicit fragmentation_strategy. - serial recovery test uploads at both 128x1 (client default) and 128x8 (MCUboot default UART buffer), exercising 1- vs 8-line-packet transactions to the bootloader's SMP server. - Vendor qemu_cortex_m0 serial_buf256/buf512 so max-payload fill runs on a non-bursty target at those buffer sizes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…meout A full-buffer echo on the emulated cortex-m0 (nRF51) takes >2.5s under CI load; the default request timeout tripped test_max_payload_roundtrip[qemu...buf512]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The emulated nRF51 (16 KB RAM) hangs ~9% of the time on a full 512-byte-buffer echo, tripping the request timeout. buf256 and the 384 serial fixture are reliable, so 256-byte buffer fill is retained; reliable 512+ buffer-fill on a real-ish MCU is a build-farm gap (needs a roomier, non-bursty fixture). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l buffers Tighten test_max_payload_roundtrip to skip a bursty native_sim fixture only when the max-size message needs >2 line packets. This runs the round-trip on buf96 (sub-line-length, line_buffers=0) and buf256 even on native_sim, proving the server actually accepts a message sized at the client's max_unencoded_size -- a regression guard for the Auto buf_size math (re: PR review on non-multiple / sub-line-length buffers). Empirically buf_size bounds the server's decoded netbuf, so the current math is conservative (no overflow), and this test locks that in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…head) The Auto strategy modelled the server's MCUmgr buf_size as an *encoded* budget (base64_max(buf_size) - per-line framing), which both depended on a floor-division line-buffer count (PR review: inconsistent for non-multiples) and left ~25-30% of the buffer unused. But buf_size is CONFIG_MCUMGR_TRANSPORT_NETBUF_SIZE, which bounds the *decoded* SMP serial frame `length | message | CRC16` reassembled into one netbuf -- so the message is simply buf_size minus the 2-byte length + 2-byte CRC16. buf_count is irrelevant to a single serial message (one netbuf per message). Verified end-to-end against native_sim/QEMU/mps2 (buf 96/256/384): a buf_size-4 message round-trips; buf_size-3 is dropped. Tests: - unit: max_unencoded_size == buf_size - 4 after Auto initialize. - integration: test_auto_config asserts the exact relation across the matrix, and test_max_payload_roundtrip round-trips a max-size message (incl. buf96/buf256). - per-fixture max_reliable_line_packets steers full-buffer transactions onto roomy targets (native_sim PTY drops >2-packet bursts; emulated nRF51 hangs beyond ~3), so DFU runs to completion on mps2 (Cortex-M3) and skips the constrained nRF51. BufferParams mode (encoded line-buffer model, e.g. MCUboot 128x8) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion across the matrix Bump vendored SMP server fixtures 2ad0d6bf -> 0eae053d (the build farm closing intercreate/smp-server-fixtures#4 and #5) and lean on the new fixtures: - mps2_an385 serial_recovery buffer matrix (buf400/512/1024/2048): a reliable, link-paced Cortex-M3 target that round-trips a real buffer-filling message where native_sim is bursty and qemu_cortex_m0 is too small. In app mode it advertises MCUmgr params, so Auto drives echo/fs/img at each buf_size. - native_sim serial_bigrx: large UART RX pool, so full multi-fragment messages round-trip on native_sim (no >2-fragment burst drop). - native_sim serial_line512: legacy-interop only (non-128 line length, for non-compliant devices; downstream users should never change line_length). Validate that smpclient MAXIMIZES per-request payload for best link throughput: assert_chunks_maximized() asserts the largest upload stride comes within a small framing budget of max_unencoded_size, wired through DFU, fs, and serial-recovery uploads. test_max_payload_roundtrip now confirms a buf_size-4 message round-trips at 2048 (2044 B unencoded, ~2.8k encoded) on the reliable matrix for the first time. The do-it-all recovery image's RAM-backed littlefs is unreliable for sizeable files on the large-netbuf variants (client uploads fine; server read-back flakes), so the fs round-trip skips recovery buf1024/buf2048; their buffer-fill stays covered by DFU (flash slot) and the max-payload echo. Integration: 199 passed, 65 skipped. task all: lint/mypy clean, 290 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| @override | ||
| async def connect(self, address: str, timeout_s: float) -> None: | ||
| self._reset_state() | ||
| loop = asyncio.get_event_loop() |
|
|
||
| ready_pattern = fixture.ready_pattern() | ||
| endpoint_future: asyncio.Future[Endpoint] | None = ( | ||
| None if ready_pattern is None else asyncio.get_event_loop().create_future() |
| mtu = PropertyMock() | ||
| max_unencoded_size = PropertyMock() | ||
|
|
| # Auto uses the full decoded netbuf: buf_size minus the 2-byte SMP serial frame | ||
| # length and 2-byte CRC16 (verified on native_sim/QEMU/mps2: buf_size-4 round-trips). | ||
| assert t.max_unencoded_size == 512 - 4 |
Related to #73
This is a breaking change.
I want to setup a simple Zephyr repo to produce test FWs. I'd like to include some QEMU builds so that we can setup automated regression testing for this sorta thing.
Thank you for the research, @joerchan!