From bf3868664d4b78b6d20eda0fe1bedb23b48fc140 Mon Sep 17 00:00:00 2001 From: ewowi Date: Fri, 31 Jul 2026 11:16:48 +0200 Subject: [PATCH 1/3] Fix S31 ethernet transmit wedge: DMA buffer size and transmit mutex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An S31 streaming to panel cards degraded with uptime and then wedged entirely, recovering only on a reboot. Two sdkconfig lines fix it: the DMA buffer was smaller than an ethernet frame, and the transmit mutex was off while two tasks shared the interface. Bench: 19 000 refused frames and two wedges in 20 minutes, against a clean hour after. Performance: no tick-path change. The fix costs ~20 KB of internal DMA RAM on the S31 (both rings, unconditional). **Core** - ethSendFailCounts splits send failures by cause. esp_eth_transmit refuses a down link BEFORE touching the MAC (ESP_ERR_INVALID_STATE) and a full ring after (ESP_ERR_NO_MEM); one counter could not tell them apart, which is what made the bug unreadable for hours. Every failure here turned out to be the ring. - ethRestartTx re-runs link negotiation and resets the descriptor rings โ€” the only way back from a wedge, since no ioctl writes the driver's link flag. **Light domain** - PanelCardDriver attempts recovery once per wedge and reports what happened. The wedge check now runs BEFORE the link check: a wedge is defined by sends failing, which is knowable whatever the link claims, and checking the link first made the branch unreachable wherever the platform reports no link. - The status line carries the split: "1000 Mbit - 3432 pkt/s, N lost (x link, y ring)". **Scripts/MoonDeck** - repo_health reads its delta baseline from git rather than the working tree, so repeated runs give identical deltas instead of comparing against the previous run, and the delta covers all pending changes. Carry-forward still reads the working tree โ€” it wants the newest numbers, the delta wants the committed ones. **Tests** - 26 unit tests (was 21): the wedge detection, the once-per-wedge bound, a wedge that survives its restart, and the cause-split counter. **Docs/CI** - lessons.md: the descriptor arithmetic, the mutex race, and the two wrong turns โ€” misreading a rate test as ruling out back-pressure, and one counter serving two faults. - The driver's catalog screenshot, regenerated against the fixed firmware. **Reviews** - ๐Ÿ‘พ the once-per-wedge bound reset on a merely-healthy link, so a surviving wedge could bounce the interface every ~20 s -> now gated on frames flowing. - ๐Ÿ‘พ ethRestartTx's return was dropped; a failed start left ethernet stopped and reported as an unplugged cable -> now its own error. - ๐Ÿ‘พ desktop ethSendFailCounts returned the streak, not a cumulative total -> fixed; the status would have read (0 link, 0 ring) while drops climbed. - ๐Ÿ‘พ repo_health carry-forward would revert fresh perf numbers to the committed ones -> delta and carry-forward now read from different sources. - ๐Ÿ‘พ a comment orphaned from its function by this change -> moved back. - ๐Ÿ‘พ statusBuf_ sized for the wrong line; truncation would cut the ring count -> 96 B. - ๐Ÿ‘พ "~46 KB" read as a saving when it is a total, and the size bump's own ~20 KB was unstated -> both corrected. - ๐Ÿ‘พ new behaviour unpinned -> tests added, which is what exposed the unreachable wedge branch above. Bench-isolated, so only what earns its place ships: buffer size and the mutex are both load-bearing (removing the mutex alone returned 3 000 drops in 4 minutes), while a 30-deep ring ran no cleaner than 10 and was reverted. Co-Authored-By: Claude Opus 5 (1M context) --- docs/assets/light/drivers/PanelCardDriver.png | Bin 99147 -> 96001 bytes docs/history/lessons.md | 17 ++++ docs/metrics/repo-health.json | 42 ++++----- docs/metrics/repo-health.md | 32 +++---- esp32/sdkconfig.defaults.esp32s31 | 19 +++- moondeck/check/repo_health.py | 39 +++++++- src/light/drivers/PanelCardDriver.h | 58 +++++++++--- src/platform/desktop/platform_desktop.cpp | 22 ++++- src/platform/esp32/platform_esp32.cpp | 27 +++++- src/platform/platform.h | 22 +++++ test/unit/light/unit_PanelCardDriver.cpp | 83 ++++++++++++++++++ 11 files changed, 305 insertions(+), 56 deletions(-) diff --git a/docs/assets/light/drivers/PanelCardDriver.png b/docs/assets/light/drivers/PanelCardDriver.png index 2b7dec4becf486e6faed739f0605065dee21121f..4ae69e688cbbd6cfbe6fd246c5b7c907331f6b70 100644 GIT binary patch literal 96001 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

hKR^Ea?yOWmko5Gk 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

  • =Gr}ICT1#@qRKA(n@a?#S4^GIf_Uo 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

    ~!>W|E>uiPfTphkUMSqQcQ>fg#>UgWi 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

    p*$SU~ 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 YMma(SxCSsUXn>dcb#0YgB{Tp30Y^;Cv;Y7A diff --git a/docs/history/lessons.md b/docs/history/lessons.md index 870c5fcc..e916a1ad 100644 --- a/docs/history/lessons.md +++ b/docs/history/lessons.md @@ -399,3 +399,20 @@ The leading untested theory is that a short frame gives the DMA no runway to pre - **Read a control's applicability before drawing conclusions from its value.** `doubleBuffer`, `useRing` and `ringSnapshot` all appear in `/api/state` while being inapplicable to this path (`supportsDoubleBuffer()` is hard `false` here; the ring controls are hidden unless `pinExpanderMode()`, and `wantsRing()` returns false in direct mode outright). Two hypotheses were built on those values and both were dead ends โ€” the UI hides them for a reason, the API does not. - **A frame-time KPI that does not scale with the frame is measuring a timeout, not a wire.** `frameTime` read ~741 ยตs flat from 10 to 600 lights. Flat where it should scale is a signal in itself. - **A fix that works is not a cause that is understood.** The pool swap reliably removes the symptom, which is tempting to write up as a root cause; the arithmetic says the mechanism is still unknown. + +## A DMA buffer smaller than the frame multiplies descriptor use โ€” and the symptom looked exactly like a failing cable + +Streaming panel-card frames from an S31 at ~5 300 packets/s degraded with uptime: clean for minutes, then refused frames, then a total transmit wedge every ~11 minutes that only a reboot cleared. It read as a hardware fault the whole way โ€” and it was two config lines. + +**The arithmetic that caused it.** `CONFIG_ETH_DMA_BUFFER_SIZE` defaults to 512 B. A panel-card frame is 1512 B, so every frame consumed **three** descriptors. A 10-descriptor TX ring therefore held ~3.3 frames while the driver fires 132 back-to-back. Setting the buffer to 1536 B (64-byte aligned) gives one descriptor per frame and the failures stop. **Check DMA buffer size against your actual frame size; the default is sized for IP MTU traffic, not for a burst sender.** + +**The second half is a race, not a size.** `CONFIG_ETH_TRANSMIT_MUTEX` defaults to off, and `mac->transmit` advances a shared descriptor pointer with no locking. With the eth netif up, lwIP and the render task both reach it. Bench-isolated: buffer fixed but mutex off โ†’ 3 000 refused frames in 4 minutes; mutex on with only 10 descriptors โ†’ none in 11. **Ring depth was never the fix** โ€” 30 descriptors ran no cleaner than 10, and a 30-deep TX ring totals ~46 KB of internal RAM against ~15 KB at 10. + +**Why it cost hours: two wrong turns worth naming.** +- *"It is not back-pressure."* Halving the send rate left the failure rate at ~13-15%, which reads as ruling out a full ring. It does not: at 3.3 frames of depth each 132-packet burst overruns the ring at any rate. The burst was the problem, never the rate. That misreading sent the investigation toward the cable and the PHY. +- *One counter for two faults.* `esp_eth_transmit` returns `ESP_ERR_INVALID_STATE` for a down link **before** touching the MAC, and `ESP_ERR_NO_MEM` for a full ring. Collapsing both into one bool made "5 million drops" unreadable. Splitting them settled the cause in one build โ€” every failure was `ring`, none was `link`. **When a counter can be incremented by two different faults, split it before theorising.** + +**What the physical evidence did and did not prove.** Both link LEDs going dark said the wire genuinely dropped โ€” true, and it correctly killed the "stale software flag" theory. But it does not identify a cause: a wedged MAC stops driving the wire, so the dark LED was a *consequence*. A longer cable was the visible difference from the working setup and looked compelling; it was untouched throughout and is exonerated. + +**Recovery still earns its place.** `esp_eth_stop()` + `esp_eth_start()` re-runs negotiation and resets the descriptor rings โ€” the only way back from a wedge short of a reboot, since no ioctl writes the driver's link flag. Bench-verified twice, recovering in ~17 s. Attempted once per wedge, never repeatedly: a restart cannot fix an unplugged cable, and retrying would bounce the interface under the user. + diff --git a/docs/metrics/repo-health.json b/docs/metrics/repo-health.json index 5f76ce9e..08e7370f 100644 --- a/docs/metrics/repo-health.json +++ b/docs/metrics/repo-health.json @@ -1,18 +1,18 @@ { - "commit": "1aff3df2", + "commit": "49795a4f", "flash": { "esp32": 1678960, - "esp32p4-eth": 1502192, + "esp32p4-eth": 1503232, "esp32p4-eth-wifi": 1793760, "esp32s3-n16r8": 1667216, "esp32s3-n8r8": 1666992, - "esp32s31": 1923856, - "desktop": 945336 + "esp32s31": 1925008, + "desktop": 945656 }, "perf": { "desktop": { - "tick_us": 125, - "fps": 8000 + "tick_us": 128, + "fps": 7812 }, "esp32": { "tick_us": 4164, @@ -21,11 +21,11 @@ }, "loc": { "core": 14889, - "light": 20150, - "platform": 12428, + "light": 20191, + "platform": 12493, "ui": 5811, - "test": 35207, - "moondeck": 19914 + "test": 35324, + "moondeck": 19943 }, "comments": { "core": { @@ -33,41 +33,41 @@ "ratio": 0.409 }, "light": { - "lines": 7783, - "ratio": 0.426 + "lines": 7804, + "ratio": 0.427 }, "platform": { - "lines": 4149, - "ratio": 0.369 + "lines": 4177, + "ratio": 0.37 }, "ui": { "lines": 1518, "ratio": 0.278 }, "test": { - "lines": 6035, + "lines": 6053, "ratio": 0.198 }, "moondeck": { - "lines": 3182, + "lines": 3187, "ratio": 0.183 } }, "tests": { - "cases": 1002, + "cases": 1007, "scenarios": 22 }, "docs": { "md_files": 170, - "md_lines": 22762, + "md_lines": 22809, "plans_files": 90, "backlog_lines": 3114, - "lessons_lines": 401, + "lessons_lines": 418, "claude_md_lines": 135 }, "complexity": { - "functions": 2179, - "over_threshold": 138, + "functions": 2187, + "over_threshold": 139, "worst_ccn": 93 } } diff --git a/docs/metrics/repo-health.md b/docs/metrics/repo-health.md index 8a83c97b..1a690e37 100644 --- a/docs/metrics/repo-health.md +++ b/docs/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `1aff3df2`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run โ€” **do not edit by hand**. +Measured at `49795a4f`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run โ€” **do not edit by hand**. Current state only; the trend is this file's git history (`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. @@ -8,45 +8,45 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Flash | |---|---:| -| desktop | 923 KB (โˆ’0 KB) โœ“ | +| desktop | 923 KB (+0 KB) โš  | | esp32 | 1,640 KB | -| esp32p4-eth | 1,467 KB (+1 KB) โš  | +| esp32p4-eth | 1,468 KB (+1 KB) โš  | | esp32p4-eth-wifi | 1,752 KB | -| esp32s3-n16r8 | 1,628 KB (+0 KB) โš  | +| esp32s3-n16r8 | 1,628 KB | | esp32s3-n8r8 | 1,628 KB | -| esp32s31 | 1,879 KB (โˆ’0 KB) โœ“ | +| esp32s31 | 1,880 KB (+1 KB) โš  | ## Render performance | Target | Tick | FPS | |---|---:|---:| -| desktop | 125 ยตs (โˆ’3 ยตs) โœ“ | 8,000 (+188) โœ“ | +| desktop | 128 ยตs (+3 ยตs) โš  | 7,812 (โˆ’188) โš  | | esp32 | 4,164 ยตs | 240 | ## Code | Area | Lines | Comments | Comment share | |---|---:|---:|---:| -| core | 14,889 (โˆ’1) โœ“ | 5,583 | 40.9 % | -| light | 20,150 (+3) โš  | 7,783 | 42.6 % | -| platform | 12,428 (โˆ’8) โœ“ | 4,149 | 36.9 % | +| core | 14,889 | 5,583 | 40.9 % | +| light | 20,191 (+41) โš  | 7,804 | 42.7 % (+0.1 %) โš  | +| platform | 12,493 (+65) โš  | 4,177 | 37.0 % (+0.1 %) โš  | | ui | 5,811 | 1,518 | 27.8 % | -| test | 35,207 | 6,035 | 19.8 % | -| moondeck | 19,914 | 3,182 | 18.3 % | +| test | 35,324 (+117) โš  | 6,053 | 19.8 % | +| moondeck | 19,943 (+29) โš  | 3,187 | 18.3 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 1,002 | +| unit cases | 1,007 (+5) โœ“ | | scenarios | 22 | ## Complexity | Metric | Value | |---|---:| -| functions | 2,179 (โˆ’3) โš  | -| over threshold | 138 | +| functions | 2,187 (+8) โœ“ | +| over threshold | 139 (+1) โš  | | worst CCN | 93 | ## Documentation @@ -54,9 +54,9 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Metric | Value | |---|---:| | markdown files | 170 | -| markdown lines | 22,762 | +| markdown lines | 22,809 (+47) โš  | | plan files | 90 | | backlog lines | 3,114 | -| lessons lines | 401 | +| lessons lines | 418 (+17) โš  | | CLAUDE.md lines | 135 | diff --git a/esp32/sdkconfig.defaults.esp32s31 b/esp32/sdkconfig.defaults.esp32s31 index 9f253300..ce0627fb 100644 --- a/esp32/sdkconfig.defaults.esp32s31 +++ b/esp32/sdkconfig.defaults.esp32s31 @@ -45,9 +45,26 @@ CONFIG_ETH_USE_ESP32_EMAC=y # XTAL. The APLL is a *fractional* PLL built for exact frequencies (its reason to exist), so # it synthesises 125 MHz precisely. Bench-proven on the S31 CoreBoard. CONFIG_ETH_EMAC_RGMII_TX_CLK_SRC_APLL=y -CONFIG_ETH_DMA_BUFFER_SIZE=512 +# A panel-card frame is 1512 B. At the 512 B default every frame spanned THREE descriptors, so a +# 10-descriptor ring held only ~3 frames while PanelCardDriver fires 132 back-to-back โ€” and the ring +# ran dry within minutes (bench: ~19 000 refused frames and two full transmit wedges in 20 minutes). +# 1536 is the 64-byte-aligned size that fits one frame per descriptor. +# +# The COUNT stays at 10: bench-isolated on 2026-07-31 โ€” buffer size and the transmit mutex below are +# what fix it, and 30 descriptors ran no cleaner than 10 (a 30-deep TX ring totals ~46 KB against +# ~15 KB at 10). The size bump itself is not free: 512โ†’1536 applies to BOTH rings, so 20 buffers +# cost ~20 KB more internal RAM than the default. Cheaper than the alternative, and measured. +CONFIG_ETH_DMA_BUFFER_SIZE=1536 CONFIG_ETH_DMA_RX_BUFFER_NUM=10 CONFIG_ETH_DMA_TX_BUFFER_NUM=10 +# The eth netif is up alongside the raw-L2 sender, so lwIP and the render task both reach +# mac->transmit, which advances a shared descriptor pointer with no locking of its own. IDF's help +# text names exactly this case: "Ethernet interface is used as shared resource and multiple +# functionalities might try to access it at a time." Default is off. +# +# Load-bearing, not belt-and-braces: with it off (and 30 descriptors) the bench saw 3 000 refused +# frames in 4 minutes; with it on and only 10 descriptors, none in 11. +CONFIG_ETH_TRANSMIT_MUTEX=y # MoonLive native codegen needs an executable heap (allocExec โ†’ MALLOC_CAP_EXEC). Like the # S3 and P4, the S31 has hardware memory protection that IDF couples to disabling the exec diff --git a/moondeck/check/repo_health.py b/moondeck/check/repo_health.py index ae1db237..4f41d044 100644 --- a/moondeck/check/repo_health.py +++ b/moondeck/check/repo_health.py @@ -203,6 +203,39 @@ def snapshot(perf=None): def load_previous(): + """The COMMITTED snapshot, read from git rather than from the working tree. + + The delta is meant to answer "what did this commit change", so the baseline has to be the last + commit โ€” not whatever a previous run left on disk. Reading the working-tree file made every run + after the first compare against the run before it, so a second run inside one commit showed a + delta of ~0 and the real change vanished. Running the check twice must give the same answer. + + Falls back to the working-tree file when git cannot answer (a fresh clone with no commit yet, or + the file untracked), which keeps a first-ever run working. + """ + try: + out = subprocess.run(["git", "show", f"HEAD:{HEALTH_FILE.relative_to(ROOT).as_posix()}"], + cwd=ROOT, capture_output=True, text=True, timeout=10) + if out.returncode == 0: + return json.loads(out.stdout) + except (subprocess.SubprocessError, OSError, json.JSONDecodeError): + pass + if not HEALTH_FILE.exists(): + return {} + try: + return json.loads(_read(HEALTH_FILE)) + except json.JSONDecodeError: + return {} + + +def load_working_tree(): + """The snapshot on disk, whatever produced it. + + Carry-forward needs the NEWEST numbers, not the committed ones: a board-attached run writes fresh + perf figures here, and a later boardless run must preserve them rather than reverting to what the + last commit happened to hold. That is the opposite of what the delta baseline wants, which is why + the two read from different places. + """ if not HEALTH_FILE.exists(): return {} try: @@ -349,8 +382,8 @@ def render_markdown(new, old): def write(perf=None, quiet=False): """Measure, print the delta, and rewrite both views. Called by the KPI gate.""" - old = load_previous() - new = merge_carry_forward(snapshot(perf), old) + old = load_previous() # committed โ†’ what the delta compares against + new = merge_carry_forward(snapshot(perf), load_working_tree()) # newest โ†’ what unmeasured metrics keep if not quiet: for line in format_delta(new, old): print(line) @@ -366,7 +399,7 @@ def main(): args = parser.parse_args() old = load_previous() - new = merge_carry_forward(snapshot(), old) + new = merge_carry_forward(snapshot(), load_working_tree()) for line in format_delta(new, old): print(line) if args.write: diff --git a/src/light/drivers/PanelCardDriver.h b/src/light/drivers/PanelCardDriver.h index ffbfd286..dbe6ade0 100644 --- a/src/light/drivers/PanelCardDriver.h +++ b/src/light/drivers/PanelCardDriver.h @@ -260,10 +260,11 @@ class PanelCardDriver : public DriverBase { /// class note says why a slow link is reported rather than refused. Runs on the 1 Hz path, so /// the snprintf here is the same accepted trade SystemModule makes. void writeLinkStatus() MM_NONBLOCKING { - if (!platform::ethLinkUp()) { - setStatus("no ethernet link", Severity::Warning); - return; - } + // The WEDGE is checked before the link, because a wedged transmit path is defined by sends + // failing โ€” which is knowable whatever the link claims. Checking the link first hid this + // branch entirely on any platform reporting no link (the desktop stub among them), which is + // how the recovery path shipped unreachable by its own tests. + // // A short streak is ordinary back-pressure: the DMA ring fills while we push a whole frame // in one tick, the frame is dropped, the next one goes. Measured on a 128x128 wall at ~1900 // packets/s, streaks of 1-4 come and go and always clear themselves โ€” reporting those as an @@ -276,13 +277,32 @@ class PanelCardDriver : public DriverBase { static constexpr uint32_t kWedgedStreak = 500; const uint32_t failStreak = platform::ethSendFailStreak(); if (failStreak >= kWedgedStreak) { - std::snprintf(statusBuf_, sizeof(statusBuf_), "transmit wedged (%u refused)", - static_cast(failStreak)); + if (!restartTried_) { + restartTried_ = true; + // A failed restart leaves the driver STOPPED: no CONNECTED event can follow, so the + // link reads down forever and nothing else here can tell that apart from an unplugged + // cable. Report it as its own error rather than letting it masquerade as one. + if (platform::ethRestartTx()) { + setStatus("transmit wedged - restarting ethernet", Severity::Warning); + } else { + setStatus("ethernet restart failed - restart the device", Severity::Error); + } + framesReported_ = framesSent_; + return; + } + std::snprintf(statusBuf_, sizeof(statusBuf_), "transmit wedged (%u refused, link %u Mbit)", + static_cast(failStreak), + static_cast(platform::ethLinkSpeedMbps())); setStatus(statusBuf_, Severity::Error); framesReported_ = framesSent_; return; } + if (!platform::ethLinkUp()) { + setStatus("no ethernet link", Severity::Warning); + framesReported_ = framesSent_; + return; + } const uint16_t mbps = platform::ethLinkSpeedMbps(); // Total frames the MAC refused since boot. A dropped frame is tolerated (the cards have no // acknowledgement, so a retry would cost the next frame instead), but it is NOT invisible: @@ -300,10 +320,22 @@ class PanelCardDriver : public DriverBase { setStatus(statusBuf_, Severity::Warning); return; } + // Re-arm the one-shot only on evidence of FLOW: frames sent this second and no failure + // streak at all. Re-arming merely because the link reads healthy would fire again while a + // wedge is still rebuilding its streak, bouncing the interface every ~20 s โ€” the loop the + // one-shot exists to prevent. + if (sent > 0 && failStreak == 0) restartTried_ = false; + if (dropped) { - std::snprintf(statusBuf_, sizeof(statusBuf_), "%u Mbit - %u packets/s, %u dropped", + // Split by cause: a flapping link and a full TX ring are different faults with + // different fixes, and one total cannot tell them apart. + uint32_t linkDown = 0, ringFull = 0; + platform::ethSendFailCounts(linkDown, ringFull); + std::snprintf(statusBuf_, sizeof(statusBuf_), + "%u Mbit - %u pkt/s, %u lost (%u link, %u ring)", static_cast(mbps), static_cast(sent), - static_cast(dropped)); + static_cast(dropped), + static_cast(linkDown), static_cast(ringFull)); } else { std::snprintf(statusBuf_, sizeof(statusBuf_), "%u Mbit - %u packets/s", static_cast(mbps), static_cast(sent)); @@ -365,8 +397,14 @@ class PanelCardDriver : public DriverBase { /// Frames the MAC refused since boot. Cumulative on purpose: the per-second rate hides a slow /// trickle of drops, and a rising total is the signal that the sender is outrunning the wire. uint32_t framesDroppedTotal_ = 0; - /// Backing store for the status line (setStatus does not copy). - char statusBuf_[48] = {}; + /// Backing store for the status line (setStatus does not copy). Sized for the longest one: the + /// split-drop report, which carries three cumulative counters and reaches ~63 chars at millions + /// of drops. Truncation would cut the RING count โ€” the number the line exists to show. + char statusBuf_[96] = {}; + /// Whether a restart has already been attempted for the CURRENT wedge. One attempt per wedge: + /// a restart cannot fix an unplugged cable, and retrying every second would bounce the interface + /// under the user. Cleared as soon as frames flow again. + bool restartTried_ = false; /// Whether this driver currently holds the raw-L2 claim, so prepare/release stay balanced /// however often the framework calls them. bool claimed_ = false; diff --git a/src/platform/desktop/platform_desktop.cpp b/src/platform/desktop/platform_desktop.cpp index 080869c9..8d6e60f3 100644 --- a/src/platform/desktop/platform_desktop.cpp +++ b/src/platform/desktop/platform_desktop.cpp @@ -719,7 +719,9 @@ size_t ethTestCount_ = 0; bool ethTestSendFails_ = false; uint16_t ethTestLinkSpeed_ = 1000; // desktop reports gigabit unless a test says otherwise int ethRawClaims_ = 0; // drivers holding the link for direct L2 use -uint32_t ethSendFails_ = 0; // consecutive ethSendRaw failures +uint32_t ethSendFails_ = 0; // consecutive ethSendRaw failures (the streak) +uint32_t ethFailTotal_ = 0; // cumulative since boot โ€” what ethSendFailCounts reports +uint32_t ethRestarts_ = 0; // ethRestartTx() calls, for the once-per-wedge test // The bound raw socket, or -1 for capture mode (the default, and all any test sees). int ethRawFd_ = -1; unsigned ethRawIfIndex_ = 0; // Linux AF_PACKET needs the index; BPF binds by name @@ -776,7 +778,7 @@ bool ethBindRawInterface(const char* ifName) { // what every unit test exercises; the raw branch is what makes a host a panel controller. bool ethSendRaw(const uint8_t* frame, size_t len) MM_NONBLOCKING { if (!frame || len == 0) return false; - if (ethTestSendFails_) { ethSendFails_++; return false; } // simulated link-down / full TX ring + if (ethTestSendFails_) { ethSendFails_++; ethFailTotal_++; return false; } // simulated link-down / full ring #ifndef _WIN32 if (ethRawFd_ >= 0) { @@ -793,7 +795,7 @@ bool ethSendRaw(const uint8_t* frame, size_t len) MM_NONBLOCKING { #endif // Track failures on the REAL send path too, not just the capture path: a bound host is // where frames actually reach a wire, so a streak here is the one that matters. - if (n != static_cast(len)) { ethSendFails_++; return false; } + if (n != static_cast(len)) { ethSendFails_++; ethFailTotal_++; return false; } ethSendFails_ = 0; return true; } @@ -819,6 +821,18 @@ bool ethSendRaw(const uint8_t* frame, size_t len) MM_NONBLOCKING { uint32_t ethSendFailStreak() MM_NONBLOCKING { return ethSendFails_; } +// A host socket has no driver link state to refuse against, so every failure is the +// ring-full analogue (a full socket buffer). +void ethSendFailCounts(uint32_t& linkDown, uint32_t& ringFull) MM_NONBLOCKING { + linkDown = 0; ringFull = ethFailTotal_; +} + +// A host raw socket has no driver-internal link state to desync, so there is nothing to +// restart โ€” clear the streak so a test can exercise the driver's recovery path. +bool ethRestartTx() { ethSendFails_ = 0; ethRestarts_++; return true; } + +uint32_t ethRestartCountForTest() { return ethRestarts_; } + // See platform.h: a claim stated by the driver, reference-counted. void ethClaimRawL2(bool claim) { if (claim) ethRawClaims_++; @@ -837,7 +851,7 @@ size_t ethTestFrameLength(size_t i) { return i < kEthTestMaxFrames ? ethTestLens const uint8_t* ethTestFrameData(size_t i) { return (ethTestFrames_ && i < kEthTestMaxFrames) ? ethTestFrames_[i] : nullptr; } -void ethTestClearFrames() { ethTestCount_ = 0; ethSendFails_ = 0; } +void ethTestClearFrames() { ethTestCount_ = 0; ethSendFails_ = 0; ethFailTotal_ = 0; } void setTestEthSendFails(bool fail) { ethTestSendFails_ = fail; } void setTestEthLinkSpeed(uint16_t mbps) { ethTestLinkSpeed_ = mbps; } void ethGetIPv4(uint8_t out[4]) MM_NONBLOCKING { diff --git a/src/platform/esp32/platform_esp32.cpp b/src/platform/esp32/platform_esp32.cpp index dcfc6f7c..1721ddea 100644 --- a/src/platform/esp32/platform_esp32.cpp +++ b/src/platform/esp32/platform_esp32.cpp @@ -937,18 +937,30 @@ bool ethRawL2Claimed() MM_NONBLOCKING { // Consecutive failures, so a caller can distinguish back-pressure from a wedged path (see // platform.h). Written on the render task, read by the driver's 1 Hz status tick. static std::atomic ethSendFails_{0}; +// Split by cause โ€” see platform.h. esp_eth_transmit checks the link BEFORE the MAC, so the two +// errors are genuinely distinct conditions rather than degrees of the same one. +static std::atomic ethFailLinkDown_{0}; +static std::atomic ethFailRingFull_{0}; bool ethSendRaw(const uint8_t* frame, size_t len) MM_NONBLOCKING { if (!ethHandle_ || !frame || len == 0) return false; if (!ethLinkUp_.load(std::memory_order_relaxed)) return false; - if (esp_eth_transmit(ethHandle_, const_cast(frame), len) != ESP_OK) { + const esp_err_t err = esp_eth_transmit(ethHandle_, const_cast(frame), len); + if (err != ESP_OK) { ethSendFails_.fetch_add(1, std::memory_order_relaxed); + if (err == ESP_ERR_INVALID_STATE) ethFailLinkDown_.fetch_add(1, std::memory_order_relaxed); + else if (err == ESP_ERR_NO_MEM) ethFailRingFull_.fetch_add(1, std::memory_order_relaxed); return false; } ethSendFails_.store(0, std::memory_order_relaxed); return true; } +void ethSendFailCounts(uint32_t& linkDown, uint32_t& ringFull) MM_NONBLOCKING { + linkDown = ethFailLinkDown_.load(std::memory_order_relaxed); + ringFull = ethFailRingFull_.load(std::memory_order_relaxed); +} + uint32_t ethSendFailStreak() MM_NONBLOCKING { return ethSendFails_.load(std::memory_order_relaxed); } @@ -958,6 +970,17 @@ uint32_t ethSendFailStreak() MM_NONBLOCKING { // simply ignored here, which is what the driver's own comment tells the user. bool ethBindRawInterface(const char*) { return true; } +bool ethRestartTx() { + if (!ethHandle_) return false; + // Clear our own flag first: the restart re-runs negotiation and the CONNECTED event sets it + // again if the link really comes back. Leaving it true would keep ethSendRaw trying against a + // driver that is mid-restart. + ethLinkUp_.store(false, std::memory_order_relaxed); + ethSendFails_.store(0, std::memory_order_relaxed); + esp_eth_stop(ethHandle_); + return esp_eth_start(ethHandle_) == ESP_OK; +} + // Negotiated link speed, asked of the driver rather than assumed from the PHY type: a gigabit PHY // on a 100 Mbit switch (or a bad cable) negotiates down, and that is precisely the case worth // reporting. 0 when there is no link to describe. @@ -985,8 +1008,10 @@ void ethGetIPv4(uint8_t out[4]) MM_NONBLOCKING { out[0] = out[1] = out[2 bool ethSendRaw(const uint8_t*, size_t) MM_NONBLOCKING { return false; } // no MAC to hand a frame to void ethClaimRawL2(bool) {} // no link to claim bool ethRawL2Claimed() MM_NONBLOCKING { return false; } +bool ethRestartTx() { return false; } // no driver to restart uint16_t ethLinkSpeedMbps() MM_NONBLOCKING { return 0; } // no link to describe uint32_t ethSendFailStreak() MM_NONBLOCKING { return 0; } // nothing sends, nothing fails +void ethSendFailCounts(uint32_t& a, uint32_t& b) MM_NONBLOCKING { a = b = 0; } bool ethBindRawInterface(const char*) { return true; } // no MAC, nothing to bind #endif // MM_NO_ETH diff --git a/src/platform/platform.h b/src/platform/platform.h index acf365ed..ebbfef8c 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -381,12 +381,31 @@ void ethClaimRawL2(bool claim); // apart from "Ethernet carries no IP because something is driving it directly". bool ethRawL2Claimed() MM_NONBLOCKING; +// Failures split by cause since boot, because the two are different faults with different fixes and +// a single counter cannot tell them apart: +// linkDown โ€” esp_eth_transmit refused before touching the MAC because the driver's link reads +// down. A flapping PHY shows up here, and each flap costs seconds of refusals. +// ringFull โ€” the MAC had no free TX descriptor. That is back-pressure: our sender outrunning the +// wire, or the DMA draining slower than it should. +void ethSendFailCounts(uint32_t& linkDown, uint32_t& ringFull) MM_NONBLOCKING; + // Consecutive ethSendRaw() failures since the last success. A raw sender polls this to tell a // dropped frame (normal back-pressure, count returns to 0) from a wedged transmit path: the IDF // driver refuses every frame once ITS link flag reads down, which can outlive our own event-driven // flag and would otherwise look like a healthy link sending nothing. uint32_t ethSendFailStreak() MM_NONBLOCKING; +// Restart the Ethernet driver after transmit has provably wedged: esp_eth_transmit refuses every +// frame once the driver's internal link state reads down, and that state can diverge from both the +// PHY and our own event-driven flag โ€” observed on an S31 under sustained TX, with the link genuinely +// lost, no DISCONNECTED event delivered, and nothing recovering short of a reboot. +// +// stop/start re-runs the driver's link negotiation, which is the only supported way back (no ioctl +// writes the link flag). Heavier than a register poke, so a caller must gate it on a long failure +// streak rather than on ordinary back-pressure. Returns true when the restart succeeded; the link +// may still be down afterwards if the cable really is out, which is the honest outcome. +bool ethRestartTx(); + // Negotiated link speed in Mbit/s (10 / 100 / 1000); 0 when no link or no driver. Reported rather // than enforced: panel cards want a gigabit link, and a driver that knows the actual speed can say // "100 Mbit, expect tearing" instead of either failing silently or refusing to run. @@ -420,6 +439,9 @@ void ethTestClearFrames(); void setTestEthSendFails(bool fail); // Override the reported link speed so a test can exercise the too-slow-link status. void setTestEthLinkSpeed(uint16_t mbps); +// How many times ethRestartTx() has run, so a test can pin the once-per-wedge bound (the driver's +// own restartTried_ is private, and the count is what the bound is actually about). +uint32_t ethRestartCountForTest(); #endif bool wifiStaInit(const char* ssid, const char* password); diff --git a/test/unit/light/unit_PanelCardDriver.cpp b/test/unit/light/unit_PanelCardDriver.cpp index 96c8781f..bf644ea9 100644 --- a/test/unit/light/unit_PanelCardDriver.cpp +++ b/test/unit/light/unit_PanelCardDriver.cpp @@ -393,3 +393,86 @@ TEST_CASE("PanelCardDriver sends nothing when the buffer covers no row") { CHECK(mm::platform::ethTestFrameCount() == 0); } + +// A transmit path that refuses everything gets ONE recovery attempt, not one per second: a restart +// cannot fix an unplugged cable, and retrying would tear the interface down repeatedly under a user +// who is watching the card to find out what is wrong. +TEST_CASE("PanelCardDriver attempts recovery once per wedge") { + clearClaims(); + mm::Buffer source; + mm::PanelCardDriver driver; + Wall wall(64, 1); + setUp(driver, source, wall, 64); + + // Drive the failure streak past the wedge threshold. + mm::platform::setTestEthSendFails(true); + for (int i = 0; i < 200; i++) { + mm::platform::setTestNowMs(1000 + i * 30); + driver.tick(); + } + REQUIRE(mm::platform::ethSendFailStreak() >= 500); + + const uint32_t before = mm::platform::ethRestartCountForTest(); + driver.tick1s(); + CHECK(mm::platform::ethRestartCountForTest() == before + 1); // fired + driver.tick1s(); + driver.tick1s(); + CHECK(mm::platform::ethRestartCountForTest() == before + 1); // and only once + + mm::platform::setTestEthSendFails(false); +} + +// A wedge that survives the restart is reported rather than retried: the second and third ticks +// must NOT fire again, or a wedge the restart cannot fix (an unplugged cable) would bounce the +// interface every tick under a user who is reading the card to find out what is wrong. +// +// Re-arming for a LATER wedge needs a live link, which the host stub does not have โ€” that half is +// bench-verified rather than pinned here, and the comment in writeLinkStatus says so. +TEST_CASE("PanelCardDriver reports a surviving wedge instead of restarting again") { + clearClaims(); + mm::Buffer source; + mm::PanelCardDriver driver; + Wall wall(64, 1); + setUp(driver, source, wall, 64); + + mm::platform::setTestEthSendFails(true); + for (int i = 0; i < 200; i++) { + mm::platform::setTestNowMs(5000 + i * 30); + driver.tick(); + } + driver.tick1s(); // fires + const uint32_t after = mm::platform::ethRestartCountForTest(); + + for (int i = 0; i < 5; i++) driver.tick1s(); // still wedged + CHECK(mm::platform::ethRestartCountForTest() == after); // and still only one restart + + mm::platform::setTestEthSendFails(false); +} + +// Failures are counted by CAUSE, because a down link and a full TX ring are different faults with +// different fixes โ€” one total cannot tell them apart, which is what made a real bug unreadable. +TEST_CASE("PanelCardDriver counts send failures by cause") { + clearClaims(); + mm::platform::ethTestClearFrames(); + mm::Buffer source; + mm::PanelCardDriver driver; + Wall wall(64, 1); + setUp(driver, source, wall, 64); + + uint32_t linkDown = 0, ringFull = 0; + mm::platform::ethSendFailCounts(linkDown, ringFull); + CHECK(ringFull == 0); + + mm::platform::setTestEthSendFails(true); + mm::platform::setTestNowMs(30000); + driver.tick(); + mm::platform::setTestEthSendFails(false); + + mm::platform::ethSendFailCounts(linkDown, ringFull); + CHECK(ringFull > 0); // cumulative, not the streak โ€” it survives a later success + mm::platform::setTestNowMs(30100); + driver.tick(); + uint32_t after = 0; + mm::platform::ethSendFailCounts(linkDown, after); + CHECK(after == ringFull); // a success does not erase the history +} From cca3fe8e227780ccff7b25815220ab58eafbcc6c Mon Sep 17 00:00:00 2001 From: ewowi Date: Fri, 31 Jul 2026 11:44:04 +0200 Subject: [PATCH 2/3] Count every dropped frame; fix a 1-in-44 flaky test; credit WLED and FPP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on the panel-card work, plus a test that had been failing in CI at random since it was written. The panel driver counted only its row frames as dropped, so its status compared two different sets of frames and the comparison meant nothing. Performance: unchanged. **Light domain** - PanelCardDriver counts brightness and sync failures too. Only row failures were counted, so four frames per tick failed invisibly and `dropped` described a different set of frames than the platform's per-cause totals it sits beside. **Core** - ethRestartTx checks esp_eth_stop before starting: a failed stop leaves the driver in a state we did not establish, and starting on top of that compounds it. The caller already turns a false return into "restart the device". - Documented why the stop/start is safe from the 1 Hz tick: register writes plus an esp_timer stop/start, microseconds rather than the seconds negotiation takes, and only ever in a wedged state where no frames are going out. **Scripts/MoonDeck** - repo_health validates the snapshot shape at load. merge_carry_forward does `old.get(section, {})`, so a JSON list or scalar would raise and a section holding a list would corrupt the merge. A rejection is announced rather than read as "no previous numbers", which would report every metric as new and look like a clean slate instead of a broken baseline. **Tests** - unit_DistortionWavesEffect freezes the clock. The effect's hue is the average of two time-advanced sines and the test ticks once, so the phase was whatever the process uptime happened to be โ€” and at 2.28% of start times the two sampled lights land on the same hue. Measured, and it matches the 2.3% predicted from the sine pair. It had a ~1-in-44 chance of failing on any run, on any branch, and the realtime sanitizer runs slowly enough to hit it. Same ClockGuard pattern BouncingBallsEffect already uses. The other 15 time-driven effect tests were checked: they assert time-invariant properties, so only this one was exposed. - 9 Python regression tests for the snapshot validation, including a control that a valid snapshot passes through untouched. **Docs/CI** - README credits: WLED and WLED-MM first (projectMM is born out of WLED, and every part here is a MoonModule), and FPP as the inspiration for PanelCardDriver. - README em-dashes removed (26), rewritten rather than substituted. **Reviews** - ๐Ÿ‡ brightness/sync failures were not counted -> fixed. - ๐Ÿ‡ esp_eth_stop's return was ignored -> checked. - ๐Ÿ‡ repo_health accepted any JSON shape -> validated, announced, and pinned by tests. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 38 ++++---- docs/metrics/repo-health.json | 22 ++--- docs/metrics/repo-health.md | 28 +++--- moondeck/check/repo_health.py | 33 ++++++- src/light/drivers/PanelCardDriver.h | 15 ++- src/platform/esp32/platform_esp32.cpp | 4 +- src/platform/platform.h | 6 ++ test/python/test_repo_health_baseline.py | 92 +++++++++++++++++++ .../unit/light/unit_DistortionWavesEffect.cpp | 13 +++ 9 files changed, 199 insertions(+), 52 deletions(-) create mode 100644 test/python/test_repo_health_baseline.py diff --git a/README.md b/README.md index a3b565ee..e75dee30 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Drive large LED installations and DMX lighting from ESP32, Teensy, Raspberry Pi, ![Web UI](docs/assets/ui/ui_theme.gif) -๐Ÿ‘‰ **Try it now:** flash an ESP32 straight from your browser โ†’ โ€” step-by-step in the [Getting started guide](docs/gettingstarted.md). +๐Ÿ‘‰ **Try it now:** flash an ESP32 straight from your browser โ†’ . Step-by-step in the [Getting started guide](docs/gettingstarted.md). ๐Ÿ“ฆ **Release + downloads:** [latest release](https://github.com/MoonModules/projectMM/releases/latest) @@ -22,21 +22,21 @@ If you like projectMM, give it a โญ๏ธ, fork it, or open an issue or pull reque ๐ŸŽ›๏ธ **Pluggable pipeline**: Layouts โ†’ Layers (effects + modifiers) โ†’ Drivers. Build it visually in the browser, and every change applies live (settings also persist to flash across power cycles). -๐Ÿ”„ **No reboot to apply a configuration change**: edit a pin map, a strand length, an output protocol, or the mic on a running device and it takes effect on the very next frame โ€” no init-at-boot step, no restart. Where most LED-controller firmware needs a reboot for a pin or protocol change, projectMM applies it live. (Flashing new *firmware* over OTA still needs the usual power cycle โ€” that's a binary swap, not a config change.) +๐Ÿ”„ **No reboot to apply a configuration change**: edit a pin map, a strand length, an output protocol, or the mic on a running device and it takes effect on the very next frame, with no init-at-boot step, no restart. Where most LED-controller firmware needs a reboot for a pin or protocol change, projectMM applies it live. (Flashing new *firmware* over OTA still needs the usual power cycle, since that's a binary swap rather than a config change.) ๐Ÿ’ก **DMX *and* addressable LEDs in one setup**: RGB strips, RGBW pixels, par lights, moving heads, all through the same pipeline. -๐Ÿ”Œ **Parallel WS2812 output**: drive many strands at once over three ESP32 peripherals โ€” RMT (every chip), the S3's LCD_CAM i80 bus (8 lanes), and the P4's Parlio engine (up to 8 lanes) โ€” each with an on-device loopback self-test that bit-verifies the wire signal. +๐Ÿ”Œ **Parallel WS2812 output**: drive many strands at once over three ESP32 peripherals: RMT (every chip), the S3's LCD_CAM i80 bus (8 lanes), and the P4's Parlio engine (up to 8 lanes), each with an on-device loopback self-test that bit-verifies the wire signal. -๐ŸŒ **Industry protocols, both directions**: send *and* receive [Art-Net](https://art-net.org.uk/), [E1.31/sACN](https://tsp.esta.org/tsp/documents/docs/ANSI_E1-31-2018.pdf), and [DDP](http://www.3waylabs.com/ddp/) over the network โ€” interoperable with Falcon, Advatek, xLights, Resolume, LedFx and other industry gear. +๐ŸŒ **Industry protocols, both directions**: send *and* receive [Art-Net](https://art-net.org.uk/), [E1.31/sACN](https://tsp.esta.org/tsp/documents/docs/ANSI_E1-31-2018.pdf), and [DDP](http://www.3waylabs.com/ddp/) over the network, interoperable with Falcon, Advatek, xLights, Resolume, LedFx and other industry gear. -๐ŸŽต **Audio-reactive**: an IยฒS microphone drives a 16-band FFT spectrum + sound level, consumed by audio-reactive effects โ€” all built fresh from the mic datasheet and textbook DSP. +๐ŸŽต **Audio-reactive**: an IยฒS microphone drives a 16-band FFT spectrum + sound level, consumed by audio-reactive effects, all built fresh from the mic datasheet and textbook DSP. -๐Ÿ  **Home-automation control**: a device joins Homebridge (and any MQTT hub) over a dependency-free MQTT 3.1.1 client โ€” on/off, brightness, and a HomeKit color wheel that picks the nearest palette. See [the MQTT module docs](docs/moonmodules/core/services.md#mqtt). +๐Ÿ  **Home-automation control**: a device joins Homebridge (and any MQTT hub) over a dependency-free MQTT 3.1.1 client: on/off, brightness, and a HomeKit color wheel that picks the nearest palette. See [the MQTT module docs](docs/moonmodules/core/services.md#mqtt). -๐Ÿ“ **On-device File Manager**: browse and edit the device filesystem from the browser โ€” a lazy folder tree with an inline editor, drag-drop upload, and create/delete, plus [firmware upload OTA](docs/moonmodules/core/services.md#firmware-update) (flash a `.bin` over the LAN, no USB). See [the File Manager docs](docs/moonmodules/core/services.md#file-manager). +๐Ÿ“ **On-device File Manager**: browse and edit the device filesystem from the browser: a lazy folder tree with an inline editor, drag-drop upload, and create/delete, plus [firmware upload OTA](docs/moonmodules/core/services.md#firmware-update) (flash a `.bin` over the LAN, no USB). See [the File Manager docs](docs/moonmodules/core/services.md#file-manager). -๐Ÿ›ก๏ธ **Robust to any input**: add, delete, replace, or reconfigure any module in any order, at any grid size, and the device keeps running โ€” degraded or idle, never crashed. Every crash that's ever found becomes a regression test, so it stays fixed. +๐Ÿ›ก๏ธ **Robust to any input**: add, delete, replace, or reconfigure any module in any order, at any grid size, and the device keeps running, degraded or idle, but never crashed. Every crash that's ever found becomes a regression test, so it stays fixed. ๐Ÿ–ฅ๏ธ **One source tree, many targets**: the same code runs on ESP32, Teensy, Raspberry Pi, and macOS / Windows / Linux. @@ -50,7 +50,7 @@ If you like projectMM, give it a โญ๏ธ, fork it, or open an issue or pull reque ๐Ÿ“ฆ **No third-party libraries**: no FastLED, no ESPAsyncWebServer, no ArduinoJson. The color math, the HTTP/WebSocket server, and the control storage are all in-tree. A library, when genuinely needed, lives behind the platform boundary in `src/platform/`, never in core. The full rationale + replacements: [building.md ยง Third-party libraries](docs/building.md#third-party-libraries). -๐Ÿ”ฌ **Industry standards, our own code**: we study the prior art hard โ€” friend repos, peripheral datasheets, the Art-Net / E1.31 / WS2812 standards โ€” carry its *ideas* forward, and credit it by name; but we write our own code rather than copying theirs or tracing their structure. Each feature is spec'd from the primary source, its behaviour pinned with unit + scenario tests, then written fresh against our own architecture, so the result is independent by construction, not a renamed fork. Textbook algorithm, textbook name, our implementation. The method: [CLAUDE.md ยง Principles](CLAUDE.md#principles). +๐Ÿ”ฌ **Industry standards, our own code**: we study the prior art hard (friend repos, peripheral datasheets, the Art-Net / E1.31 / WS2812 standards), carry its *ideas* forward, and credit it by name; but we write our own code rather than copying theirs or tracing their structure. Each feature is spec'd from the primary source, its behaviour pinned with unit + scenario tests, then written fresh against our own architecture, so the result is independent by construction, not a renamed fork. Textbook algorithm, textbook name, our implementation. The method: [CLAUDE.md ยง Principles](CLAUDE.md#principles). ๐Ÿงฑ **One module model**: every effect, modifier, layout, and driver is a `MoonModule`: one base class, a uniform lifecycle, declared controls. That uniformity is why the UI renders any module with zero per-module code, and why a new capability is a new file, not a new framework. See [architecture.md ยง MoonModules](docs/architecture.md#moonmodules). @@ -162,15 +162,17 @@ We built, maintained, and contributed to these projects, so projectMM is grounde ## Credits -Specific people whose work directly shaped parts of projectMM. We study their thinking with respect and write our own code against our architecture rather than tracing theirs โ€” these credits name the prior art behind a feature: - -- **Frank ([softhack007](https://github.com/softhack007))** โ€” main author of the WLED-MM audio-reactive usermod, the most-used open-source audio-reactive LED implementation. The ideas behind [AudioService](docs/moonmodules/core/moxygen/AudioService.md) (including the adaptive noise-gate concept, analysed with his permission) descend from years of collaboration on WLED-SR / WLED-MM. -- **[troyhacks](https://github.com/troyhacks/WLED)** โ€” reworked the WLED-MM audio-reactive DSP to run on Espressif's [esp-dsp](https://github.com/espressif/esp-dsp) FFT (a low-latency, "stupid fast" alternative to ArduinoFFT); the same esp-dsp FFT choice [AudioService](docs/moonmodules/core/moxygen/AudioService.md) makes. See its Prior art notes. -- **[hpwit](https://github.com/hpwit) (Yves Bazin)** โ€” the clockless I2S / RMT / Parlio LED-driver techniques and the [ESPLiveScript](https://github.com/hpwit/ESPLiveScript) live-script engine behind the LED drivers and MoonLive. -- **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))** โ€” author of the native [WLED-Android](https://github.com/Moustachauve/WLED-Android) app. Its source let us reverse-engineer exactly what the WLED app reads, so projectMM devices appear in (and are controllable from) the native WLED apps. -- **The [Improv Wi-Fi](https://github.com/improv-wifi) project** โ€” the open Improv serial provisioning standard ([sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js)) that the projectMM web installer uses to provision a freshly-flashed device over USB. -- **[FastLED](https://github.com/FastLED/FastLED)** โ€” the canonical LED-effects library whose conventions the LED-effect world shares. projectMM links no part of FastLED, but it carries forward FastLED's recognisable *names and models* for the color/animation primitives โ€” `scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), the `beatsin8` / `inoise8` / `qadd8` family โ€” so a contributor recognises them on sight. The implementations are projectMM's own, integer-only and hot-path-tuned for our render loop; FastLED is the prior art behind the convention, credited here and in each primitive's notes. -- **wladi ([myhome-control](https://shop.myhome-control.de))** โ€” designer of the [MHC-WLED ESP32-P4 shield](https://shop.myhome-control.de/en/ABC-WLED-ESP32-P4-shield/HW10027), and the source of the hardware and the pinout details that got its **line-in audio** working in [AudioService](docs/moonmodules/core/moxygen/AudioService.md): the onboard PCM1808 I2S ADC (WS 26 / SD 33 / SCK 32 / MCLK 36), the PCM1808's stereo wiring, and its `FMT` format-select jumper (open = I2S/Philips, our default; tie to 3V3 for left-justified) โ€” which is what confirmed the standard-I2S path the ADC needs. +Specific people whose work directly shaped parts of projectMM. We study their thinking with respect and write our own code against our architecture rather than tracing theirs. These credits name the prior art behind a feature: + +- **[WLED](https://github.com/wled/WLED) and [WLED-MM](https://github.com/MoonModules/WLED)**: projectMM is born out of WLED, and takes the usermod idea to a new level. Here *everything* is a mod (a MoonModule): effects, drivers, networking, the file system, the system manager. It also integrates tightly with WLED: a projectMM device can act as a WLED device, and it talks to WLED devices (audio sync, discovery, and more). +- **Frank ([softhack007](https://github.com/softhack007))**: main author of the WLED-MM audio-reactive usermod, the most-used open-source audio-reactive LED implementation. The ideas behind [AudioService](docs/moonmodules/core/moxygen/AudioService.md) (including the adaptive noise-gate concept, analysed with his permission) descend from years of collaboration on WLED-SR / WLED-MM. +- **[troyhacks](https://github.com/troyhacks/WLED)**: reworked the WLED-MM audio-reactive DSP to run on Espressif's [esp-dsp](https://github.com/espressif/esp-dsp) FFT (a low-latency, "stupid fast" alternative to ArduinoFFT); the same esp-dsp FFT choice [AudioService](docs/moonmodules/core/moxygen/AudioService.md) makes. See its Prior art notes. +- **[hpwit](https://github.com/hpwit) (Yves Bazin)**: the clockless I2S / RMT / Parlio LED-driver techniques and the [ESPLiveScript](https://github.com/hpwit/ESPLiveScript) live-script engine behind the LED drivers and MoonLive. +- **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**: author of the native [WLED-Android](https://github.com/Moustachauve/WLED-Android) app. Its source let us reverse-engineer exactly what the WLED app reads, so projectMM devices appear in (and are controllable from) the native WLED apps. +- **The [Improv Wi-Fi](https://github.com/improv-wifi) project**: the open Improv serial provisioning standard ([sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js)) that the projectMM web installer uses to provision a freshly-flashed device over USB. +- **[FastLED](https://github.com/FastLED/FastLED)**: the canonical LED-effects library whose conventions the LED-effect world shares. projectMM links no part of FastLED, but it carries forward FastLED's recognisable *names and models* for the color/animation primitives (`scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), the `beatsin8` / `inoise8` / `qadd8` family), so a contributor recognises them on sight. The implementations are projectMM's own, integer-only and hot-path-tuned for our render loop; FastLED is the prior art behind the convention, credited here and in each primitive's notes. +- **[FPP](https://github.com/FalconChristmas/fpp) (Falcon Player)**: the show player that drives LED panel receiver cards from a Raspberry Pi. Seeing an FPP rig feed a wall of HUB75 panels is what prompted [PanelCardDriver](docs/moonmodules/light/drivers.md#panelcard): if a Linux host can send those frames, so can a board that is already rendering them, which removes the host from the installation entirely. FPP is the inspiration, and the reference point for what good looks like here: it sustains 50 fps. +- **wladi ([myhome-control](https://shop.myhome-control.de))**: designer of the [MHC-WLED ESP32-P4 shield](https://shop.myhome-control.de/en/ABC-WLED-ESP32-P4-shield/HW10027), and the source of the hardware and the pinout details that got its **line-in audio** working in [AudioService](docs/moonmodules/core/moxygen/AudioService.md): the onboard PCM1808 I2S ADC (WS 26 / SD 33 / SCK 32 / MCLK 36), the PCM1808's stereo wiring, and its `FMT` format-select jumper (open = I2S/Philips, our default; tie to 3V3 for left-justified), which is what confirmed the standard-I2S path the ADC needs. ## Contributing diff --git a/docs/metrics/repo-health.json b/docs/metrics/repo-health.json index 08e7370f..2fac1632 100644 --- a/docs/metrics/repo-health.json +++ b/docs/metrics/repo-health.json @@ -1,18 +1,18 @@ { - "commit": "49795a4f", + "commit": "bf386866", "flash": { "esp32": 1678960, "esp32p4-eth": 1503232, "esp32p4-eth-wifi": 1793760, "esp32s3-n16r8": 1667216, "esp32s3-n8r8": 1666992, - "esp32s31": 1925008, + "esp32s31": 1924992, "desktop": 945656 }, "perf": { "desktop": { - "tick_us": 128, - "fps": 7812 + "tick_us": 127, + "fps": 7874 }, "esp32": { "tick_us": 4164, @@ -21,11 +21,11 @@ }, "loc": { "core": 14889, - "light": 20191, - "platform": 12493, + "light": 20198, + "platform": 12501, "ui": 5811, - "test": 35324, - "moondeck": 19943 + "test": 35429, + "moondeck": 19968 }, "comments": { "core": { @@ -33,11 +33,11 @@ "ratio": 0.409 }, "light": { - "lines": 7804, + "lines": 7807, "ratio": 0.427 }, "platform": { - "lines": 4177, + "lines": 4185, "ratio": 0.37 }, "ui": { @@ -45,7 +45,7 @@ "ratio": 0.278 }, "test": { - "lines": 6053, + "lines": 6063, "ratio": 0.198 }, "moondeck": { diff --git a/docs/metrics/repo-health.md b/docs/metrics/repo-health.md index 1a690e37..438106a9 100644 --- a/docs/metrics/repo-health.md +++ b/docs/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `49795a4f`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run โ€” **do not edit by hand**. +Measured at `bf386866`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run โ€” **do not edit by hand**. Current state only; the trend is this file's git history (`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. @@ -8,19 +8,19 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Flash | |---|---:| -| desktop | 923 KB (+0 KB) โš  | +| desktop | 923 KB | | esp32 | 1,640 KB | -| esp32p4-eth | 1,468 KB (+1 KB) โš  | +| esp32p4-eth | 1,468 KB | | esp32p4-eth-wifi | 1,752 KB | | esp32s3-n16r8 | 1,628 KB | | esp32s3-n8r8 | 1,628 KB | -| esp32s31 | 1,880 KB (+1 KB) โš  | +| esp32s31 | 1,880 KB (โˆ’0 KB) โœ“ | ## Render performance | Target | Tick | FPS | |---|---:|---:| -| desktop | 128 ยตs (+3 ยตs) โš  | 7,812 (โˆ’188) โš  | +| desktop | 127 ยตs (โˆ’1 ยตs) โœ“ | 7,874 (+62) โœ“ | | esp32 | 4,164 ยตs | 240 | ## Code @@ -28,25 +28,25 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Area | Lines | Comments | Comment share | |---|---:|---:|---:| | core | 14,889 | 5,583 | 40.9 % | -| light | 20,191 (+41) โš  | 7,804 | 42.7 % (+0.1 %) โš  | -| platform | 12,493 (+65) โš  | 4,177 | 37.0 % (+0.1 %) โš  | +| light | 20,198 (+7) โš  | 7,807 | 42.7 % | +| platform | 12,501 (+8) โš  | 4,185 | 37.0 % | | ui | 5,811 | 1,518 | 27.8 % | -| test | 35,324 (+117) โš  | 6,053 | 19.8 % | -| moondeck | 19,943 (+29) โš  | 3,187 | 18.3 % | +| test | 35,429 (+105) โš  | 6,063 | 19.8 % | +| moondeck | 19,968 (+25) โš  | 3,187 | 18.3 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 1,007 (+5) โœ“ | +| unit cases | 1,007 | | scenarios | 22 | ## Complexity | Metric | Value | |---|---:| -| functions | 2,187 (+8) โœ“ | -| over threshold | 139 (+1) โš  | +| functions | 2,187 | +| over threshold | 139 | | worst CCN | 93 | ## Documentation @@ -54,9 +54,9 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Metric | Value | |---|---:| | markdown files | 170 | -| markdown lines | 22,809 (+47) โš  | +| markdown lines | 22,809 | | plan files | 90 | | backlog lines | 3,114 | -| lessons lines | 418 (+17) โš  | +| lessons lines | 418 | | CLAUDE.md lines | 135 | diff --git a/moondeck/check/repo_health.py b/moondeck/check/repo_health.py index 4f41d044..68b7209f 100644 --- a/moondeck/check/repo_health.py +++ b/moondeck/check/repo_health.py @@ -202,6 +202,26 @@ def snapshot(perf=None): } +def _valid_snapshot(data, source): + """A snapshot mapping, or {} โ€” and say so out loud when it is neither. + + merge_carry_forward does `old.get(section, {})`, so a JSON list or scalar would raise, and a + section holding a list would corrupt the merge. A malformed baseline must not read as "no + previous numbers" either: that silently reports every metric as new, which looks like a clean + slate rather than a broken file. + """ + if not isinstance(data, dict): + print(f"repo-health: ignoring {source} โ€” expected an object, got {type(data).__name__}", + file=sys.stderr) + return {} + for key in ("flash", "perf", "complexity"): + if key in data and not isinstance(data[key], dict): + print(f"repo-health: ignoring {source} โ€” section '{key}' is " + f"{type(data[key]).__name__}, expected an object", file=sys.stderr) + return {} + return data + + def load_previous(): """The COMMITTED snapshot, read from git rather than from the working tree. @@ -217,14 +237,19 @@ def load_previous(): out = subprocess.run(["git", "show", f"HEAD:{HEALTH_FILE.relative_to(ROOT).as_posix()}"], cwd=ROOT, capture_output=True, text=True, timeout=10) if out.returncode == 0: - return json.loads(out.stdout) - except (subprocess.SubprocessError, OSError, json.JSONDecodeError): + return _valid_snapshot(json.loads(out.stdout), "the committed snapshot") + except json.JSONDecodeError: + print("repo-health: the committed snapshot is not valid JSON โ€” " + "falling back to the working tree", file=sys.stderr) + except (subprocess.SubprocessError, OSError): pass if not HEALTH_FILE.exists(): return {} try: - return json.loads(_read(HEALTH_FILE)) + return _valid_snapshot(json.loads(_read(HEALTH_FILE)), str(HEALTH_FILE.name)) except json.JSONDecodeError: + print(f"repo-health: {HEALTH_FILE.name} is not valid JSON โ€” treating as empty", + file=sys.stderr) return {} @@ -239,7 +264,7 @@ def load_working_tree(): if not HEALTH_FILE.exists(): return {} try: - return json.loads(_read(HEALTH_FILE)) + return _valid_snapshot(json.loads(_read(HEALTH_FILE)), str(HEALTH_FILE.name)) except json.JSONDecodeError: return {} diff --git a/src/light/drivers/PanelCardDriver.h b/src/light/drivers/PanelCardDriver.h index dbe6ade0..ce48deaa 100644 --- a/src/light/drivers/PanelCardDriver.h +++ b/src/light/drivers/PanelCardDriver.h @@ -214,8 +214,13 @@ class PanelCardDriver : public DriverBase { // Sent TWICE, like the sync below. Card firmware v13+ acts on the second copy only; // older firmware ignores the duplicate, so sending both costs one frame and works on // every version rather than making the behaviour depend on a firmware probe we do not do. - if (platform::ethSendRaw(packet_, len)) framesSent_++; - if (platform::ethSendRaw(packet_, len)) framesSent_++; + // + // Counted like the rows: `dropped` and the platform's per-cause totals must describe the + // SAME set of frames, or comparing them tells you nothing. + for (int i = 0; i < 2; i++) { + if (platform::ethSendRaw(packet_, len)) framesSent_++; + else framesDroppedTotal_++; + } } // One card row per wall row, in order. The card's own row numbering runs across its outputs @@ -250,8 +255,10 @@ class PanelCardDriver : public DriverBase { if (anyRowSent) { const size_t len = buildColorLightSyncPacket(packet_, kCardGain); // Twice, for the same firmware reason as the brightness frame above. - if (platform::ethSendRaw(packet_, len)) framesSent_++; - if (platform::ethSendRaw(packet_, len)) framesSent_++; + for (int i = 0; i < 2; i++) { + if (platform::ethSendRaw(packet_, len)) framesSent_++; + else framesDroppedTotal_++; + } } } diff --git a/src/platform/esp32/platform_esp32.cpp b/src/platform/esp32/platform_esp32.cpp index 1721ddea..aac03773 100644 --- a/src/platform/esp32/platform_esp32.cpp +++ b/src/platform/esp32/platform_esp32.cpp @@ -977,7 +977,9 @@ bool ethRestartTx() { // driver that is mid-restart. ethLinkUp_.store(false, std::memory_order_relaxed); ethSendFails_.store(0, std::memory_order_relaxed); - esp_eth_stop(ethHandle_); + // A failed stop leaves the driver in a state we did not establish; starting on top of that + // would compound it. Report instead โ€” the caller turns this into a "restart the device" status. + if (esp_eth_stop(ethHandle_) != ESP_OK) return false; return esp_eth_start(ethHandle_) == ESP_OK; } diff --git a/src/platform/platform.h b/src/platform/platform.h index ebbfef8c..6b8a6a7a 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -404,6 +404,12 @@ uint32_t ethSendFailStreak() MM_NONBLOCKING; // writes the link flag). Heavier than a register poke, so a caller must gate it on a long failure // streak rather than on ordinary back-pressure. Returns true when the restart succeeded; the link // may still be down afterwards if the cable really is out, which is the honest outcome. +// +// NOT MM_NONBLOCKING, and called only from the 1 Hz status tick โ€” never from the render path. Both +// halves are register writes plus an esp_timer stop/start (no semaphore wait, no link poll), so the +// cost is microseconds rather than the seconds negotiation itself takes; the link comes back later +// via the CONNECTED event, not by blocking here. It runs only in a wedged state, where by +// definition no frames are going out anyway. bool ethRestartTx(); // Negotiated link speed in Mbit/s (10 / 100 / 1000); 0 when no link or no driver. Reported rather diff --git a/test/python/test_repo_health_baseline.py b/test/python/test_repo_health_baseline.py new file mode 100644 index 00000000..f51c21f0 --- /dev/null +++ b/test/python/test_repo_health_baseline.py @@ -0,0 +1,92 @@ +"""repo-health's baseline must survive a malformed snapshot without corrupting a run. + +Two separate loaders read it: `load_previous` (the COMMITTED snapshot, what the delta compares +against) and `load_working_tree` (the newest numbers, what carry-forward preserves). Both feed +`merge_carry_forward`, which does `old.get(section, {})` โ€” so a JSON list or scalar where an object +belongs would raise, and a section holding a list would corrupt the merge. + +A malformed file must also not read as "no previous numbers": that reports every metric as new, +which looks like a clean slate rather than a broken baseline. So the rule is reject-and-announce, +never silently accept. +""" + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(ROOT / "moondeck" / "check")) + +import repo_health # noqa: E402 + + +# ---- shape validation ---- + +def test_a_snapshot_that_is_not_an_object_is_rejected(): + """A list or scalar cannot be a snapshot; accepting one would raise inside the merge.""" + assert repo_health._valid_snapshot([1, 2, 3], "t") == {} + assert repo_health._valid_snapshot("nope", "t") == {} + assert repo_health._valid_snapshot(42, "t") == {} + + +def test_a_section_of_the_wrong_shape_is_rejected(): + """`flash`/`perf`/`complexity` are merged key-by-key, so each must be an object.""" + assert repo_health._valid_snapshot({"flash": []}, "t") == {} + assert repo_health._valid_snapshot({"perf": 7}, "t") == {} + assert repo_health._valid_snapshot({"complexity": "x"}, "t") == {} + + +def test_a_valid_snapshot_passes_through_unchanged(): + """The guard must not damage the normal case โ€” this is the control for the rejections above.""" + good = {"flash": {"esp32": 1}, "perf": {}, "complexity": {}, "other": 5} + assert repo_health._valid_snapshot(good, "t") is good + + +def test_a_snapshot_with_no_sections_is_still_valid(): + """Sections are optional: a first-ever snapshot has none, and that is not malformed.""" + assert repo_health._valid_snapshot({"lines": {"src": 10}}, "t") == {"lines": {"src": 10}} + + +# ---- the merge the validation protects ---- + +def test_carry_forward_keeps_previous_values_for_unmeasured_sections(): + """The reason the shapes matter: a run that measured nothing must not drop the old numbers.""" + old = {"flash": {"esp32": 100, "esp32s3": 200}, "perf": {"tick": 5}, "complexity": {}} + new = {"flash": {"esp32": 150}} + merged = repo_health.merge_carry_forward(new, old) + assert merged["flash"]["esp32"] == 150 # this run measured it + assert merged["flash"]["esp32s3"] == 200 # this run did not โ€” the old value survives + assert merged["perf"]["tick"] == 5 + + +def test_carry_forward_survives_a_rejected_baseline(): + """A malformed baseline degrades to "nothing to carry", not to a crash.""" + merged = repo_health.merge_carry_forward({"flash": {"esp32": 1}}, + repo_health._valid_snapshot([1, 2], "t")) + assert merged["flash"] == {"esp32": 1} + + +# ---- the loaders ---- + +def test_malformed_json_on_disk_reads_as_empty(tmp_path, monkeypatch): + """Unparseable JSON is announced and treated as empty rather than raising mid-run.""" + bad = tmp_path / "repo-health.json" + bad.write_text("{ not json", encoding="utf-8") + monkeypatch.setattr(repo_health, "HEALTH_FILE", bad) + assert repo_health.load_working_tree() == {} + + +def test_a_wrong_shape_on_disk_reads_as_empty(tmp_path, monkeypatch): + """Valid JSON of the wrong shape is rejected by the same rule as unparseable JSON.""" + bad = tmp_path / "repo-health.json" + bad.write_text(json.dumps([1, 2, 3]), encoding="utf-8") + monkeypatch.setattr(repo_health, "HEALTH_FILE", bad) + assert repo_health.load_working_tree() == {} + + +def test_a_good_file_on_disk_loads(tmp_path, monkeypatch): + """The control: the loader must actually load a well-formed snapshot.""" + good = tmp_path / "repo-health.json" + good.write_text(json.dumps({"flash": {"esp32": 1}}), encoding="utf-8") + monkeypatch.setattr(repo_health, "HEALTH_FILE", good) + assert repo_health.load_working_tree() == {"flash": {"esp32": 1}} diff --git a/test/unit/light/unit_DistortionWavesEffect.cpp b/test/unit/light/unit_DistortionWavesEffect.cpp index 5ea0430e..4b664b4f 100644 --- a/test/unit/light/unit_DistortionWavesEffect.cpp +++ b/test/unit/light/unit_DistortionWavesEffect.cpp @@ -4,6 +4,11 @@ #include "light/layouts/Layouts.h" #include "light/effects/DistortionWavesEffect.h" #include "light/layouts/GridLayout.h" +#include "platform/platform.h" // setTestNowMs โ€” freeze millis() for a deterministic phase + +// Restore the real clock after any test that froze it, so a frozen value can't leak into +// order-dependent neighbours. +namespace { struct ClockGuard { ~ClockGuard() { mm::platform::setTestNowMs(0); } }; } static void buildLayer(mm::Layouts& layouts, mm::GridLayout& grid, mm::Layer& layer, mm::DistortionWavesEffect& fx, @@ -28,6 +33,14 @@ TEST_CASE("DistortionWavesEffect writes non-zero RGB data") { } TEST_CASE("DistortionWavesEffect produces spatial variation") { + // The clock is FROZEN because the pattern moves with time: a light's hue is the average of a + // horizontal and a vertical sine, both advanced by the elapsed-time phase. Layer::tick() reads + // platform::millis(), so a single-tick test starts at whatever the process uptime happens to + // be โ€” and at ~2.3% of phases the two sampled lights land on the same hue and this assertion + // fails. It flaked in CI on exactly that. Freezing makes the phase (and so the property) fixed. + ClockGuard guard; + mm::platform::setTestNowMs(1); + mm::Layouts layouts; mm::GridLayout grid; mm::Layer layer; mm::DistortionWavesEffect fx; buildLayer(layouts, grid, layer, fx, 16, 16, 1); auto& buf = layer.buffer(); From 4d724c640698105e7fca044586af51c4853fed5b Mon Sep 17 00:00:00 2001 From: ewowi Date: Fri, 31 Jul 2026 12:52:58 +0200 Subject: [PATCH 3/3] Fix a 4-second render stall in the wedge recovery; name the card vendors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-merge review findings on the panel-card work. The recovery path was the weak spot: it blocked the render thread for up to 4 seconds against a comment claiming microseconds, a failed restart hid behind an unplugged-cable warning, and the tests that were supposed to pin the guard could not fail. Performance: no change to the steady-state tick. The recovery blocks up to ~4 s when it fires, which is now stated rather than mis-stated; it fires only in a wedged state where no frames are going out. **Core** - ethRestartTx: clear ethLinkUp_ only AFTER esp_eth_stop succeeds. Clearing first and then failing left transmit permanently refused with no event able to set the flag again, behind a "no ethernet link" warning. - A link-down refusal now increments the link counter. It was silent, so `dropped` and the per-cause totals beside it described different sets of frames. - Documented the real cost of ethRestartTx: esp_eth_start restarts autonegotiation, which polls the PHY with vTaskDelay(100 ms) up to autonego_timeout_ms (4000 by default, and a wedged link is down so it runs to the timeout). The previous comment claimed microseconds and no link poll, which was wrong on both counts. **Light domain** - A failed restart is latched, so the next tick cannot soften it to the link warning. It is the one state a user cannot resolve by plugging a cable back in. - PanelCardDriver's more-info section lists the other receiver-card vendors (NovaStar, ColorLight, Linsn, Mooncell, Huidu, DBstar, Xixun) with links, and invites contributions: a *Packet.h plus a kFormatOptions entry, with the window, correction, chunking and platform seam already shared. It also says what is NOT known โ€” each vendor speaks its own proprietary L2 protocol, and whether another format fits this driver's row-plus-sync model is unverified. **Scripts/MoonDeck** - check_esp32_built asks ninja's dependency database what each firmware actually compiles. PanelCardDriver.h is gated out of the S3 build, so ninja correctly does nothing, the binary's timestamp never moves, and the gate reported it stale forever with no rebuild able to clear it. Any comment-only edit to that header would have blocked every future commit. **Tests** - The once-per-wedge tests were vacuous: the desktop restart clears the streak, so the wedge branch was never re-entered and they passed with the guard deleted. They now rebuild the streak between ticks, and the guard was mutation-tested (deleted -> fails, restored -> passes). - A test for the failed-restart path, reachable via a new setTestEthRestartFails hook. - Two tests for the git baseline, including one proving the delta comes from the commit rather than the working tree. - clearClaims also resets the platform's simulated-failure switches: they are process-global, so a test leaving one set poisoned the next, which is why one case passed alone and failed in sequence. **Docs/CI** - performance.md: re-measured on the fixed firmware, and it now records the DMA finding and its ~20 KB cost. Corrected a claim that ~32 FPS was the driver's ceiling; it is the render's, and a lighter effect mix measures ~56 FPS. - README: WLED-iOS alongside WLED-Android in the Moustachauve credit. - No em-dashes in any line this branch adds. **Reviews** - ๐Ÿ‘พ ethRestartTx blocks ~4 s on the render thread, comment said microseconds -> measured in the IDF source, comment corrected, trade stated. - ๐Ÿ‘พ a failed restart masqueraded as an unplugged cable -> ordering fixed, error latched. - ๐Ÿ‘พ the once-per-wedge tests could not fail -> rewritten and mutation-tested. - ๐Ÿ‘พ dropped vs the per-cause totals counted different frames -> link-down counted. - ๐Ÿ‘พ load_previous untested -> two tests with a tmp git repo. - ๐Ÿ‘พ the restart-failed branch was untestable -> test hook added. - ๐Ÿ‘พ new prose added em-dashes -> removed from every added line. - ๐Ÿ‘พ carried-forward flash figures for targets not rebuilt: accepted, that is the run-before-commit shape of the KPI gate rather than a branch defect. Bench: flashed to the S31 and ran past both historical failure points (wedges at 9:04 and 20:23 before the fix). 22+ minutes, zero drops, zero wedges, 4 251 packets/s against a 4 224 baseline. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 +- docs/history/lessons.md | 12 +-- docs/metrics/repo-health.json | 26 +++--- docs/metrics/repo-health.md | 24 +++--- docs/performance.md | 29 ++++--- esp32/sdkconfig.defaults.esp32s31 | 4 +- moondeck/check/check_esp32_built.py | 42 ++++++++- moondeck/check/repo_health.py | 14 +-- src/light/drivers/PanelCardDriver.h | 56 ++++++++++-- src/platform/desktop/platform_desktop.cpp | 14 ++- src/platform/esp32/platform_esp32.cpp | 20 +++-- src/platform/platform.h | 23 +++-- test/python/test_repo_health_baseline.py | 49 ++++++++++- test/scenarios/light/scenario_perf_full.json | 4 +- .../light/scenario_peripheral_grid_sweep.json | 8 +- .../unit/light/unit_DistortionWavesEffect.cpp | 4 +- test/unit/light/unit_PanelCardDriver.cpp | 86 +++++++++++++------ 17 files changed, 305 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index e75dee30..3f85e489 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ Specific people whose work directly shaped parts of projectMM. We study their th - **Frank ([softhack007](https://github.com/softhack007))**: main author of the WLED-MM audio-reactive usermod, the most-used open-source audio-reactive LED implementation. The ideas behind [AudioService](docs/moonmodules/core/moxygen/AudioService.md) (including the adaptive noise-gate concept, analysed with his permission) descend from years of collaboration on WLED-SR / WLED-MM. - **[troyhacks](https://github.com/troyhacks/WLED)**: reworked the WLED-MM audio-reactive DSP to run on Espressif's [esp-dsp](https://github.com/espressif/esp-dsp) FFT (a low-latency, "stupid fast" alternative to ArduinoFFT); the same esp-dsp FFT choice [AudioService](docs/moonmodules/core/moxygen/AudioService.md) makes. See its Prior art notes. - **[hpwit](https://github.com/hpwit) (Yves Bazin)**: the clockless I2S / RMT / Parlio LED-driver techniques and the [ESPLiveScript](https://github.com/hpwit/ESPLiveScript) live-script engine behind the LED drivers and MoonLive. -- **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**: author of the native [WLED-Android](https://github.com/Moustachauve/WLED-Android) app. Its source let us reverse-engineer exactly what the WLED app reads, so projectMM devices appear in (and are controllable from) the native WLED apps. +- **Christophe Gagnier ([@Moustachauve](https://github.com/Moustachauve))**: author of the native [WLED-Android](https://github.com/Moustachauve/WLED-Android) and [WLED-iOS](https://github.com/Moustachauve/WLED-iOS) apps. Their source let us reverse-engineer exactly what those apps read, so projectMM devices appear in (and are controllable from) the native WLED apps. - **The [Improv Wi-Fi](https://github.com/improv-wifi) project**: the open Improv serial provisioning standard ([sdk-cpp](https://github.com/improv-wifi/sdk-cpp) / [sdk-js](https://github.com/improv-wifi/sdk-js)) that the projectMM web installer uses to provision a freshly-flashed device over USB. - **[FastLED](https://github.com/FastLED/FastLED)**: the canonical LED-effects library whose conventions the LED-effect world shares. projectMM links no part of FastLED, but it carries forward FastLED's recognisable *names and models* for the color/animation primitives (`scale8`, `sin8`, the gradient-palette model (`CRGBPalette16` / `colorFromPalette`), the `beatsin8` / `inoise8` / `qadd8` family), so a contributor recognises them on sight. The implementations are projectMM's own, integer-only and hot-path-tuned for our render loop; FastLED is the prior art behind the convention, credited here and in each primitive's notes. - **[FPP](https://github.com/FalconChristmas/fpp) (Falcon Player)**: the show player that drives LED panel receiver cards from a Raspberry Pi. Seeing an FPP rig feed a wall of HUB75 panels is what prompted [PanelCardDriver](docs/moonmodules/light/drivers.md#panelcard): if a Linux host can send those frames, so can a board that is already rendering them, which removes the host from the installation entirely. FPP is the inspiration, and the reference point for what good looks like here: it sustains 50 fps. diff --git a/docs/history/lessons.md b/docs/history/lessons.md index e916a1ad..4326c870 100644 --- a/docs/history/lessons.md +++ b/docs/history/lessons.md @@ -400,19 +400,19 @@ The leading untested theory is that a short frame gives the DMA no runway to pre - **A frame-time KPI that does not scale with the frame is measuring a timeout, not a wire.** `frameTime` read ~741 ยตs flat from 10 to 600 lights. Flat where it should scale is a signal in itself. - **A fix that works is not a cause that is understood.** The pool swap reliably removes the symptom, which is tempting to write up as a root cause; the arithmetic says the mechanism is still unknown. -## A DMA buffer smaller than the frame multiplies descriptor use โ€” and the symptom looked exactly like a failing cable +## A DMA buffer smaller than the frame multiplies descriptor use, and the symptom looked exactly like a failing cable -Streaming panel-card frames from an S31 at ~5 300 packets/s degraded with uptime: clean for minutes, then refused frames, then a total transmit wedge every ~11 minutes that only a reboot cleared. It read as a hardware fault the whole way โ€” and it was two config lines. +Streaming panel-card frames from an S31 at ~5 300 packets/s degraded with uptime: clean for minutes, then refused frames, then a total transmit wedge every ~11 minutes that only a reboot cleared. It read as a hardware fault the whole way, and it was two config lines. **The arithmetic that caused it.** `CONFIG_ETH_DMA_BUFFER_SIZE` defaults to 512 B. A panel-card frame is 1512 B, so every frame consumed **three** descriptors. A 10-descriptor TX ring therefore held ~3.3 frames while the driver fires 132 back-to-back. Setting the buffer to 1536 B (64-byte aligned) gives one descriptor per frame and the failures stop. **Check DMA buffer size against your actual frame size; the default is sized for IP MTU traffic, not for a burst sender.** -**The second half is a race, not a size.** `CONFIG_ETH_TRANSMIT_MUTEX` defaults to off, and `mac->transmit` advances a shared descriptor pointer with no locking. With the eth netif up, lwIP and the render task both reach it. Bench-isolated: buffer fixed but mutex off โ†’ 3 000 refused frames in 4 minutes; mutex on with only 10 descriptors โ†’ none in 11. **Ring depth was never the fix** โ€” 30 descriptors ran no cleaner than 10, and a 30-deep TX ring totals ~46 KB of internal RAM against ~15 KB at 10. +**The second half is a race, not a size.** `CONFIG_ETH_TRANSMIT_MUTEX` defaults to off, and `mac->transmit` advances a shared descriptor pointer with no locking. With the eth netif up, lwIP and the render task both reach it. Bench-isolated: buffer fixed but mutex off โ†’ 3 000 refused frames in 4 minutes; mutex on with only 10 descriptors โ†’ none in 11. **Ring depth was never the fix**: 30 descriptors ran no cleaner than 10, and a 30-deep TX ring totals ~46 KB of internal RAM against ~15 KB at 10. **Why it cost hours: two wrong turns worth naming.** - *"It is not back-pressure."* Halving the send rate left the failure rate at ~13-15%, which reads as ruling out a full ring. It does not: at 3.3 frames of depth each 132-packet burst overruns the ring at any rate. The burst was the problem, never the rate. That misreading sent the investigation toward the cable and the PHY. -- *One counter for two faults.* `esp_eth_transmit` returns `ESP_ERR_INVALID_STATE` for a down link **before** touching the MAC, and `ESP_ERR_NO_MEM` for a full ring. Collapsing both into one bool made "5 million drops" unreadable. Splitting them settled the cause in one build โ€” every failure was `ring`, none was `link`. **When a counter can be incremented by two different faults, split it before theorising.** +- *One counter for two faults.* `esp_eth_transmit` returns `ESP_ERR_INVALID_STATE` for a down link **before** touching the MAC, and `ESP_ERR_NO_MEM` for a full ring. Collapsing both into one bool made "5 million drops" unreadable. Splitting them settled the cause in one build: every failure was `ring`, none was `link`. **When a counter can be incremented by two different faults, split it before theorising.** -**What the physical evidence did and did not prove.** Both link LEDs going dark said the wire genuinely dropped โ€” true, and it correctly killed the "stale software flag" theory. But it does not identify a cause: a wedged MAC stops driving the wire, so the dark LED was a *consequence*. A longer cable was the visible difference from the working setup and looked compelling; it was untouched throughout and is exonerated. +**What the physical evidence did and did not prove.** Both link LEDs going dark said the wire genuinely dropped, which is true and correctly killed the "stale software flag" theory. But it does not identify a cause: a wedged MAC stops driving the wire, so the dark LED was a *consequence*. A longer cable was the visible difference from the working setup and looked compelling; it was untouched throughout and is exonerated. -**Recovery still earns its place.** `esp_eth_stop()` + `esp_eth_start()` re-runs negotiation and resets the descriptor rings โ€” the only way back from a wedge short of a reboot, since no ioctl writes the driver's link flag. Bench-verified twice, recovering in ~17 s. Attempted once per wedge, never repeatedly: a restart cannot fix an unplugged cable, and retrying would bounce the interface under the user. +**Recovery still earns its place.** `esp_eth_stop()` + `esp_eth_start()` re-runs negotiation and resets the descriptor rings: the only way back from a wedge short of a reboot, since no ioctl writes the driver's link flag. Bench-verified twice, recovering in ~17 s. Attempted once per wedge, never repeatedly: a restart cannot fix an unplugged cable, and retrying would bounce the interface under the user. diff --git a/docs/metrics/repo-health.json b/docs/metrics/repo-health.json index 2fac1632..2dd812ce 100644 --- a/docs/metrics/repo-health.json +++ b/docs/metrics/repo-health.json @@ -1,5 +1,5 @@ { - "commit": "bf386866", + "commit": "cca3fe8e", "flash": { "esp32": 1678960, "esp32p4-eth": 1503232, @@ -7,7 +7,7 @@ "esp32s3-n16r8": 1667216, "esp32s3-n8r8": 1666992, "esp32s31": 1924992, - "desktop": 945656 + "desktop": 945752 }, "perf": { "desktop": { @@ -21,10 +21,10 @@ }, "loc": { "core": 14889, - "light": 20198, - "platform": 12501, + "light": 20214, + "platform": 12526, "ui": 5811, - "test": 35429, + "test": 35504, "moondeck": 19968 }, "comments": { @@ -33,19 +33,19 @@ "ratio": 0.409 }, "light": { - "lines": 7807, + "lines": 7815, "ratio": 0.427 }, "platform": { - "lines": 4185, - "ratio": 0.37 + "lines": 4198, + "ratio": 0.371 }, "ui": { "lines": 1518, "ratio": 0.278 }, "test": { - "lines": 6063, + "lines": 6073, "ratio": 0.198 }, "moondeck": { @@ -54,20 +54,20 @@ } }, "tests": { - "cases": 1007, + "cases": 1008, "scenarios": 22 }, "docs": { "md_files": 170, - "md_lines": 22809, + "md_lines": 22818, "plans_files": 90, "backlog_lines": 3114, "lessons_lines": 418, "claude_md_lines": 135 }, "complexity": { - "functions": 2187, - "over_threshold": 139, + "functions": 2188, + "over_threshold": 140, "worst_ccn": 93 } } diff --git a/docs/metrics/repo-health.md b/docs/metrics/repo-health.md index 438106a9..396c38d0 100644 --- a/docs/metrics/repo-health.md +++ b/docs/metrics/repo-health.md @@ -1,6 +1,6 @@ # Repo health -Measured at `bf386866`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run โ€” **do not edit by hand**. +Measured at `cca3fe8e`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.** Current state only; the trend is this file's git history (`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human. @@ -8,19 +8,19 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Target | Flash | |---|---:| -| desktop | 923 KB | +| desktop | 924 KB (+0 KB) โš  | | esp32 | 1,640 KB | | esp32p4-eth | 1,468 KB | | esp32p4-eth-wifi | 1,752 KB | | esp32s3-n16r8 | 1,628 KB | | esp32s3-n8r8 | 1,628 KB | -| esp32s31 | 1,880 KB (โˆ’0 KB) โœ“ | +| esp32s31 | 1,880 KB | ## Render performance | Target | Tick | FPS | |---|---:|---:| -| desktop | 127 ยตs (โˆ’1 ยตs) โœ“ | 7,874 (+62) โœ“ | +| desktop | 127 ยตs | 7,874 | | esp32 | 4,164 ยตs | 240 | ## Code @@ -28,25 +28,25 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Area | Lines | Comments | Comment share | |---|---:|---:|---:| | core | 14,889 | 5,583 | 40.9 % | -| light | 20,198 (+7) โš  | 7,807 | 42.7 % | -| platform | 12,501 (+8) โš  | 4,185 | 37.0 % | +| light | 20,214 (+16) โš  | 7,815 | 42.7 % | +| platform | 12,526 (+25) โš  | 4,198 | 37.1 % (+0.1 %) โš  | | ui | 5,811 | 1,518 | 27.8 % | -| test | 35,429 (+105) โš  | 6,063 | 19.8 % | -| moondeck | 19,968 (+25) โš  | 3,187 | 18.3 % | +| test | 35,504 (+75) โš  | 6,073 | 19.8 % | +| moondeck | 19,968 | 3,187 | 18.3 % | ## Tests | Kind | Count | |---|---:| -| unit cases | 1,007 | +| unit cases | 1,008 (+1) โœ“ | | scenarios | 22 | ## Complexity | Metric | Value | |---|---:| -| functions | 2,187 | -| over threshold | 139 | +| functions | 2,188 (+1) โœ“ | +| over threshold | 140 (+1) โš  | | worst CCN | 93 | ## Documentation @@ -54,7 +54,7 @@ Current state only; the trend is this file's git history (`git log -p docs/metri | Metric | Value | |---|---:| | markdown files | 170 | -| markdown lines | 22,809 | +| markdown lines | 22,818 (+9) โš  | | plan files | 90 | | backlog lines | 3,114 | | lessons lines | 418 | diff --git a/docs/performance.md b/docs/performance.md index 5efd30e2..42dd5ec1 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -259,28 +259,37 @@ The **acceptance floors** these establish for the parallel backends: RMT **8ร—25 ## Panel cards over raw Ethernet (`PanelCardDriver`, ESP32-S31) -Measured on an S31 driving two 128ร—64 HUB75 panels through a ColorLight 5A-75 receiver card, gigabit -RGMII link, 2026-07-30. +Measured on an S31 driving two 128x64 HUB75 panels through a ColorLight 5A-75 receiver card over a +gigabit RGMII link, 2026-07-31. -| | ยตs/tick | note | +| | us/tick | note | |---|---:|---| -| **PanelCardDriver** | **2 636** | 16 384 lights: correction + 130 frames handed to the MAC | +| **PanelCardDriver** | **~2 500** | 16 384 lights: correction + 132 frames handed to the MAC | | PreviewDriver | 5 687 | the browser preview, same buffer | -| GameOfLifeEffect | 17 592 | the render, and the largest single cost | +| a heavy effect (GameOfLife) | 17 592 | the render, and the largest single cost | -The wall runs at **~32 FPS**, which is `1 000 000 / (2 636 + 5 687 + 17 592 + overhead)` โ€” the render -dominates, and the panel driver is the cheapest of the three active modules despite pushing 130 -packets per frame (2 brightness + 128 rows + 2 sync, at 497 pixels per row packet). +The panel driver is the cheapest active module despite pushing 132 packets per frame (2 brightness + +128 rows + 2 sync, at 497 pixels per row packet). The wall's frame rate is set by the render, not by +the output: a heavy effect at 17.6 ms dominates a 26 ms tick, giving ~32 FPS, while a lighter effect +mix measures ~56 FPS on the same wall. **The ceiling is packets, not pixels.** Each frame is sent synchronously from `tick()`, so a taller wall costs proportionally more rows; a 256-row wall would double the packet count. The card format's -1 Gbit requirement is a wire-time constraint rather than a bandwidth one โ€” at 100 Mbit the same bytes +1 Gbit requirement is a wire-time constraint rather than a bandwidth one: at 100 Mbit the same bytes take ten times as long and overrun the inter-frame window the sync depends on ([drivers.md](moonmodules/light/drivers.md#panelcard)). +**The DMA ring is what makes it stable.** `CONFIG_ETH_DMA_BUFFER_SIZE` defaults to 512 B, so a +1512 B frame spanned three descriptors and a 10-descriptor ring held ~3.3 frames while the driver +fires 132 back-to-back. At that depth the S31 refused ~19 000 frames and wedged twice inside 20 +minutes; at 1536 B per buffer (one descriptor per frame) plus `CONFIG_ETH_TRANSMIT_MUTEX`, it runs +clean. Both are bench-isolated, and ring COUNT is not the lever: 30 descriptors ran no cleaner than +10. Cost: ~20 KB of internal DMA RAM, since the size applies to both rings +([lessons.md](history/lessons.md)). + **Static RAM: 0 B.** The driver's 1 512 B packet buffer is a class member, so it costs nothing on a board that never adds the driver; `check_footprint --module PanelCardDriver --firmware esp32s31` -reports 3 270 B of flash and no static RAM. +reports ~3 500 B of flash and no static RAM. No scenario contract yet: the driver needs a receiver card on the wire, so the numbers above are a bench record rather than an asserted ceiling. diff --git a/esp32/sdkconfig.defaults.esp32s31 b/esp32/sdkconfig.defaults.esp32s31 index ce0627fb..76770ee2 100644 --- a/esp32/sdkconfig.defaults.esp32s31 +++ b/esp32/sdkconfig.defaults.esp32s31 @@ -46,11 +46,11 @@ CONFIG_ETH_USE_ESP32_EMAC=y # it synthesises 125 MHz precisely. Bench-proven on the S31 CoreBoard. CONFIG_ETH_EMAC_RGMII_TX_CLK_SRC_APLL=y # A panel-card frame is 1512 B. At the 512 B default every frame spanned THREE descriptors, so a -# 10-descriptor ring held only ~3 frames while PanelCardDriver fires 132 back-to-back โ€” and the ring +# 10-descriptor ring held only ~3 frames while PanelCardDriver fires 132 back-to-back, and the ring # ran dry within minutes (bench: ~19 000 refused frames and two full transmit wedges in 20 minutes). # 1536 is the 64-byte-aligned size that fits one frame per descriptor. # -# The COUNT stays at 10: bench-isolated on 2026-07-31 โ€” buffer size and the transmit mutex below are +# The COUNT stays at 10: bench-isolated on 2026-07-31. Buffer size and the transmit mutex below are # what fix it, and 30 descriptors ran no cleaner than 10 (a 30-deep TX ring totals ~46 KB against # ~15 KB at 10). The size bump itself is not free: 512โ†’1536 applies to BOTH rings, so 20 buffers # cost ~20 KB more internal RAM than the default. Cheaper than the alternative, and measured. diff --git a/moondeck/check/check_esp32_built.py b/moondeck/check/check_esp32_built.py index 7d287911..a6afe87a 100644 --- a/moondeck/check/check_esp32_built.py +++ b/moondeck/check/check_esp32_built.py @@ -20,6 +20,7 @@ """ import argparse +import subprocess import sys import time from pathlib import Path @@ -49,7 +50,7 @@ SKIP_PREFIXES = ("src/platform/desktop/",) -def newest_source(): +def newest_source(only=None): """The most recently modified source file that feeds a firmware image.""" newest_path, newest_mtime = None, 0.0 @@ -67,11 +68,15 @@ def newest_source(): continue if path.suffix not in SOURCE_SUFFIXES: continue + if only is not None and rel_posix not in only: + continue # this firmware does not compile it, so it cannot make it stale mtime = path.stat().st_mtime if mtime > newest_mtime: newest_path, newest_mtime = path, mtime for rel in SOURCE_FILES: + if only is not None and rel not in only: + continue path = ROOT / rel if path.exists() and path.stat().st_mtime > newest_mtime: newest_path, newest_mtime = path, path.stat().st_mtime @@ -79,6 +84,39 @@ def newest_source(): return newest_path, newest_mtime +def compiled_sources(firmware): + """The files this firmware's build actually reads, from ninja's own dependency database. + + A firmware compiles a SUBSET of src/: a driver gated behind a per-firmware flag (MM_PANEL_CARDS) + is not in the S3 build at all. Comparing every source against that binary flags it stale forever + over a file it cannot compile, and no rebuild can clear it โ€” the build correctly does nothing, so + the binary's timestamp never moves. Asking ninja what it depends on is the authoritative answer. + + Returns None when the database cannot be read, and the caller then falls back to scanning + everything: over-reporting staleness is the safe direction for a freshness gate. + """ + build_dir = ROOT / "build" / f"esp32-{firmware}" + if not (build_dir / ".ninja_deps").exists(): + return None + try: + out = subprocess.run(["ninja", "-t", "deps"], cwd=build_dir, + capture_output=True, text=True, timeout=60) + except (subprocess.SubprocessError, OSError): + return None + if out.returncode != 0: + return None + deps = set() + for line in out.stdout.splitlines(): + line = line.strip() + if not line.startswith(("output", "deps")) and line: + path = (build_dir / line).resolve() + try: + deps.add(path.relative_to(ROOT).as_posix()) + except ValueError: + pass # outside the repo (IDF, toolchain) โ€” not ours to watch + return deps or None + + def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--firmware", required=True, @@ -100,7 +138,7 @@ def main(): return 1 built = binary.stat().st_mtime - newest_path, newest_mtime = newest_source() + newest_path, newest_mtime = newest_source(compiled_sources(args.firmware)) age_h = (time.time() - built) / 3600 if newest_path is not None and newest_mtime > built: diff --git a/moondeck/check/repo_health.py b/moondeck/check/repo_health.py index 68b7209f..eb8d5566 100644 --- a/moondeck/check/repo_health.py +++ b/moondeck/check/repo_health.py @@ -203,7 +203,7 @@ def snapshot(perf=None): def _valid_snapshot(data, source): - """A snapshot mapping, or {} โ€” and say so out loud when it is neither. + """A snapshot mapping, or {}, saying so out loud when it is neither. merge_carry_forward does `old.get(section, {})`, so a JSON list or scalar would raise, and a section holding a list would corrupt the merge. A malformed baseline must not read as "no @@ -211,12 +211,12 @@ def _valid_snapshot(data, source): slate rather than a broken file. """ if not isinstance(data, dict): - print(f"repo-health: ignoring {source} โ€” expected an object, got {type(data).__name__}", + print(f"repo-health: ignoring {source}: expected an object, got {type(data).__name__}", file=sys.stderr) return {} for key in ("flash", "perf", "complexity"): if key in data and not isinstance(data[key], dict): - print(f"repo-health: ignoring {source} โ€” section '{key}' is " + print(f"repo-health: ignoring {source}: section '{key}' is " f"{type(data[key]).__name__}, expected an object", file=sys.stderr) return {} return data @@ -226,7 +226,7 @@ def load_previous(): """The COMMITTED snapshot, read from git rather than from the working tree. The delta is meant to answer "what did this commit change", so the baseline has to be the last - commit โ€” not whatever a previous run left on disk. Reading the working-tree file made every run + commit, not whatever a previous run left on disk. Reading the working-tree file made every run after the first compare against the run before it, so a second run inside one commit showed a delta of ~0 and the real change vanished. Running the check twice must give the same answer. @@ -239,7 +239,7 @@ def load_previous(): if out.returncode == 0: return _valid_snapshot(json.loads(out.stdout), "the committed snapshot") except json.JSONDecodeError: - print("repo-health: the committed snapshot is not valid JSON โ€” " + print("repo-health: the committed snapshot is not valid JSON, " "falling back to the working tree", file=sys.stderr) except (subprocess.SubprocessError, OSError): pass @@ -248,7 +248,7 @@ def load_previous(): try: return _valid_snapshot(json.loads(_read(HEALTH_FILE)), str(HEALTH_FILE.name)) except json.JSONDecodeError: - print(f"repo-health: {HEALTH_FILE.name} is not valid JSON โ€” treating as empty", + print(f"repo-health: {HEALTH_FILE.name} is not valid JSON, treating as empty", file=sys.stderr) return {} @@ -346,7 +346,7 @@ def render_markdown(new, old): L = [f"# Repo health", "", f"Measured at `{new.get('commit', '?')}`. Generated by " f"[`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every " - f"KPI-gate run โ€” **do not edit by hand**.", "", + f"KPI-gate run. **Do not edit by hand.**", "", "Current state only; the trend is this file's git history " "(`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make " "growth visible, the judgment stays human.", ""] diff --git a/src/light/drivers/PanelCardDriver.h b/src/light/drivers/PanelCardDriver.h index ce48deaa..bba1dc5a 100644 --- a/src/light/drivers/PanelCardDriver.h +++ b/src/light/drivers/PanelCardDriver.h @@ -34,7 +34,8 @@ namespace mm { /// /// The deep dives are under *More info*, below the attribute/method lists: /// @xref{why-a-gigabit-link|why these cards need a gigabit link}, -/// @xref{running-this-on-a-host|running this on a desktop or a Pi}. +/// @xref{running-this-on-a-host|running this on a desktop or a Pi}, +/// @xref{other-card-vendors|other card vendors, and how to add one}. /// @card PanelCardDriver.png /// /// @moreinfo @@ -63,6 +64,35 @@ namespace mm { /// pin the wire format with no hardware and no privileges. /// /// On ESP32 `interface` is ignored: the chip has one MAC. +/// +/// ## Other card vendors +/// +/// ColorLight is one of several receiver-card makers, which is why this driver is named for the +/// category and carries a `format` selector rather than being a ColorLight driver: +/// +/// | vendor | position in the market | +/// |---|---| +/// | **[NovaStar](https://www.novastar.tech)** | the global leader; large-scale displays and stage events | +/// | **[ColorLight](https://www.colorlightinside.com)** | cost-effective, strong outdoor and fine-pitch support (the format implemented here) | +/// | **[Linsn](https://www.linsnled.com)** | affordable and stable, common on budget installations | +/// | **[Mooncell](https://www.mooncell.com.cn)** | full-color high-refresh niche | +/// | **[Huidu](https://www.huidu.cn)** | small and mid projects, storefront signage, mostly ASYNCHRONOUS | +/// | **DBstar**, **Xixun** | also in the field | +/// +/// **Contributions welcome.** Adding one is a `*Packet.h` beside ColorLight5A75Packet.h plus an +/// entry in `kFormatOptions`: the window, the correction, the chunking and the platform seam are +/// already shared, and the desktop capture path lets the byte layout be pinned by unit tests with no +/// hardware. What it actually costs is the research, not the code. +/// +/// Two things to know before starting. Each vendor speaks its **own proprietary L2 protocol**, so a +/// ColorLight frame will not drive a NovaStar card and the byte layout has to be obtained per +/// vendor. And whether another format fits this driver's row-plus-sync model is **unverified**: the +/// shape is an invitation, not a promise, and a format that addresses panels differently may need +/// the driver to grow rather than just gain a packet file. +/// +/// Huidu is the one to approach with care: its controllers are largely asynchronous, playing from +/// onboard storage rather than being fed live, which is a different product category from a +/// real-time sender. class PanelCardDriver : public DriverBase { public: /// Panel cards are RGB, so this references the "RGB" preset rather than the strips' "GRB" โ€” @@ -267,8 +297,16 @@ class PanelCardDriver : public DriverBase { /// class note says why a slow link is reported rather than refused. Runs on the 1 Hz path, so /// the snprintf here is the same accepted trade SystemModule makes. void writeLinkStatus() MM_NONBLOCKING { + // A failed restart outranks everything below: it is the one state a user cannot resolve by + // plugging a cable back in. Cleared only when frames flow again (see the re-arm below). + if (restartFailed_) { + setStatus("ethernet restart failed - restart the device", Severity::Error); + framesReported_ = framesSent_; + return; + } + // The WEDGE is checked before the link, because a wedged transmit path is defined by sends - // failing โ€” which is knowable whatever the link claims. Checking the link first hid this + // failing, which is knowable whatever the link claims. Checking the link first hid this // branch entirely on any platform reporting no link (the desktop stub among them), which is // how the recovery path shipped unreachable by its own tests. // @@ -292,6 +330,11 @@ class PanelCardDriver : public DriverBase { if (platform::ethRestartTx()) { setStatus("transmit wedged - restarting ethernet", Severity::Warning); } else { + // LATCHED: after a failed restart the driver is stopped, so no frame is sent, the + // streak stops growing, and the next tick would otherwise fall through to the + // link branch and report a plain "no ethernet link", indistinguishable from an + // unplugged cable, for a board that needs a power cycle. + restartFailed_ = true; setStatus("ethernet restart failed - restart the device", Severity::Error); } framesReported_ = framesSent_; @@ -329,9 +372,9 @@ class PanelCardDriver : public DriverBase { } // Re-arm the one-shot only on evidence of FLOW: frames sent this second and no failure // streak at all. Re-arming merely because the link reads healthy would fire again while a - // wedge is still rebuilding its streak, bouncing the interface every ~20 s โ€” the loop the + // wedge is still rebuilding its streak, bouncing the interface every ~20 s: the loop the // one-shot exists to prevent. - if (sent > 0 && failStreak == 0) restartTried_ = false; + if (sent > 0 && failStreak == 0) { restartTried_ = false; restartFailed_ = false; } if (dropped) { // Split by cause: a flapping link and a full TX ring are different faults with @@ -406,12 +449,15 @@ class PanelCardDriver : public DriverBase { uint32_t framesDroppedTotal_ = 0; /// Backing store for the status line (setStatus does not copy). Sized for the longest one: the /// split-drop report, which carries three cumulative counters and reaches ~63 chars at millions - /// of drops. Truncation would cut the RING count โ€” the number the line exists to show. + /// of drops. Truncation would cut the RING count, the number the line exists to show. char statusBuf_[96] = {}; /// Whether a restart has already been attempted for the CURRENT wedge. One attempt per wedge: /// a restart cannot fix an unplugged cable, and retrying every second would bounce the interface /// under the user. Cleared as soon as frames flow again. bool restartTried_ = false; + /// Set when a recovery attempt itself failed, which leaves the interface stopped. Latched so the + /// error cannot be overwritten by the softer link warning on the next tick. + bool restartFailed_ = false; /// Whether this driver currently holds the raw-L2 claim, so prepare/release stay balanced /// however often the framework calls them. bool claimed_ = false; diff --git a/src/platform/desktop/platform_desktop.cpp b/src/platform/desktop/platform_desktop.cpp index 8d6e60f3..5d3fa4ed 100644 --- a/src/platform/desktop/platform_desktop.cpp +++ b/src/platform/desktop/platform_desktop.cpp @@ -720,8 +720,9 @@ bool ethTestSendFails_ = false; uint16_t ethTestLinkSpeed_ = 1000; // desktop reports gigabit unless a test says otherwise int ethRawClaims_ = 0; // drivers holding the link for direct L2 use uint32_t ethSendFails_ = 0; // consecutive ethSendRaw failures (the streak) -uint32_t ethFailTotal_ = 0; // cumulative since boot โ€” what ethSendFailCounts reports +uint32_t ethFailTotal_ = 0; // cumulative since boot; what ethSendFailCounts reports uint32_t ethRestarts_ = 0; // ethRestartTx() calls, for the once-per-wedge test +bool ethRestartFails_ = false; // simulated recovery failure // The bound raw socket, or -1 for capture mode (the default, and all any test sees). int ethRawFd_ = -1; unsigned ethRawIfIndex_ = 0; // Linux AF_PACKET needs the index; BPF binds by name @@ -828,8 +829,15 @@ void ethSendFailCounts(uint32_t& linkDown, uint32_t& ringFull) MM_NONBLOCKING { } // A host raw socket has no driver-internal link state to desync, so there is nothing to -// restart โ€” clear the streak so a test can exercise the driver's recovery path. -bool ethRestartTx() { ethSendFails_ = 0; ethRestarts_++; return true; } +// restart, so clear the streak and let a test exercise the driver's recovery path. +bool ethRestartTx() { + ethRestarts_++; + if (ethRestartFails_) return false; + ethSendFails_ = 0; + return true; +} + +void setTestEthRestartFails(bool fail) { ethRestartFails_ = fail; } uint32_t ethRestartCountForTest() { return ethRestarts_; } diff --git a/src/platform/esp32/platform_esp32.cpp b/src/platform/esp32/platform_esp32.cpp index aac03773..3572603f 100644 --- a/src/platform/esp32/platform_esp32.cpp +++ b/src/platform/esp32/platform_esp32.cpp @@ -937,14 +937,21 @@ bool ethRawL2Claimed() MM_NONBLOCKING { // Consecutive failures, so a caller can distinguish back-pressure from a wedged path (see // platform.h). Written on the render task, read by the driver's 1 Hz status tick. static std::atomic ethSendFails_{0}; -// Split by cause โ€” see platform.h. esp_eth_transmit checks the link BEFORE the MAC, so the two +// Split by cause; see platform.h. esp_eth_transmit checks the link BEFORE the MAC, so the two // errors are genuinely distinct conditions rather than degrees of the same one. static std::atomic ethFailLinkDown_{0}; static std::atomic ethFailRingFull_{0}; bool ethSendRaw(const uint8_t* frame, size_t len) MM_NONBLOCKING { if (!ethHandle_ || !frame || len == 0) return false; - if (!ethLinkUp_.load(std::memory_order_relaxed)) return false; + if (!ethLinkUp_.load(std::memory_order_relaxed)) { + // Counted, not silent: the driver counts every false into its own total, so skipping this + // one would make `dropped` and the per-cause totals describe different sets of frames. + // Deliberately NOT part of the streak: the streak drives wedge detection and re-arming, + // and a link genuinely down is the case a restart cannot fix. + ethFailLinkDown_.fetch_add(1, std::memory_order_relaxed); + return false; + } const esp_err_t err = esp_eth_transmit(ethHandle_, const_cast(frame), len); if (err != ESP_OK) { ethSendFails_.fetch_add(1, std::memory_order_relaxed); @@ -975,11 +982,14 @@ bool ethRestartTx() { // Clear our own flag first: the restart re-runs negotiation and the CONNECTED event sets it // again if the link really comes back. Leaving it true would keep ethSendRaw trying against a // driver that is mid-restart. - ethLinkUp_.store(false, std::memory_order_relaxed); - ethSendFails_.store(0, std::memory_order_relaxed); // A failed stop leaves the driver in a state we did not establish; starting on top of that - // would compound it. Report instead โ€” the caller turns this into a "restart the device" status. + // would compound it. Report instead: the caller turns this into a "restart the device" status. + // Nothing is cleared BEFORE this point: clearing ethLinkUp_ first and then failing would leave + // transmit permanently refused behind a "no ethernet link" warning, with no event able to set + // the flag again. if (esp_eth_stop(ethHandle_) != ESP_OK) return false; + ethLinkUp_.store(false, std::memory_order_relaxed); + ethSendFails_.store(0, std::memory_order_relaxed); return esp_eth_start(ethHandle_) == ESP_OK; } diff --git a/src/platform/platform.h b/src/platform/platform.h index 6b8a6a7a..a8d2914c 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -383,9 +383,9 @@ bool ethRawL2Claimed() MM_NONBLOCKING; // Failures split by cause since boot, because the two are different faults with different fixes and // a single counter cannot tell them apart: -// linkDown โ€” esp_eth_transmit refused before touching the MAC because the driver's link reads +// linkDown: esp_eth_transmit refused before touching the MAC because the driver's link reads // down. A flapping PHY shows up here, and each flap costs seconds of refusals. -// ringFull โ€” the MAC had no free TX descriptor. That is back-pressure: our sender outrunning the +// ringFull: the MAC had no free TX descriptor. That is back-pressure: our sender outrunning the // wire, or the DMA draining slower than it should. void ethSendFailCounts(uint32_t& linkDown, uint32_t& ringFull) MM_NONBLOCKING; @@ -397,7 +397,7 @@ uint32_t ethSendFailStreak() MM_NONBLOCKING; // Restart the Ethernet driver after transmit has provably wedged: esp_eth_transmit refuses every // frame once the driver's internal link state reads down, and that state can diverge from both the -// PHY and our own event-driven flag โ€” observed on an S31 under sustained TX, with the link genuinely +// PHY and our own event-driven flag, observed on an S31 under sustained TX, with the link genuinely // lost, no DISCONNECTED event delivered, and nothing recovering short of a reboot. // // stop/start re-runs the driver's link negotiation, which is the only supported way back (no ioctl @@ -405,11 +405,15 @@ uint32_t ethSendFailStreak() MM_NONBLOCKING; // streak rather than on ordinary back-pressure. Returns true when the restart succeeded; the link // may still be down afterwards if the cable really is out, which is the honest outcome. // -// NOT MM_NONBLOCKING, and called only from the 1 Hz status tick โ€” never from the render path. Both -// halves are register writes plus an esp_timer stop/start (no semaphore wait, no link poll), so the -// cost is microseconds rather than the seconds negotiation itself takes; the link comes back later -// via the CONNECTED event, not by blocking here. It runs only in a wedged state, where by -// definition no frames are going out anyway. +// BLOCKS FOR UP TO ~4 SECONDS, and the caller must treat that as the cost of the call. esp_eth_start +// restarts autonegotiation, which polls the PHY status register with vTaskDelay(100 ms) up to +// autonego_timeout_ms (4000 by default, and a wedged link is down so the loop runs to the timeout). +// NOT MM_NONBLOCKING for that reason. +// +// The trade is deliberate: the tick that calls this is the 1 Hz housekeeping tick, and it only fires +// in a wedged state where every frame is being refused anyway. A stalled render loop for one tick +// costs nothing a user can see when the wall is already dark, and the alternative is a wall that +// stays dark until someone power-cycles the board. Called at most once per wedge, never in a loop. bool ethRestartTx(); // Negotiated link speed in Mbit/s (10 / 100 / 1000); 0 when no link or no driver. Reported rather @@ -445,6 +449,9 @@ void ethTestClearFrames(); void setTestEthSendFails(bool fail); // Override the reported link speed so a test can exercise the too-slow-link status. void setTestEthLinkSpeed(uint16_t mbps); +// Make ethRestartTx() fail, so a test can exercise the recovery-failed path, the one case +// that strands transmit and must stay visible rather than reading as an unplugged cable. +void setTestEthRestartFails(bool fail); // How many times ethRestartTx() has run, so a test can pin the once-per-wedge bound (the driver's // own restartTried_ is private, and the count is what the bound is actually about). uint32_t ethRestartCountForTest(); diff --git a/test/python/test_repo_health_baseline.py b/test/python/test_repo_health_baseline.py index f51c21f0..d5ed9881 100644 --- a/test/python/test_repo_health_baseline.py +++ b/test/python/test_repo_health_baseline.py @@ -2,7 +2,7 @@ Two separate loaders read it: `load_previous` (the COMMITTED snapshot, what the delta compares against) and `load_working_tree` (the newest numbers, what carry-forward preserves). Both feed -`merge_carry_forward`, which does `old.get(section, {})` โ€” so a JSON list or scalar where an object +`merge_carry_forward`, which does `old.get(section, {})`, so a JSON list or scalar where an object belongs would raise, and a section holding a list would corrupt the merge. A malformed file must also not read as "no previous numbers": that reports every metric as new, @@ -37,7 +37,7 @@ def test_a_section_of_the_wrong_shape_is_rejected(): def test_a_valid_snapshot_passes_through_unchanged(): - """The guard must not damage the normal case โ€” this is the control for the rejections above.""" + """The guard must not damage the normal case; this is the control for the rejections above.""" good = {"flash": {"esp32": 1}, "perf": {}, "complexity": {}, "other": 5} assert repo_health._valid_snapshot(good, "t") is good @@ -55,7 +55,7 @@ def test_carry_forward_keeps_previous_values_for_unmeasured_sections(): new = {"flash": {"esp32": 150}} merged = repo_health.merge_carry_forward(new, old) assert merged["flash"]["esp32"] == 150 # this run measured it - assert merged["flash"]["esp32s3"] == 200 # this run did not โ€” the old value survives + assert merged["flash"]["esp32s3"] == 200 # this run did not, so the old value survives assert merged["perf"]["tick"] == 5 @@ -90,3 +90,46 @@ def test_a_good_file_on_disk_loads(tmp_path, monkeypatch): good.write_text(json.dumps({"flash": {"esp32": 1}}), encoding="utf-8") monkeypatch.setattr(repo_health, "HEALTH_FILE", good) assert repo_health.load_working_tree() == {"flash": {"esp32": 1}} + + +# ---- the git baseline (the behaviour this branch adds) ---- + +def test_the_baseline_comes_from_the_commit_not_the_working_tree(tmp_path, monkeypatch): + """Running the check twice must give the same delta. + + Reading the working-tree file made every run after the first compare against the run BEFORE it, + so a second run inside one commit showed a delta of ~0 and the real change vanished. The + baseline has to be the committed snapshot. + """ + import subprocess + repo = tmp_path / "repo" + (repo / "docs" / "metrics").mkdir(parents=True) + health = repo / "docs" / "metrics" / "repo-health.json" + + run = lambda *a: subprocess.run(a, cwd=repo, check=True, capture_output=True) + run("git", "init", "-q") + run("git", "config", "user.email", "t@t") + run("git", "config", "user.name", "t") + health.write_text(json.dumps({"flash": {"esp32": 100}}), encoding="utf-8") + run("git", "add", "-A") + run("git", "commit", "-qm", "baseline") + + # A later run overwrites the working tree; the COMMITTED value must still be the baseline. + health.write_text(json.dumps({"flash": {"esp32": 999}}), encoding="utf-8") + + monkeypatch.setattr(repo_health, "ROOT", repo) + monkeypatch.setattr(repo_health, "HEALTH_FILE", health) + assert repo_health.load_previous() == {"flash": {"esp32": 100}} # from the commit + assert repo_health.load_working_tree() == {"flash": {"esp32": 999}} # from disk + + +def test_the_baseline_falls_back_to_disk_when_git_cannot_answer(tmp_path, monkeypatch): + """A fresh checkout with no commit yet must still produce a first snapshot rather than erroring.""" + repo = tmp_path / "norepo" + (repo / "docs" / "metrics").mkdir(parents=True) + health = repo / "docs" / "metrics" / "repo-health.json" + health.write_text(json.dumps({"flash": {"esp32": 7}}), encoding="utf-8") + + monkeypatch.setattr(repo_health, "ROOT", repo) + monkeypatch.setattr(repo_health, "HEALTH_FILE", health) + assert repo_health.load_previous() == {"flash": {"esp32": 7}} diff --git a/test/scenarios/light/scenario_perf_full.json b/test/scenarios/light/scenario_perf_full.json index b4db61b9..acd66ca4 100644 --- a/test/scenarios/light/scenario_perf_full.json +++ b/test/scenarios/light/scenario_perf_full.json @@ -1956,7 +1956,7 @@ "desktop-macos": { "tick_us": [ 1, - 8 + 15 ], "free_heap": [ 0, @@ -1968,7 +1968,7 @@ ], "at": [ "2026-06-17", - "2026-07-10" + "2026-07-31" ] }, "esp32": { diff --git a/test/scenarios/light/scenario_peripheral_grid_sweep.json b/test/scenarios/light/scenario_peripheral_grid_sweep.json index ec1be1eb..bc1bd77e 100644 --- a/test/scenarios/light/scenario_peripheral_grid_sweep.json +++ b/test/scenarios/light/scenario_peripheral_grid_sweep.json @@ -645,7 +645,7 @@ "desktop-macos": { "tick_us": [ 68, - 227 + 350 ], "free_heap": [ 0, @@ -657,7 +657,7 @@ ], "at": [ "2026-07-26", - "2026-07-28" + "2026-07-31" ] } } @@ -1311,7 +1311,7 @@ "desktop-macos": { "tick_us": [ 68, - 245 + 284 ], "free_heap": [ 0, @@ -1323,7 +1323,7 @@ ], "at": [ "2026-07-26", - "2026-07-28" + "2026-07-31" ] } } diff --git a/test/unit/light/unit_DistortionWavesEffect.cpp b/test/unit/light/unit_DistortionWavesEffect.cpp index 4b664b4f..5f14e3aa 100644 --- a/test/unit/light/unit_DistortionWavesEffect.cpp +++ b/test/unit/light/unit_DistortionWavesEffect.cpp @@ -4,7 +4,7 @@ #include "light/layouts/Layouts.h" #include "light/effects/DistortionWavesEffect.h" #include "light/layouts/GridLayout.h" -#include "platform/platform.h" // setTestNowMs โ€” freeze millis() for a deterministic phase +#include "platform/platform.h" // setTestNowMs: freeze millis() for a deterministic phase // Restore the real clock after any test that froze it, so a frozen value can't leak into // order-dependent neighbours. @@ -36,7 +36,7 @@ TEST_CASE("DistortionWavesEffect produces spatial variation") { // The clock is FROZEN because the pattern moves with time: a light's hue is the average of a // horizontal and a vertical sine, both advanced by the elapsed-time phase. Layer::tick() reads // platform::millis(), so a single-tick test starts at whatever the process uptime happens to - // be โ€” and at ~2.3% of phases the two sampled lights land on the same hue and this assertion + // be, and at ~2.3% of phases the two sampled lights land on the same hue and this assertion // fails. It flaked in CI on exactly that. Freezing makes the phase (and so the property) fixed. ClockGuard guard; mm::platform::setTestNowMs(1); diff --git a/test/unit/light/unit_PanelCardDriver.cpp b/test/unit/light/unit_PanelCardDriver.cpp index bf644ea9..2b5ddaad 100644 --- a/test/unit/light/unit_PanelCardDriver.cpp +++ b/test/unit/light/unit_PanelCardDriver.cpp @@ -14,6 +14,7 @@ #include "light/layouts/Layouts.h" #include "light/layouts/GridLayout.h" #include "light/layouts/PanelsLayout.h" +#include // status() substring checks namespace { @@ -56,6 +57,12 @@ void setUp(mm::PanelCardDriver& driver, mm::Buffer& source, Wall& wall, // destroyed without release() being called, so the claim count would otherwise carry across cases. void clearClaims() { while (mm::platform::ethRawL2Claimed()) mm::platform::ethClaimRawL2(false); + // The platform's simulated-failure switches are process-global: a test that leaves one set + // poisons whichever test runs next, which is how a passing-alone/failing-in-sequence result + // appears. Reset them here so every case starts from a known platform state. + mm::platform::setTestEthSendFails(false); + mm::platform::setTestEthRestartFails(false); + mm::platform::ethTestClearFrames(); } // The packet-type byte of a captured frame (offset 12), which says row / sync / brightness. @@ -397,6 +404,21 @@ TEST_CASE("PanelCardDriver sends nothing when the buffer covers no row") { // A transmit path that refuses everything gets ONE recovery attempt, not one per second: a restart // cannot fix an unplugged cable, and retrying would tear the interface down repeatedly under a user // who is watching the card to find out what is wrong. +// Drive the failure streak past the wedge threshold. Split out because a test that does not REBUILD +// the streak between ticks proves nothing: the restart clears it, so the wedge branch is simply not +// re-entered and the guard under test never runs. +static void wedge(mm::PanelCardDriver& driver, uint32_t fromMs) { + mm::platform::setTestEthSendFails(true); + for (int i = 0; i < 200; i++) { + mm::platform::setTestNowMs(fromMs + i * 30); + driver.tick(); + } + REQUIRE(mm::platform::ethSendFailStreak() >= 500); +} + +// A wedge earns ONE recovery attempt. The second and third wedges in the same episode must NOT fire +// again: a restart cannot fix an unplugged cable, and retrying would tear the interface down every +// tick under a user reading the card to find out what is wrong. TEST_CASE("PanelCardDriver attempts recovery once per wedge") { clearClaims(); mm::Buffer source; @@ -404,53 +426,63 @@ TEST_CASE("PanelCardDriver attempts recovery once per wedge") { Wall wall(64, 1); setUp(driver, source, wall, 64); - // Drive the failure streak past the wedge threshold. - mm::platform::setTestEthSendFails(true); - for (int i = 0; i < 200; i++) { - mm::platform::setTestNowMs(1000 + i * 30); - driver.tick(); - } - REQUIRE(mm::platform::ethSendFailStreak() >= 500); - + wedge(driver, 1000); const uint32_t before = mm::platform::ethRestartCountForTest(); driver.tick1s(); CHECK(mm::platform::ethRestartCountForTest() == before + 1); // fired + + // Rebuild the streak so the wedge branch is genuinely re-entered; without this the guard is + // never reached and the assertion below would pass even with the guard deleted. + wedge(driver, 20000); driver.tick1s(); + wedge(driver, 40000); driver.tick1s(); - CHECK(mm::platform::ethRestartCountForTest() == before + 1); // and only once + CHECK(mm::platform::ethRestartCountForTest() == before + 1); // and still only once mm::platform::setTestEthSendFails(false); } -// A wedge that survives the restart is reported rather than retried: the second and third ticks -// must NOT fire again, or a wedge the restart cannot fix (an unplugged cable) would bounce the -// interface every tick under a user who is reading the card to find out what is wrong. -// -// Re-arming for a LATER wedge needs a live link, which the host stub does not have โ€” that half is -// bench-verified rather than pinned here, and the comment in writeLinkStatus says so. -TEST_CASE("PanelCardDriver reports a surviving wedge instead of restarting again") { +// A wedge that survives its restart is REPORTED, with the refusal count, rather than retried. +TEST_CASE("PanelCardDriver reports a wedge that survives its restart") { clearClaims(); mm::Buffer source; mm::PanelCardDriver driver; Wall wall(64, 1); setUp(driver, source, wall, 64); - mm::platform::setTestEthSendFails(true); - for (int i = 0; i < 200; i++) { - mm::platform::setTestNowMs(5000 + i * 30); - driver.tick(); - } - driver.tick1s(); // fires - const uint32_t after = mm::platform::ethRestartCountForTest(); + wedge(driver, 5000); + driver.tick1s(); // fires the one restart + wedge(driver, 25000); // still wedged afterwards + driver.tick1s(); + + CHECK(std::string(driver.status()).find("transmit wedged") != std::string::npos); + mm::platform::setTestEthSendFails(false); +} + +// A recovery that FAILS leaves the interface stopped, which no user action short of a power cycle +// resolves. That must not be reported as "no ethernet link", the message for an unplugged cable. +TEST_CASE("PanelCardDriver keeps a failed restart visible") { + clearClaims(); + mm::Buffer source; + mm::PanelCardDriver driver; + Wall wall(64, 1); + setUp(driver, source, wall, 64); + + mm::platform::setTestEthRestartFails(true); + wedge(driver, 60000); + driver.tick1s(); + CHECK(std::string(driver.status()).find("restart failed") != std::string::npos); - for (int i = 0; i < 5; i++) driver.tick1s(); // still wedged - CHECK(mm::platform::ethRestartCountForTest() == after); // and still only one restart + // Latched: the next tick must not soften it to the link warning. + driver.tick1s(); + CHECK(std::string(driver.status()).find("restart failed") != std::string::npos); + mm::platform::setTestEthRestartFails(false); mm::platform::setTestEthSendFails(false); } // Failures are counted by CAUSE, because a down link and a full TX ring are different faults with -// different fixes โ€” one total cannot tell them apart, which is what made a real bug unreadable. +// different fixes; one total cannot tell them apart, which is what made a real bug unreadable. TEST_CASE("PanelCardDriver counts send failures by cause") { clearClaims(); mm::platform::ethTestClearFrames(); @@ -469,7 +501,7 @@ TEST_CASE("PanelCardDriver counts send failures by cause") { mm::platform::setTestEthSendFails(false); mm::platform::ethSendFailCounts(linkDown, ringFull); - CHECK(ringFull > 0); // cumulative, not the streak โ€” it survives a later success + CHECK(ringFull > 0); // cumulative, not the streak; it survives a later success mm::platform::setTestNowMs(30100); driver.tick(); uint32_t after = 0;