Conversation
Lights around a screen can now follow what is on it. A Video service publishes a frame, an Ambilight effect gives each light the mean of the picture nearest it, and a Rectangle layout puts a strip around the display. Performance: not collected (no board attached this cycle). **Core** - VideoService publishes one frame per tick through a static seat, the same one-active-source election AudioService uses for its mic. Two sources: a synthesised test pattern that needs no hardware, and a binary PPM off the filesystem. - VideoFrame is a POD with a borrowed pointer and a sequence number. `seq` is compared for inequality only, never ordering, so a consumer can tell "this is the frame I already have" without a wraparound rule. **Light domain** - AmbilightEffect averages a rectangle of the source per light position. The mean rather than a sampled pixel, because a single pixel flickers on grain and moving edges. `saturation` pushes each channel back out from its zone's luma, since averaging mixes hues toward grey. - RectangleLayout emits a hollow perimeter, 2(width+height)-4 lights, with startCorner and clockwise describing the wiring rather than the shape. - Drivers gain gamma and per-channel white balance. Both fold into the brightness LUT the driver already builds, so the hot path stays one lookup per channel and neither costs anything per light. Gamma is applied before the linear scales, or a colour would shift as the brightness slider moved. **Tests** - The frame-to-light mapping end to end through the real static seam, the PPM header grammar, and the eight wiring permutations of the rectangle. **Docs** - Video service, Ambilight effect, Rectangle layout, and the two new driver controls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An HDMI grabber presents itself as a UVC webcam, so the lights can follow a games console or a set-top box rather than a file. The Video service's third source. Performance: not collected (no board attached this cycle). **Core** - The device's own format list drives a dropdown. It is read when a device enumerates, which happens before any stream opens, so it survives a request the device refuses: the case where knowing what it does offer matters most. Filtered to MJPEG, since nothing else is decodable here. **Light domain** - Nothing. The effect reads frames through the same seam whatever produced them. **Platform** - MJPEG off the wire, decoded by the P4's JPEG hardware. Uncompressed does not fit: 640x480 YUY2 at 60 fps is 37 MB/s against a USB 2.0 host's 24.6. - Decoding runs on a task of its own. jpeg_decoder_process() blocks and the render tick is MM_NONBLOCKING, so videoCaptureFrame is a triple-buffered index load and can carry the annotation honestly rather than suppressing the warning. - hasUsbVideo needs both a High-Speed USB PHY and a JPEG decoder. "Has USB" is not enough: the S3 has USB but only the slow kind. The source is not offered where it cannot work, and the component is not pulled into those builds. **Tests** - The desktop platform advertises no formats and does not offer the source, so a config restored from a capture-capable board falls back rather than selecting a dead option. **Docs** - The USB source, and how to choose a format from what the device lists. Untested on hardware: written from the component and IDF sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A white screen on 300 RGBW lights asks for about 12 A. Nothing stopped it, so a supply sized for the average could brown out on a menu, and the failure looks like a data problem rather than a power one. Performance: not collected (no board attached this cycle). **Light domain** - Correction::measure() prices the frame before the emit loop and sets a scale apply() folds into its existing lookup. Off unless maxCurrentMa is set, and the scale is 256 at unity so an unlimited frame is bit-exact. - Milliamps are configured per CHANNEL, not per light. A white die draws about twice a colour one, so a single per-light figure under-reports white-heavy frames, which is the direction that browns out a supply. This is the bug WLED carries as #3707. Defaults are measured on a 5 m SK6812 RGBW strip. - measure() reuses apply()'s own arithmetic, the same LUT and the same white derivation, so the estimate cannot drift from what is emitted. The same white frame costs 40 mA a light under Min and 16 under Accurate, and a limiter that assumed the worst would dim the cheaper mode for nothing. - Wired into RmtLedDriver. limitsCurrent() hides the controls on drivers that do not measure, rather than offering settings they ignore. **Tests** - The numbers, not just that something got smaller: off is bit-exact, a frame inside budget is untouched, an over-budget one halves, and the estimate follows whiteMode rather than a per-light constant. **Docs** - The three controls, and why the milliamps are per channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three gaps that only show up once the thing runs unattended behind a television. Performance: not collected (no board attached this cycle). **Core** - A stream that stopped sending left its last frame published for ever, so a console going to sleep lit the room with a frozen picture. VideoService drops it after staleMs and every effect falls back to black. A control rather than a constant, and 0 keeps the last picture for a source you would rather hold than blank. **Platform** - A disconnect logged and did nothing: recovery meant toggling the source by hand. The frame callback flags it and the decode task reopens, because uvc_host_stream_open blocks for up to its timeout and so belongs on neither the event callback nor the render tick. The decode task's existing wait doubles as the retry heartbeat. **Light domain** - PanelCardDriver joins RmtLedDriver in pricing its frame against the budget: its window is flat, so it is the same call. ParallelLedDriver still has none. Its encode forks across both cores, runs from two dispatch sites, and reads either the live buffer or a windowed snapshot. An under-counting limiter reports safe while the supply sags, which is worse than an absent one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Border averages went straight to the strip, so film grain and compression noise arrived as visible jitter. Lights now ease toward their new colour, and come up from black rather than snapping on. Performance: not collected (no board attached this cycle). **Light domain** - Each light walks a fraction of the way toward its new colour per frame. snapAbove lands anything big enough to be a scene cut at once, because smoothing a cut reads as the lights lagging the picture, which is the one moment you would notice. - The accumulators are 8.8, not bytes. That is the whole mechanism: a slow setting moves a channel a fraction of a count per frame, and in whole bytes every step rounds to zero and the light never arrives. Allocated only while smoothing is on, so off is the untouched path with nothing per pixel. - fadeInMs ramps the level up from black when a picture arrives after a gap: boot, a console waking, a grabber replugged. Its own control rather than a reuse of smoothing, which lags the colour and not the level. **Tests** - Convergence in both directions. A shift floors, so a falling channel's last steps round away from zero and a rising one's round to it: they arrive by different routes and only one of them for free. - The rig gained a tick that advances the source, because a test measuring per-frame behaviour against a frozen service measures nothing. **Docs** - The three controls, with the setting that matches Hyperion's default feel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A border light's zone is 1/height of the frame, a sliver at the very edge where compression is worst and thin dark borders live. It can now reach further in without changing how many lights there are. Performance: not collected (no board attached this cycle). **Light domain** - edgeDepth is a percentage of height for the top and bottom, of width for the sides, which is what Hyperion's two depth parameters mean between them. It samples about 8%. - It SETS the depth rather than raising a floor, so a value below a position's own share makes its zone thinner instead: useful when the strip sits against the bezel and should track the extreme edge. - The depth is rounded up, so a small percentage on a small frame cannot land on 0 and silently turn the control off. - Interior positions are on no edge, so a video wall is untouched at any setting, and 0 keeps the plain division. **Tests** - Off is the plain division exactly, the outer row demonstrably samples deeper, a value below the natural share makes it thinner, and every interior position is byte-identical. **Docs** - The control, and that it sets rather than floors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The driver current limiting matters most for was the one without it: many strands means more lights and more amps. Performance: not collected (no board attached this cycle). **Light domain** - measureFrame() runs before encodeRows forks across both cores, so both halves read a limit that is already settled and no atomics are needed. The fork was the objection to doing this at all, and it dissolves once the measure sits outside the parallel section rather than inside it. - The source resolves exactly as encodeRows does. A snapshot is pre-biased by -winStart_, so one base serves either. - laneStart_ is a running sum of laneCounts_, so the lanes tile from winStart_ and one flat walk of their total covers precisely what the encode will touch. **Tests** - Three uneven lanes: sizing the pass by the longest instead of the sum under-counts, which reports a frame safe while the supply sags. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 2.35:1 film puts black bars exactly where the top and bottom lights look, so they go dark while the screen is bright. The lights now map across the picture found inside the frame. Performance: not collected (no board attached this cycle). **Light domain** - edgeDepth cannot fix this, which is why detection is a separate thing: it widens a zone from the edge, so the bar stays inside it however deep it reaches. - With no bars found the picture is the frame, so off is the untouched path. - barLevel because bars are not black after compression. 12, about 5%, matches Hyperion. - A reading is refused past 40% of an axis, so a dark SCENE cannot blank the strip, and adopted only after 30 agreeing frames, so bars appearing at a cut do not twitch the mapping. - Eight probes per scanned line rather than every pixel. A bar is uniform, and the whole scan costs a fraction of one averaging pass. **Tests** - A letterboxed PPM: the top light reads black on the first frame and the picture after the hysteresis window. A frame that fills the picture reports no bars, so a full-frame source is never cropped. **Docs** - The two controls, and what to suspect when bars are never detected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The old 640x480 is 4:3, so a 16:9 source letterboxes into it and the top and bottom lights average bars on everything, menus included. Performance: not collected (no board attached this cycle). **Core** - 848x480 is the opening bid only. The device's own list still drives the dropdown, and it is read when one enumerates, so a wrong guess costs a single failed open rather than leaving the user with nothing to pick from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A border layout maps about 9% of its logical box to an LED, and the effect averaged all of it: the other 91% was work the mapping then discarded. Performance: not collected (no board attached this cycle). **Core** - MappingLUT::hasDestination answers it in O(1). An empty CSR run is two equal offsets, and an identity mapping short-circuits, so a video wall is unaffected. **Light domain** - The box is cleared first. Skipped positions never reach an LED, but PreviewDriver reads the raw buffer and would otherwise show a ghost image where none is lit. **Tests** - A real border layout, since every existing case used a grid where nothing is ever skipped and would have passed against a broken implementation. The perimeter carries the picture and all 36 interior positions are exactly black. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asking the mapping per position still walked a box that is mostly unlit, and cleared it every frame. On a 200x200 border layout that was 1.74 ms a frame of work whose result nothing reads. Performance: not collected (no board attached this cycle). **Light domain** - prepare() packs the positions that reach an LED into y<<16|x, and tick() walks those: no per-frame clear, no walk over positions the mapping discards. A one-off 3.1 KB at that size. - The unlit ones keep the black Layer::prepare() left on the same rebuild, which nothing but the preview reads. - A table-free mapping lights everything, so it keeps the plain loop rather than a list that would be 0,1,2,3... the size of the box. - The fallback when the list cannot be allocated still asks the mapping per position. Painting an unlit one leaves a ghost image in the preview, and a clear alone does not prevent that: the loop would overwrite it. **Tests** - The interior stays black across further frames, not only the first, so the dependency on Layer::prepare() clearing the buffer fails here rather than showing as a ghost in the preview. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two builds the layout could not describe: four separate strips rather than one bent around a frame, and a run that starts partway along an edge. Performance: not collected (no board attached this cycle). **Light domain** - sharedCorners off gives 2(w+h) instead of 2(w+h)-4. Each edge keeps its own end, so two lights land on every corner coordinate. A 20x10 box is 60 lights rather than 56. - offset slides where index 0 sits. It rides walkIndex's existing modular walk, so it composes with startCorner and a full lap wraps to nothing. - Both are wiring rather than shape, except that sharedCorners changes the light count. **Tests** - The count, that the extra lights land ON the corners rather than past them, and that offset rotates the order while leaving the coordinate set identical. **Docs** - Both controls, with the light count for a worked example. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The render loop can outrun the source, 60 Hz against 30 fps video, and re-averaging a frame it already painted produces the same pixels. Performance: not collected (no board attached this cycle). **Core** - The file source re-presents its picture per tick instead of publishing once and going quiet, the way a camera pointed at a still object does. Without that it was the one source whose seq never moved again, and the effect would have needed its own machinery to cope, a forced-render flag and a bar-hysteresis guard, to work around a source behaving unlike the others. **Light domain** - What the effect advances then runs at the source's rate rather than the loop's, which is the rate it should run at: the fade is wall-clock so it only samples less often, and the smoother has nothing to move toward while its target stands still. **Tests** - A repeated frame leaves the strip untouched rather than half-painted or cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 commits upstream. This branch is video IN, UVC capture and screen-follow lighting; that work is video OUT, HLS and NDI drivers and the P4's H.264 encoder. No overlap in platform.h, and none of this branch's new files exist upstream. Performance: not collected (no board attached this cycle). **Light domain** - Correction::apply() changed on both sides for different reasons and they compose. Upstream adds the srcChannels parameter, the master dimmer and the motion remap; this branch keeps the per-channel briLut and the current-limit scale. **Core** - addUint8 / addUint16 / addBool are now one overloaded addControl. Renamed at this branch's call sites in VideoService, DriverBase, AmbilightEffect and RectangleLayout. **Platform** - Dropped this branch's desktop audioMic stubs, superseded by platform_desktop_audio.cpp: keeping both gave duplicate symbols. **Scripts/MoonDeck** - Removed moondeck/event, deleted upstream. Resolved by taking upstream's generated files (repo-health, scenario observations) and keeping both sides everywhere the two had added in the same place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The current limiter missed a channel that draws at full every frame, and the prose this branch added did not meet the writing rules that arrived with v4.0.0. Performance: not collected (no board attached this cycle). **Light domain** - Correction::measure() counts a master dimmer. It is held at 255 every frame, and on an addressable strip every byte is a die: the IRGB preset puts a Dimmer on one of them, and 300 lights of that is amps the budget never saw. Outside the loop, since the value is a constant. On a fixture with its own supply this over-reports, which is the safe direction, and the drivers that feed one do not price frames at all. **Docs** - Rewrote this branch's four sections as bullets. The Video entry was stale: it still described only the test-pattern and file sources, with no usb, offered or staleMs. Added the format-choice guidance, the current-limit controls, and the two new Rectangle controls. **Tests** - A dimmer draws on a black frame, so the limiter sees it. Every line this branch adds is now free of em-dashes. Upstream's own prose is left alone: an earlier pass rewrote 281 lines of a file this branch had added 25 to, which was reverted.
Co-authored-by: Gohnnyman <57104366+Gohnnyman@users.noreply.github.com>
Fourteen findings from the PR review. Twelve fixed, one resolved by removing the feature it was about, one accepted with a reason. Performance: not collected (no board attached this cycle). **Light domain** - The current estimate summed into a `uint32_t`, which a 500x500 grid at the top of the milliamp range overflows three times over. A wrapped total reads as a small demand, so the limiter switched itself off exactly where it was needed. 64-bit throughout. - A master dimmer was priced as though the scale would shrink it, but apply() holds it at 255 whatever the limit says. It now comes off the budget first and the colours scale into what is left, with limit 0 when the fixed draw alone is over. - Yellow and UV are emitted from the same corrected RGB and were unpriced, so an RGBWYP fixture could exceed its cap. Each has its own milliamp figure, since amber sits near red while a UV die usually draws more, and both are shown only where the fixture carries them. - PanelCardDriver no longer claims to limit current. It streams over raw Ethernet to receiver cards with their own power, so there was nothing on this board's rail to cap. That also removes the reported hole in its raw fallback: the fix was to delete the feature rather than patch it, and it now matches NetworkSendDriver and Hue. - Turning black-bar detection off cleared the adopted bars but left `candidate_` and a saturated `stable_`, so re-enabling agreed with itself and never adopted again. All three are cleared. **Core** - The PPM parser checked that a byte followed maxval but not that it was whitespace, so "P6\n2 2\n255X" was accepted with the X eaten, shifting every channel one place. **Platform** - The advertised-format list used its count as a lock, which it is not: a reader can load a nonzero count in the instant before the writer clears it. Double buffered, so a rewrite cannot touch what a reader is copying. - Teardown waited 500 ms for the decode task, which may be inside uvc_host_stream_open's own 3 s. It then freed the semaphores and the JPEG engine underneath it. It now clears `lost` so no new reopen starts, wakes the task, and waits without a deadline. - A first open that found no device tore everything down, so plugging a grabber in after boot did nothing until the source was re-selected. It now keeps the decode task alive with `lost` set, which is the same retry a replug uses; reopen() sizes the buffers if the first open never got that far. - reopen() ignored a failed uvc_host_stream_start and cleared `lost` anyway, so retries stopped permanently while no frames could arrive. **Tests** - The letterbox fixture wrote to a hard-coded `build/` path while ctest roots the filesystem in the build tree, so the file landed where the service never looked. The test passed only when the binary ran from a checkout, and failed under ctest. It now asks for the resolved root. - Cases for the dimmer's fixed cost, the per-emitter figures, the whitespace separator, and detection turned off and on again. **Docs** - The two new milliamp controls, which drivers offer them and why the others do not, and how a dimmer is counted without being scaled. Not changed: the file source bumping `seq` per tick. The alternative asks the consumer to carry a forced-render flag and a hysteresis guard to accommodate one source that behaves unlike the others; re-presenting a still picture the way a camera aimed at a still object does removes both. VideoFrame's comment now states that contract rather than leaving it to be inferred.
Nine more findings, six of them defects the first round introduced or missed. Two were holes in fixes made an hour earlier. Performance: not collected (no board attached this cycle). **Platform** - The slot handoff read `published` and claimed it in two separate atomics. Between them the decoder could publish, then pick the slot just read as free and decode into a buffer being displayed. Both indices live in one word now, claimed with a compare-exchange; a lost race hands back the newer frame instead. - Two banks were not enough for the advertised-format list: a reader loads bank 0, one connect event publishes bank 1, and a second overwrites bank 0 underneath it. A seqlock, so a reader that sees the generation move across its copy retries. - A reopen only sized the decode buffers when there were none. Width and height are requests, so a replacement device can negotiate larger and every frame would then be dropped as oversized while the stream looked healthy. Sized on every reopen, grow-only. - videoCaptureFormatGeneration() so a consumer can notice a rewrite without copying the list. **Core** - A grabber plugged in after prepare() enumerates on its own, but nothing read the new list, so `offered` kept its "no device" placeholder until an unrelated rebuild. tick1s compares the generation and asks for a prepare, which keeps the reading and the control rebuild on the cold path where they belong. **Light domain** - The modelled draw was floored, so one channel at 254 cost 7.97 mA and read as 7: a 7 mA budget applied no limit. Rounded up, because a cap that understates is not a cap. - The lit-position list was sized by the box rather than by what goes in it, which is 156 KB on a 200x200 rectangle to hold 3 KB. Counted first, then sized. - The repeated-frame skip ran after a prepare() that had just cleared the layer, so a rebuild against a source holding a frozen frame stayed black for ever. Gated on `primed_` again, the term dropped as unnecessary in the first round. - Motion channels are still not priced, and the reason is now written where the dimmer is priced: on every fixture that really has pan and tilt those bytes are DMX control values drawing nothing from this rail. **Tests** - "Every light is written" asserted only that one was, so it passed while every non-corner position could have been skipped. It fills the buffer with a value the effect cannot produce and requires that none survives, which is 143 assertions the old form was not making. - The rounded estimate, and the per-emitter milliamp figures.
USB capture no longer chases a device that goes away. One open is one device at one negotiated format, buffers live from init to deinit, and a replug is picked up by re-opening rather than by patching the stream in place. The frame-gap timeout also loses its "hold forever" setting, which never matched how a UVC device behaves. Performance: not collected, no board attached (collect_kpi.py --commit needs one). Core - videoCaptureFrame's buffers are allocated in videoCaptureInit and freed in videoCaptureDeinit, never in between, so no task can reallocate while another reads. videoCaptureBufferGeneration() existed only to observe that race and is gone from the seam. - The UVC driver installs once, like the host library. Installing per open re-enumerated the attached device and bumped the format generation, which is the very signal the caller re-inits on. - Replug recovery moves out of the platform: a return re-enumerates, bumps the format generation, and VideoService re-opens on its own thread. Removing reopen() takes the stream handle out of the decode task, which is what forced the teardown ordering and the reallocation in the first place. - videoCaptureDeinit stops the stream, then joins the decode task, then frees. The join is unbounded on purpose: the 40 ms decode timeout bounds it. - VideoService closes the device through one door. The published frame borrows a platform buffer, so closeCapture() drops it before deinit; release() freed first and cleared after. - staleMs no longer treats 0 as "hold the last frame forever". A UVC device streams continuously whatever is on the wire, so a gap means the grabber stopped, not that the content paused. Floored above a frame interval: the render loop outruns the capture, so ordinary gaps would read as loss. - The negotiated-size status compares against what was last shown, not against the frame, which a stale drop resets to zero and made it re-fire on recovery. - PPM header limit raised to 256 bytes, with its own error. - hasUsbVideo requires CONFIG_IDF_TARGET_ESP32P4, so the capability flag agrees with the implementation gate. Light domain - presetHasRole() drives Yellow/UV control visibility from the selected preset rather than a correction_ that prepare() has not rebuilt yet. Tests - Source selection uses the named kSource* constants instead of bare indices. Docs/CI - services.md and effects.md follow the code; American spelling across our lines. - The graphify ignore rule drops its root anchor: output is written beside whatever path was scanned, so a nested one is output too, not a source dir. Reviews - 👾 Buffer lease ends at the next call: partly wrong, inUse holds the slot indefinitely. Fixed the real half by removing the mid-flight reallocation. - 👾 Seqlock payload must be atomic to be race-free in the C++ model: done. - 👾 Yellow/UV current not gated on white mode: kept ours, apply() already zeroes both when the mode is None. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A UVC capture now survives unrelated UI changes: adding an effect no longer tears the device down and renegotiates. The stream-open timeout was ten times longer than intended. Performance: not collected, no board attached (collect_kpi.py --commit needs one). Core - prepare() reopens only when the source, the selected format, or the device generation changed. Every tree-wide rebuild ran through it before, dropping the published frame and blocking on negotiation. - uvc_host_stream_open takes FreeRTOS ticks, not milliseconds: a raw 3000 was 30 s at the default 100 Hz tick, not 3 s. - The open targets the device the format list came from, and its first streaming function only. "Any device" could open one the list did not describe. Light domain - The sparse lit-list checks every z plane. A D2 effect's front slice is extruded across the depth, so a column whose LEDs sit only at z > 0 was skipped and got black. Tests - The fade-in test drives the test clock. It ran on the real one against a 4000 ms ramp, and both its bounds held on a fade stuck at black. - The wiring test compares every position, not the set of them. A set has no order, so six of the eight walks could run backwards and still pass. - scenario_Video_mutation: the producer/consumer pair through the Scheduler, including removing the service while the consumer renders. - The scenario runner learns Services, RectangleLayout, VideoService and AmbilightEffect, plus RectangleLayout's width/height props (ignored before, so a fixture silently measured the default). Registering Services also un-skips scenario_Audio_mutation's remove step, which had never run. Reviews - 👾 prepare() tears down a working stream: fixed. - 👾 sparse list only checks z = 0: fixed. - 👾 open discards the enumerated device address: fixed. Checks: spec drift, prose, platform boundary, hot-path, desktop build, unit tests (1698), scenarios and firmware freshness (P4 rev3 + classic ESP32) all pass. Three MoonLive scenarios fail identically on a clean tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A dark scene is no longer cropped to its middle. The current cap no longer charges for a dimmer channel that draws nothing. Performance: not collected, no board attached. Light domain - barFrom reaching its 40% ceiling reports no bar. It returned the ceiling, so an all-dark frame took bars on all four edges and kStableFrames held that crop past the scene. - measure() prices neither the dimmer nor motion: on the fixtures declaring them those bytes are DMX control values drawing nothing from this rail. The fixed-cost path also let an over-budget frame through with limit at 0. Tests - A dark-but-uneven frame pins the bar ceiling; a dimmer costs the estimate nothing. Docs/CI - drivers.md follows measure(); American spelling in our .clangd and comments. Reviews - 🐇 dark scene adopts 40% bars: done. - 🐇 fixed dimmer budget exceeds the cap: done, by not pricing it. - 🐇 RMT measure has no test: deferred. tick() opens with `if constexpr (rmtTxChannels == 0) return`, so it cannot run on the host; a test needs an RMT peripheral mock. measure and apply take the same winStart_ and n, so a mistake lights the wrong LEDs rather than only mispricing. - 🐇 hardware capture unverified: deferred to bench, no board yet. - 🐇 British spelling in three files: done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Video bytes are gamma-encoded, so their arithmetic mean is not the mean of the picture: half black and half white averaged to 127 where the light is 188, and a lit scene rendered as a dim mush. VideoFrame::tone now carries the source's curve to LINEAR light (12-bit, since linear has no headroom at the dark end); meanOf averages there and encodes once per light. SDR stops being the no-curve special case: sRGB is a curve like any other, and HDR had the same defect, re-encoding per pixel before averaging. whiteLevel: the W phosphor is separate hardware the RGB trims cannot reach, and it is often brighter than the trio, so whites blow out while colours look right. A fifth briLut row, pre-scaled like the balance trims so the curve still lands last and the hot path is unchanged. Priced by the limiter, or a trimmed white would be charged for current the strip never draws. Radio telemetry off the render tick. wifiStaRssi/TxPower were synchronous co-processor RPCs, 55-90 ms each on a P4, called once a second from inside the tick: measured 10% of wall clock lost to a periodic freeze. A poller task refreshes, the connect event carries BSSID and channel, the getters read a cache. Decode drops were silent. Four paths returned with no counter and no log, so a stuttering picture had nothing to look at. Counted per kind, warned once, and shown in the Video card as drop bad/noSlot/busy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every comment this branch touched now follows the one-line rule check_docgen enforces, with the depth moved into `@moreinfo` appendices. No behavior change: the diff is comments, docs and generated assets only. check_docgen: 510 errors to 0, warnings 3211 to 3090, which is the committed baseline, with no rule risen. **Core** - VideoService: a one-line class lead, the three sources and pixel ownership in an appendix, `///` on every public control and constant - VideoFrame: `///` on every member, the tone-curve rationale moved to the appendix **Light domain** - AmbilightEffect: file lead folded into the class comment, the source/destination table redrawn as markdown - RectangleLayout: the perimeter and wiring rules moved to an appendix, `///` on the six controls - Correction: `///` on briLut, the white-mode options and the per-channel current figures - ParallelLedDriver, RmtLedDriver: `///` on limitsCurrent **Platform** - platform.h: the UVC block documented member by member, each VideoCaptureStats counter named - platform_esp32_usbvideo: the file lead carries the three-thread and buffer-ownership appendices **Tests** - unit_VideoService, unit_AmbilightEffect, unit_RectangleLayout: leads folded into their `/// @module` block **Docs/CI** - screenshot_modules: RectangleLayout, AmbilightEffect and VideoService entries, plus the five card assets they generate - repo-health refreshed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream moved the module registry out of main.cpp and the scenario runner into the shared src/module_types.cpp, so the ambilight types move with it. **Conflicts** - src/main.cpp, test/scenario_runner.cpp: took upstream's structure, which no longer registers types in either file - src/module_types.cpp: RectangleLayout, AmbilightEffect and VideoService registered there instead, each in its alphabetical slot with its spec page - test/scenario_runner.cpp: kept the RectangleLayout construct-time width/height apply, rewritten to upstream's @Xref comment style beside the GridLayout pair it mirrors - docs/moonmodules/light/effects.md: took upstream's projectMM to MoonLight rename - docs/reference/metrics/repo-health: took upstream's snapshot, ours being the older reading Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The VideoService card's image was one directory level short, so it 404'd on the site. Two origin lines still named the project by its old name. KPI: 5184lights | Desktop:1985KB | tick:28/6us(FPS:35714/166666) | tick:6832us(FPS:146) | heap:32989KB | src:282(72569) | test:212(47359) | lizard:283w **Docs/CI** - services.md: the card image resolves from the page, which is what test_catalog_card_images_resolve_on_disk caught - layouts.md, effects.md: `Origin: projectMM` becomes `Origin: MoonLight`, matching upstream's rename - repo-health: measured on an ESP32-P4 rev3 running this branch, so the ESP32 tick is a live reading rather than a carried one **Tests** - the two scenarios the runner re-measured, scenario_Video_mutation gaining its desktop-macos observations Deltas: ESP32 tick 6,832 us (-1,522), FPS 146 (+27), heap 32,989 KB. esp32p4rev3-eth-wifi 2,440 KB, 60% of its partition. Unit cases 2,171 (+71), scenarios 12 (+1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: MoonModules/projectMM/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lights around a display follow what is on it: a Video service publishes frames, an Ambilight effect gives each light the mean of the picture nearest it, and a Rectangle layout puts a strip around the screen.
New modules
VideoService— three sources: a synthesised test pattern, a binary PPM off the filesystem, and USB capture.hdrdeclares the source curve (off / HDR10 PQ / HLG), since MJPEG carries no metadata.AmbilightEffect— a per-light box filter, averaged in linear light rather than in the encoding. Optional smoothing, edge depth, and letterbox detection.RectangleLayout— a hollow perimeter, with four start corners, a direction, shared or split corners, and a wiring offset.Capture is ESP32-P4 only. MJPEG off a UVC grabber, decoded by the P4's JPEG hardware. No other ESP32 has a High-Speed USB host, and at Full Speed MJPEG starves. Every other target links a stub whose init fails, which the service reports as a status rather than an error.
Two things ride along that are not about video: per-channel white balance plus a
whiteLevelfor the white die, and a cap on the current a frame may draw.Verified — 2071 unit cases, 12/12 scenarios, and on an ESP32-P4 rev3 running this branch: tick 6,832 µs, FPS 146, of which Ambilight is 114 µs.