Skip to content

Commit 183626b

Browse files
committed
fix: include scheduling in Modbus exchange budget
1 parent be9f6fc commit 183626b

12 files changed

Lines changed: 232 additions & 25 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,22 @@ jobs:
484484
- name: Test CSV viewers with real LVGL
485485
shell: pwsh
486486
run: python tools/test_csv_viewer.py --compiler clang --lvgl-library build/offline-ui/out/lvgl_host.lib --output build/csv-viewer-ci
487+
- name: Test Modbus TCP with loopback peers
488+
shell: pwsh
489+
run: ./tools/test_modbus_network.ps1
490+
- name: Retain Modbus TCP evidence
491+
uses: actions/upload-artifact@v4
492+
if: always()
493+
with:
494+
name: modbus-network-logs
495+
path: |
496+
build/modbus-network/compile.log
497+
build/modbus-network/result.log
498+
build/modbus-network/source.json
499+
build/modbus-network/*-wire.json
500+
build/modbus-network/sources/
501+
if-no-files-found: warn
502+
retention-days: 14
487503
- name: Retain CSV viewer UI evidence
488504
uses: actions/upload-artifact@v4
489505
if: always()

‎ROADMAP.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Phase 3 software progress has begun:
126126
- [x] Byte Lab, UDP Console, and SPI Console share a bounded RAM byte clipboard. It carries encoded payloads into explicit send/transfer drafts and complete UDP replies back to Byte Lab. Invalid, oversized, or truncated data cannot silently replace a valid copy; paste never transmits. SPI also rejects invalid or overlong drafts without filtering them into different bytes. Native UI/network/SPI-adapter checks and firmware build pass; physical panels and bus/network measurements remain open.
127127

128128
- [x] Modbus TCP provides explicit connection tests and confirmed function 01-04 reads, with cancellation and OTA blocking through socket cleanup. Saved replies support raw 16-bit words and unsigned/signed/Float32 register pairs in four byte orders without extra traffic. Reference/native checks, the firmware build and ST7121 Wi-Fi fixture tests pass for all four functions, single-byte fragmentation, saved views, exceptions, timeout and cancellation; physical device maps, further network faults and the second panel family remain open.
129+
- [x] Modbus TCP captures its five-second deadline at request acceptance, includes scheduling time in the displayed duration and rejects already expired workers before socket creation. All 24 actual app/LVGL/loopback scenarios pass, including seven delayed-start controls that fail against the preceding source and 25 timeout/retry pairs with stable handles and LVGL memory. Delayed reads/probes, remaining budgets, stale admission and Stop/Home ownership are checked with controlled native startup/clock boundaries. CI retains exact compiled sources, hashes, logs and peer request bytes. Physical ESP-IDF scheduling, lwIP/Wi-Fi timing and both panels remain open.
129130
- [x] Subnet Lab and the new device/network apps share strict decimal IPv4 parsing. Address/numeric fields retain invalid characters, line breaks, and excess length for rejection through Home/re-entry, with host tests proving no worker or traffic for invalid network input. Physical input paths remain unverified.
130131
- [x] UDP Console sends one confirmed 0-128-byte hex/ASCII datagram to a selected IPv4 endpoint, filters replies to that peer, and displays an explicitly bounded 512-byte preview. A three-second deadline, cancellation, source-port controls, immutable request/result identity, and OTA blocking cover its lifecycle. Native checks, firmware build and ST7121 Wi-Fi tests pass for 25 repeated echoes, reply copy, timeout, cancellation and 0/512/513-byte replies after fixing SDK task cleanup; broader network faults and the second panel family remain open.
131132
- [x] Subnet Lab calculates IPv4 networks, netmasks, broadcast and host ranges, and optional peer membership entirely offline, including /0 counts, /31 point-to-point links, and /32 host routes. All 33 prefixes match independent host reference checks; real LVGL input/lifecycle checks and the firmware build pass. Physical touch and both panel families remain open.

‎docs/architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Serial logging retains its existing stream and 96-byte path buffers. Start advan
5050

5151
[RTU Frames](modbus-rtu.md) is a separate offline app that builds eight-byte read requests into the shared clipboard and decodes complete pasted replies. It shares the pure read PDU decoder/value formatter with Modbus TCP and the CRC helper with Byte Lab. Input edits invalidate results once per edit sequence, avoiding per-character rendering allocations. Its leave hook detaches the keyboard and clears UI pointers; it owns no UART, timer, worker, network session or storage writer. A pasted frame cannot establish serial timing or its originating request address. Native checks and the 2026-09-29 firmware build pass. ST7121 passed the example decode and request handoff into stopped Serial; broader panel and wired-fixture checks remain open.
5252

53-
Modbus TCP uses a single bounded network worker for a connection test or one read request. The worker owns and closes its nonblocking socket; Stop/Home signals cancellation without waiting in LVGL. OTA checks its busy state through cleanup. It publishes raw register/bit values with the request identity; the UI formats that snapshot into 16-bit or selectable 32-bit views without new traffic. Float32 interpretation shares Byte Lab's pure binary32 decoder. Its framing/decoding module is host-testable and supports only read functions 01-04. See [Modbus TCP inspector](modbus-tcp.md).
53+
Modbus TCP uses a single bounded network worker for a connection test or one read request. Its locked request snapshot captures the acceptance time; the worker uses that original five-second budget and includes scheduling time in the result duration. An already expired worker creates no socket. The worker owns and closes its nonblocking socket; Stop/Home signals cancellation without waiting in LVGL. OTA checks its busy state through cleanup. It publishes raw register/bit values with the request identity; the UI formats that snapshot into 16-bit or selectable 32-bit views without new traffic. Float32 interpretation shares Byte Lab's pure binary32 decoder. Its framing/decoding module is host-testable and supports only read functions 01-04. See [Modbus TCP inspector](modbus-tcp.md).
5454

5555
Network Diagnostics creates one task per accepted operation and releases it after cleanup. It shares `network_resolver.c` with HTTP: two locked lookup slots attach worker-owned state, while queued dispatch and late DNS replies carry IDs instead of caller pointers. The core copies each name before calling lwIP; cached-answer buffers match the SDK's configured capacity and callers receive at most four addresses. Diagnostics includes task scheduling in its ten-second DNS wait and requests cancellation on Cancel/Home. Its four raw echo probes use `network_ping.c`, which owns one nonblocking socket, checks cancellation around I/O and gaps, applies absolute probe waits and a six-second budget after DNS, and validates replies against the selected source and fresh payload. mDNS retains busy ownership through its existing three-second query and result cleanup. Native worker/LVGL and socket/clock checks cover these paths; physical latency and routing remain open. See [Network Diagnostics](network-diagnostics.md).
5656

‎docs/hardware-smoke-checklist.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Use current-limited power and 3.3 V signals. Disconnect external loads before bo
6565

6666
- [ ] In Modbus TCP, use the [Python fixture](modbus-tcp.md) on a reachable PC: test TCP, read all four functions and boundary addresses, compare decimal/hex/bit values, and confirm exception/fragmented/timeout responses. Check disconnected Wi-Fi, unreachable/closed port, field changes and confirmation expiry, Stop/Home during each network phase, re-entry during cancellation, and OTA refusal until the worker is idle. Repeat on both panel families and record heap before/after repeated operations.
6767
- [ ] In Modbus TCP, compare the 32-bit unsigned/signed/Float32 views against a device's documented register map in each applicable byte order. Verify raw words, pairing from the original address, an odd last register, negative zero, NaN/infinity, keyboard/result visibility, and readable selector popups. Changing the view or editing the next request must preserve the earlier result's identity and send no traffic; Home/re-entry retains the view and closes an open popup. Pure references and host UI/network checks cover these paths; physical panel and device results remain open.
68+
- [ ] Measure Modbus accepted-request timing while other permitted work is active. The five-second exchange budget and displayed duration include worker scheduling; an already expired worker must create no socket. Check Stop/Home before the worker runs, re-entry while cancellation is pending and successful fresh retries, retaining OTA ownership through socket cleanup. Native held-thread/clock controls cover these paths; physical ESP-IDF/lwIP timing and both panels remain unverified.
6869

6970
- [ ] In NTP Lab, Modbus TCP, Wake-on-LAN, and UDP Console, reject abbreviated/octal/leading-zero IPv4 addresses, signs/suffixes/exponents in numeric fields, and overlong input without any network traffic. Paste line breaks inside addresses, MAC addresses, and numeric fields; ensure they remain invalid through Home/re-entry. Repeat split-line rejection in Electronics, Subnet Lab, and Resistor Lab, verifying that old results clear and fields remain readable on both panels. Native regression tests pass; tablet input paths and lwIP behavior remain open.
7071

‎docs/modbus-tcp.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The result identifies the request it belongs to. The default register view inclu
1818

1919
**Test TCP** connects to the selected address and port without sending a Modbus request. Success proves that the TCP connection opened, not that the endpoint implements Modbus. It can also check an explicitly selected port on another network device.
2020

21-
Each exchange has a five-second deadline covering connection, transmission, and response. **Stop** or Home requests cancellation; the worker closes its socket before becoming idle. Socket cleanup can add a short scheduler delay beyond the exchange deadline. Firmware installation is blocked while it is still active. There is no retry or reconnect loop. Form values remain in RAM until restart.
21+
Each exchange has a five-second deadline starting when the confirmed read or TCP test is accepted, covering worker scheduling, connection, transmission, and response. A worker first scheduled after that deadline reports a timeout before opening a socket. The displayed duration includes scheduling time. **Stop** or Home requests cancellation; the worker closes its socket before becoming idle. Socket cleanup can add a short scheduler delay beyond the exchange deadline. Firmware installation is blocked while it is still active. There is no retry or reconnect loop. Form values remain in RAM until restart.
2222

2323
The framing and function handling follow the Modbus Organization's [application protocol specification](https://www.modbus.org/file/secure/modbusprotocolspecification.pdf) and [TCP/IP implementation guide](https://www.modbus.org/file/secure/messagingimplementationguide.pdf).
2424

@@ -66,6 +66,8 @@ The actual app source also passed real loopback TCP tests with LVGL, native Wind
6666

6767
Pure view tests cover all four byte orders, integer limits, Float32 special values, single/odd register counts, maximum addresses, and bounded output. An independent Python reference checks all three 32-bit views for 4,096 register/order cases.
6868

69-
Run `./tools/test_modbus_network.ps1` on Windows to repeat all 17 scenarios, or add `-Modes ui,views` for a focused display run. They also check that invalid IPv4/numeric/Unicode drafts survive editing and Home, and that invalid input, an expired confirmation, or a changed field sends no request. The view scenario verifies actual replies, order changes without additional traffic, original request identity, and unpaired registers; the UI scenario checks 100 lifecycle cycles and writes PPM previews under `build/modbus-network/`. The runner needs Python, the downloaded managed LVGL source, a native C compiler, CMake, and Ninja; it builds the host LVGL library if missing. Its server binds only to loopback and stops after the tests.
69+
Run `./tools/test_modbus_network.ps1` on Windows to repeat all 24 scenarios, or add `-Modes ui,views` for a focused display run. They also check that invalid IPv4/numeric/Unicode drafts survive editing and Home, and that invalid input, an expired confirmation, or a changed field sends no request. The view scenario verifies actual replies, order changes without additional traffic, original request identity, and unpaired registers; the UI scenario checks 100 lifecycle cycles and writes PPM previews under `build/modbus-network/`. The runner needs Python, the downloaded managed LVGL source, a native C compiler, CMake, and Ninja; it builds the host LVGL library if missing. Its server binds only to loopback and stops after the tests.
70+
71+
Seven delayed-start scenarios hold the native worker and advance its controlled monotonic clock. They verify expired reads and probes open no socket, a remaining budget permits a reply but is not renewed, Stop/Home retain cancellation and busy ownership, and reported duration includes the wait. Twenty-five timeout/retry pairs check fresh deadlines, independent peer request counts, stable native handles and LVGL memory after closing the UI. All seven scenarios fail their deadline/duration controls against the preceding app source. These checks use real LVGL, native threads and TCP peers; they do not establish ESP-IDF scheduling, lwIP timing or physical Wi-Fi latency. CI retains exact compiled sources, hashes, result logs and peer request bytes as `modbus-network-logs`.
7072

7173
The 2026-09-29 ST7121 pass used USB-injected touches and a PC peer over Wi-Fi. It passed the connect-only probe, FC01-04 reads fragmented into single bytes, saved integer/Float32 and word-order views without extra traffic, popup/Home retention, exception `0x06`, the five-second timeout, and Stop/Home cancellation. A stopped PC listener timed out rather than returning an immediate refusal. See the firmware identity and exact scope in the [smoke checklist](hardware-smoke-checklist.md#usb-only-development-pass-2026-09-29). Physical device maps, Wi-Fi loss, memory-pressure faults, task stack high-water and the second panel family remain open.

‎docs/testing.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,9 @@ The Serial runner also checks bounded RX capture across reads, freeze/Stop/Home
545545

546546
`./tools/test_network_ui.ps1` checks the actual Diagnostics app, shared resolver and LVGL using native Windows threads and synthetic DNS/ping/mDNS services. It requires native Clang, CMake/Ninja and the cached Debug LVGL library, which it never rebuilds. Its 34 scenarios run with both one-address and four-address DNS configurations: lookup/cache/failure/timeout, bounded answer copies, two concurrent clients, Cancel/Home and stale callbacks, original target after edits, pending completion, mDNS ownership through query/result cleanup, failed task creation, retained excess input, disconnected/refresh behavior and 25 lifecycles with unchanged UI allocation and Windows handle counts. It checks all five buttons fit their action row. Use `-Modes home-dns,mdns-cleanup,resolver-concurrent`; logs and PPMs go under `build/network-ui/`. The native CI job runs both complete configurations and retains `ci.log` plus their case logs. These 68 checks use no raw socket, external DNS or tablet and do not establish physical cancellation latency or resolver-table recovery.
547547

548-
On Windows, `./tools/test_modbus_network.ps1` tests the actual Modbus app against a loopback-only Python fixture using real LVGL, sockets, and threads. Its 17 scenarios cover input preservation/rejection, read functions, 32-bit views, fragmented and invalid responses, confirmation gates, timeout/cancellation, and repeated resource cleanup. Use `-Modes ui,views` to check layouts and saved-reply interpretations; rendered PPMs go under `build/modbus-network/`. It uses the same native build tools plus Python and reuses the offline LVGL build, creating it if missing. The native adapters do not emulate ESP-IDF/lwIP or Wi-Fi hardware.
548+
On Windows, `./tools/test_modbus_network.ps1` tests the actual Modbus app against a loopback-only Python fixture using real LVGL, sockets, and threads. Its 24 scenarios cover input preservation/rejection, read functions, 32-bit views, fragmented and invalid responses, confirmation gates, timeout/cancellation, and repeated resource cleanup. Seven delayed-start scenarios hold the native thread before entering the actual worker and advance its controlled monotonic clock. Expired reads/probes must create no socket; remaining time is shared with I/O, duration includes the wait, and Stop/Home cannot release busy ownership early. Twenty-five timeout/retry pairs check fresh budgets, independent peer request counts and stable native handles/LVGL memory after closing the UI. All seven delayed-start controls fail against the preceding app. The native adapters do not emulate ESP-IDF/lwIP or verify physical Wi-Fi timing.
549+
550+
Use `-Modes ui,views` to check layouts and saved-reply interpretations; rendered PPMs go under `build/modbus-network/`. `-ToolSource` selects a preceding app source for regression controls, and `-Output` selects an evidence directory. The runner compiles normalized source snapshots and executes its copied Python fixture, recording source/library/executable hashes, compiler/result logs and per-scenario peer request bytes. CI runs the complete suite after the existing LVGL checks and retains `modbus-network-logs` for 14 days, including available failure evidence. It uses the same native build tools plus Python and reuses the offline LVGL build, creating it if missing.
549551

550552
`./tools/test_device_network.ps1` uses the same Windows host setup for NTP Lab, Wake-on-LAN, and UDP Console. It covers 39 scenarios including invalid input without network activity, exact packet contents, Byte Lab/UDP clipboard round trips, oversized/wrong-peer replies, source-port conflicts, confirmation/cancellation, allocation failure, and repeated resource cleanup. Use `-Modes udp-clipboard,udp-echo,udp-home` to select cases. Logs and rendered PNGs go under `build/device-network/`. See [Device and network tools](device-network-tools.md) and [UDP Console](udp-console.md) for manual PC fixtures and remaining hardware validation.
551553

‎main/modbus_tool.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ typedef struct {
2626
modbus_request_t request;
2727
bool probe;
2828
bool present;
29+
int64_t accepted_us;
2930
uint32_t duration_ms;
3031
char status[160];
3132
modbus_result_t result;
@@ -125,7 +126,7 @@ static void modbus_worker(void *argument)
125126
portENTER_CRITICAL(&job_lock);
126127
job = published;
127128
portEXIT_CRITICAL(&job_lock);
128-
int64_t started = esp_timer_get_time();
129+
int64_t started = job.accepted_us;
129130
int64_t deadline = started + MODBUS_DEADLINE_US;
130131
int fd = -1;
131132
int error = operation_error(deadline);
@@ -363,6 +364,7 @@ static void start_clicked(lv_event_t *event)
363364
snprintf(job.status, sizeof(job.status), "%s in progress; STOP or Home cancels. Exchange limit: 5 seconds.",
364365
probe ? "TCP test" : "Read");
365366
portENTER_CRITICAL(&job_lock);
367+
job.accepted_us = esp_timer_get_time();
366368
published = job;
367369
job_cancelled = false;
368370
job_busy = true;

0 commit comments

Comments
 (0)