Tests¶
Every test, by what it is about: the unit tests and scenarios, the Python and JS tests of the scripts and the web pages, and the checks with what each last found. Pick a subject, a module or a part of the system, to read its tests: what each one proves, where it lives, and the command that runs it. A scenario lists its steps, with the contract it is held to and the range it was observed in on each board. The strategy behind them is Testing strategy.
Core · Internals¶
ActiveInstance¶
- ActiveInstance: a survivor reclaims an emptied seat via claim()
- ActiveInstance: destroying a non-holder leaves the seat intact
- ActiveInstance: destructor vacates the seat (no dangling static)
- ActiveInstance: first claim wins, a second does not displace
- ActiveInstance: seat starts empty
- ActiveInstance: the seat is per type
- ActiveInstance: vacate releases only the holder's seat
Addressing¶
- a network proven to drop multicast adds a broadcast copy to occasional traffic, never to a light stream
- multicast sends one copy to the group, and broadcast one to every device
- unicast sends a copy to each host, skipping one not resolved yet
Buffer¶
- Buffer allocate and accessors
- Buffer allocate with zero returns false
- Buffer clear zeros data
- Buffer double free is safe
- Buffer move assignment
- Buffer move constructor
Color¶
- hsvToRgb black when value is zero
- hsvToRgb is constexpr
- hsvToRgb near-blue at h=170
- hsvToRgb near-green at h=85
- hsvToRgb produces color at intermediate hue
- hsvToRgb red at h=0
- hsvToRgb white when saturation is zero
- scale8
- scale8 is constexpr
Control¶
- a Text control with no validator accepts anything that fits
- a palette named with the longest name kept round-trips by name
- a per-control validator accepts a valid value and rejects bad input
- a persist-label Select saves the option string and loads by it
- applyControlValue applies an explicit zero when the key is present
- applyControlValue leaves a control untouched when its key is absent
- applyControlValue still applies a present key
- applyControlValue: a Select accepts an option label as a string value
- applyControlValue: a palette takes a name as well as an index, and an unknown name changes nothing
- applyControlValue: an empty Palette rejects/no-ops and never underflows to 255
- applyControlValue: an empty Select rejects/no-ops instead of accepting index 0
- applyControlValue: an overlong Select label does not prefix-match a real option
- applyControlValue: the Select overlong-label boundary is the longest name kept
- json::hasKey detects presence independent of value
- the validator enforces its length limit on the long end
- writeControlValue: a palette saved by name round-trips, and an unflagged one stays an index
- a file-path control carries the directory and extension the module declared
- a file-path control listing every file omits the extension filter
- a file-path control persists and reloads its name
- a file-path control stores a name, never a file body
- a file-path control with no directory offers no picker rather than a broken one
- addControl binds the widget its member's type calls for
- addControl carries an explicit range to the descriptor
- addControl without a range gets its own type's full range
- an int32 control carries a value no 16-bit control could hold
- an int32 control clamps a write past its range and refuses it under Strict
- an int32 control publishes its range to the UI
- an int32 control round-trips a negative value
- ControlType::List metadata carries a parallel detail array
- ControlType::List type identity + persistable + restore round-trip
- ControlType::List value serializes as an array of row summaries
- ControlType::List with an empty source emits []
- EditableList: a plain ListSource is not editable
- EditableList: a row id is stable across add / delete / reorder
- EditableList: add returns a fresh stable id each time
- EditableList: delete by id; a locked row is protected
- EditableList: edit a row field by id
- a list is written to flash unless its source is derived
DevicePlugin¶
- MmPlugin claims a presence packet carrying the MoonLight marker
- MmPlugin declines a plain WLED packet (no MoonLight marker)
- Plugins decline a short / garbage datagram, never read out of bounds
- WledPlugin claims a plain WLED packet as WLED
- WledPlugin declines a MoonLight-marked packet (that's a peer, not a WLED)
- WledPlugin tolerates an empty name (the module supplies the IP fallback)
FirmwareUpdate¶
- a HEX file or anything but a Teensy 4 image is refused from its first bytes
- a Teensy image without its vector table, without a MoonLight mark, or for another firmware is refused
- the built Teensy image passes every check, when the build tree has one
H264Bitstream¶
- H264Bitstream answers an empty buffer without reading past it
- H264Bitstream finds each frame start in a multi-frame buffer
- H264Bitstream ignores a trailing partial NAL
- H264Bitstream keeps the parameter sets with the frame they describe
- H264Bitstream opens an access unit on a first slice alone
- H264Bitstream opens no access unit on a parameter set or SEI
- H264Bitstream reads a multi-slice picture as one access unit
- H264Bitstream reads three-byte and four-byte start codes alike
- H264Bitstream reports a keyframe by its IDR NAL
HostList¶
- a blank host list is no hosts, which leaves the service idle
- a host list of bare host numbers continues the same subnet
- a host list range expands over the last octet
- a host list takes names and mixes them with addresses
- a host list's cap is enforced, not silently truncated
- a host list's full quads may switch subnet, and ranges and lists mix
- a malformed host list is refused, never guessed at
HostResolver¶
- a host name that never resolves stays waiting, and costs no lookup until it is due
- a host name that stops resolving keeps its last known address
- a host name waits for its first lookup, then answers its address
- on a board with no worker task, a name that fails is retried only once it is due
- on a board with no worker task, the asking side looks a name up one non-blocking step per ask
ImprovFrame¶
- ImprovFrameParser accepts a well-formed frame
- ImprovFrameParser detects bad checksum
- ImprovFrameParser handles a zero-length payload frame
- ImprovFrameParser handles back-to-back frames on the same instance
- ImprovFrameParser rejects oversize length byte
- ImprovFrameParser resyncs after garbage bytes
- ImprovFrameParser resyncs on aborted magic — stray 'I' restarts the search
- ImprovFrameParser resyncs on bad version when bad byte is 'I'
- ImprovFrameParser round-trips builder output for every frame type
- buildImprovFrame handles zero-length payload
- buildImprovFrame refuses oversize payload
- buildImprovFrame refuses too-small output buffer
- buildImprovFrame writes magic + version + type + length + payload + checksum
- improvChecksum is sum mod 256
ImprovOpReassembler¶
- a duplicate chunk is rejected and resets the buffer
- a multi-chunk op reassembles in order and NUL-terminates
- a non-zero opening seq (no fresh start) is rejected
- a single-frame op (seq 0, last 1) is Ready with the exact bytes
- an APPLY_OP chunk is refused while the previous document waits, or when malformed, so nothing overwrites an unapplied document
- an APPLY_OP document is acknowledged chunk by chunk and marked ready on its last chunk, on every transport alike
- an empty final chunk still completes (last with zero bytes)
- an out-of-order chunk (skipped seq) is rejected
- exactly buffer-minus-one bytes fits (boundary)
- overflow past the buffer (minus the NUL) is rejected, not truncated
- reset() drops a partial op
- seq 0 mid-stream abandons a partial op and starts fresh
InputMapping¶
InstallationId¶
- a different address produces a different installation id
- the installation id cannot be correlated with the network identity
- the installation id does not contain the underlying address
- the installation id is 32 hex characters
- the installation id is stable across calls
JsonSink¶
- JsonSink::detach hands over the heap buffer; sink frees nothing after
- JsonSink::detach is a no-op in fixed-buffer mode
- JsonSink::detach on an empty buffer-mode sink returns null, not a dangling block
- a finite value past the integer range still writes a number
- a non-finite reading writes null rather than unparseable text
- a whole value writes as an integer and a fraction compactly
- a document larger than one doubling still serializes whole
- a heap sink that runs out of memory says so instead of truncating silently
JsonUtil¶
- escaped quotes and backslashes round-trip inside a string value
- malformed inputs fail cleanly without crashing
- negative and fractional numbers
- no length cap: a long input parses (heap-sized text arena)
- no node cap: a large array parses (heap-grown node pool)
- overflow safety: nesting deeper than kMaxDepth fails cleanly
- parse a flat object reads each typed field
- parse a nested object
- parse an array of objects (the persisted device list use case)
- parseString decodes the standard JSON string escapes (symmetric with the writer)
MappingLUT¶
- MappingLUT 1:N mapping
- MappingLUT default is identity (no LUT)
- MappingLUT free and rebuild
- MappingLUT paged destinations read back identically
- MappingLUT setIdentity
- MappingLUT single-block path when a block fits (not paged)
ModuleFactory¶
- ModuleFactory: create returns a module of the registered type
- ModuleFactory: create returns nullptr for unknown type
- ModuleFactory: dynamic capacity grows past initial size
- ModuleFactory: registerType captures role via probe instance
- ModuleFactory: the registration probe lives on the heap, however large the module
- ModuleFactory: typeName / typeRole bounds-check
MoonLive¶
- a returned value reaches the return register on RISC-V
- a returned value reaches the return register on Thumb-2
- a returned value reaches the return register on Xtensa
- a scripted effect compiles for RISC-V, straight-line and looped
- a scripted effect compiles for Thumb-2, straight-line and looped
- a scripted effect compiles for Xtensa, straight-line and looped
- a scripted modifier compiles for RISC-V
- a scripted modifier compiles for Thumb-2
- a scripted modifier compiles for Xtensa
- an unencodable script is refused, not truncated, on RISC-V
- an unencodable script is refused, not truncated, on Thumb-2
- an unencodable script is refused, not truncated, on Xtensa
- every function in a class starts where a call can reach it, on RISC-V
- every function in a class starts where a call can reach it, on Thumb-2
- every function in a class starts where a call can reach it, on Xtensa
- every shipped script compiles for RISC-V
- every shipped script compiles for Thumb-2
- every shipped script compiles for Xtensa
- fill plus a loop on RISC-V emits what it currently can
- fill plus a loop on Thumb-2 emits what it currently can
- fill plus a loop on Xtensa emits what it currently can
- structs, joined conditions and the arithmetic builtins compile for RISC-V
- structs, joined conditions and the arithmetic builtins compile for Thumb-2
- structs, joined conditions and the arithmetic builtins compile for Xtensa
- taking a script function's value stashes it past the pool restore on RISC-V
- taking a script function's value stashes it past the pool restore on Thumb-2
- taking a script function's value stashes it past the pool restore on Xtensa
- the RISC-V assembler's tables fit a small heap block
- the RISC-V machine code for the shipped scripts is what it was
- the Thumb-2 assembler's tables fit a small heap block
- the Thumb-2 machine code for the shipped scripts is what it was
- the Xtensa assembler's tables fit a small heap block
- the Xtensa machine code for the shipped scripts is what it was
- the shipped grid layout compiles for RISC-V
- the shipped grid layout compiles for Thumb-2
- the shipped grid layout compiles for Xtensa
- emitted Xtensa code branches only to instruction boundaries
- emitted Xtensa code keeps every frame offset inside the frame
- emitted Xtensa code reads no register a call destroyed
- arm64 codegen: skipped (not an arm64 host)
- arm64: branchGeS branches on GE where branchGeU branches on HS
- arm64: load32 and store32 scale their immediate by four
- arm64: mulhi widens to 64 bits before taking the high word
- arm64: retValue moves the value into x0, the AAPCS64 return register
- arm64: shlImm and sarImm emit lsl and the ARITHMETIC asr
- arm64: the indexed 32-bit forms address base plus a register offset
- RISC-V mulhi emits mulh, one funct3 from mul
- RISC-V sarImm sets the arithmetic-shift bit that srli lacks
- Thumb-2 mulhi emits SMMUL, the signed high word, where mulReg emits MUL
- Thumb-2 sarImm emits ASR where shrImm emits LSR
- a Thumb-2 host call loads the address into r12 and calls it with BLX, never BL
- a Thumb-2 prologue saves r4-r11 and lr, then loads the fifth argument from the caller's sp
- x86_64 codegen test skipped: not an x86_64 host
- x86_64: a class with a script-to-script call compiles
- x86_64: a signed comparison branches on the signed condition, so a negative compares below zero
- x86_64: addReg(R0, R0, R1) is REX.W + 01 /r (in-place accumulate)
- x86_64: addReg(R0, R1, R2) inserts a mov first (d != a and d != b)
- x86_64: an unsigned comparison branches on the unsigned condition
- x86_64: bind + branchIfZero(R0, L) patches to the correct rel32 after finalize
- x86_64: branchNe emits cmp + jne (0F 85 rel32)
- x86_64: call() emits at least 14 vreg saves + a movabs + call rax + 14 restores
- x86_64: callLabel(L) patches to rel32 target - (site + 5)
- x86_64: load8(R0, R4, 3) is a movzx r32, byte ptr [ctrls + 3] with disp32
- x86_64: load8Idx(R0, R1, R2) is movzx r32, byte ptr [base+index] with SIB
- x86_64: movImm(R0, -1) sign-extends via the imm32 form (no negative-imm branch)
- x86_64: movImm(R0, 42) is mov r64, 42 (sign-extended imm32)
- x86_64: movImm(R2, 42) uses REX.B for the r8-r15 destination on Win64
- x86_64: movPtr(R0, addr) is the 10-byte movabs r64, imm64 form
- x86_64: movReg(R0, R0) elides the move (a no-op copy emits nothing)
- x86_64: movReg(R0, R1) is a 3-byte REX.W + 89 /r register-to-register mov
- x86_64: mulImm(R0, R1, 42) is imul r64, r/m64, imm32
- x86_64: mulReg(R0, R0, R1) is REX.W + 0F AF /r (two-operand)
- x86_64: mulhi borrows no allocatable register
- x86_64: mulhi handles every aliasing of its operands
- x86_64: prologue(0) is push rbp; mov rbp,rsp; push nonvols; sub rsp,N; (Win64: load kArg4)
- x86_64: ret is a single 0xC3 byte
- x86_64: retValue moves the value into rax, the return register
- x86_64: spillStore(R0, 0) writes to slot 0 through rbp with disp32
- x86_64: store8 with a byte-half register (val is R4/R12/etc.) always emits REX
- x86_64: store8(R0, R1, R2) is mov [R0+R1], R2_low8 with a SIB byte
- x86_64: the compare is 32 bits wide, the width a MoonLive value actually has
- x86_64: the epilogue pops exactly what the prologue pushed, in reverse
- x86_64: two sequential call-bearing loops stay under the density bound
- Xtensa addImm never encodes an add of zero as the narrow form
- Xtensa branchGeS inverts to blt where branchGeU inverts to bltu
- Xtensa load32 emits the two-byte l32i.n with a scaled offset
- Xtensa load32/store32 reach the WHOLE arena, not just the first 60 bytes
- Xtensa mulhi emits mulsh, the signed high half
- Xtensa shlImm encodes 32-n where sarImm encodes n
- Xtensa shrImm reaches every shift from 16 to 31 through extui
- Xtensa slotAddr reaches every frame slot, past the byte addi carries
- Xtensa the indexed 32-bit forms go through the a12 address scratch
- Xtensa: a fixed multiply emits mulsh beside mull
- Xtensa: every shipped script compiles at the device's own code budget
- the Xtensa vreg map names only registers the windowed ABI leaves free
- MoonLive recompiling swaps the program live (fill <-> setRGB)
- MoonLive.compile(source) on a bad script leaves the engine !ok with an error
- a bool initializer takes no sign
- a bool local truncates the same way, so any non-zero reads true
- a bool member is initialized and compared with true and false
- a bool member takes 0 or 1 and refuses anything else
- a brightness above full saturates to full instead of wrapping to a dark band
- a brightness that went below zero renders black rather than full
- a byte array packs one byte per element where an int array takes four
- a byte local WRAPS at 255, exactly as a byte member does
- a byte local keeps its width across a block boundary
- a byte member and an int member occupy the same sized slot
- a byte member assigned past its range keeps only its own byte
- a byte member outside 0..255 is refused at the declaration
- a byte parameter wraps at 255, the way a byte local does
- a chain of else if arms picks exactly one
- a class reports every function it defined, and where each one starts
- a comparison against a subtraction that went negative takes the negative branch
- a compile error reports the exact offset the editor turns into a line and column
- a control is declared by calling addControl, and a plain member is not
- a control refuses a member the UI has no widget for
- a control's range can be computed, not just written as a literal
- a conversion refuses a value already of its target type
- a descending for is refused with the workaround named
- a fixed array is refused with a diagnostic
- a fixed divide by zero saturates toward the numerator's sign
- a fixed divide is exact for values far past 128
- a fixed local reports its scaling where it is read
- a fixed local starts at a whole number, which the literal adopts
- a fixed member holds a fractional value written as a decimal
- a fixed member outside its range is refused at the declaration
- a fixed multiply is correct when its destination aliases a source
- a fixed multiply keeps the sign of a negative operand
- a fixed value passed to a built-in names the conversion
- a for counts up to its bound with < and through it with <=
- a for loop declares its counter, as every other variable in the language does
- a for loop's condition and step must name the loop variable
- a function declaring more parameters than the block holds is refused
- a function name too long to store is refused, not silently truncated
- a function receives the value its caller passed
- a function the host has no name for is still reported
- a function the script calls can light pixels and read the script's controls
- a function without a declared return type is refused
- a helper's value can drive a loop and a member
- a literal outside the fixed range does not adopt
- a local declared inside an if body does not leak past it
- a local is initialized from a call, which is what a sensor read looks like
- a local literal is range-checked against its declared type
- a local may be fixed, which is what a per-pixel coordinate needs
- a local may not shadow a built-in function
- a local may not shadow a member or a system variable
- a local of every value type is accepted; only string is refused
- a local variable holds a value for the rest of the tick
- a local variable is assignable after it is declared
- a local variable must be initialized where it is declared
- a long script compiles or refuses, but never spins
- a loop header refuses a fixed value in any of its three clauses
- a loop over a count still runs every step after comparisons became signed
- a member declaration and a typed function are told apart
- a member may not be named after a conversion or a boolean literal
- a member survives a spill across the 32-bit slot access
- a multi-call statement reuses dead vregs and stays within the register budget
- a nested call does not clobber the arguments of the call it sits inside
- a nested loop cannot reuse the enclosing loop's variable
- a nonzero seed selects the Julia set rather than the Mandelbrot set
- a number too large for an int is refused rather than wrapped
- a parameter name is checked the way a local is
- a recursive function keeps its own arguments through the calls it makes
- a return fires only when its condition holds
- a return must match the type its function declared
- a returned value can be computed
- a returned value survives the calls that follow it in the same expression
- a returning function still works as a statement, with its value dropped
- a script cannot declare a name the engine already defines
- a script dense in struct copies compiles, its IR growing past the reservation
- a script function's return value can be used in an expression
- a script returns a string the host can read
- a script that recurses without end keeps rendering instead of resetting
- a string array is refused with a diagnostic rather than mis-read
- a struct or a condition used where it does not fit is refused with what to write instead
- a system variable cannot be registered outside the arena's 32-bit cells
- a void function cannot be used as a value
- a whole number initializing a fixed member means its whole value
- a whole-number member refuses a fixed initializer
- an argument is a copy: writing the parameter leaves the caller's variable alone
- an array element carries its array's type, not its index's
- an array element refuses a value of the wrong type
- an array index is refused as a fixed value
- an array is indexed correctly at both element widths, by a computed index
- an empty function does not consume the recursion budget
- an int array element round-trips a value no byte could hold
- an int local takes a whole number, so a fractional initializer is refused
- an int member assigned a negative in tick reads back negative
- an int member holds a large positive
- an int member holds a value far outside any 16-bit range
- an int member written negative reads back negative
- an integer literal adopts fixed at a meet point, a variable does not
- an out-of-range index still clamps to the last element after the scaling change
- an unexpected character inside a call's arguments is refused, not looped on
- arguments reach a function two calls deep
- blocks reuse frame slots, so the frame is not spent by a script's total locals
- calling a function with the wrong number of arguments is refused
- compileSource rejects malformed programs with a diagnostic, never crashes
- compileSource: a literal may be any value an int member can hold
- compileSource: a missing code buffer is refused before any lowering runs
- compileSource: a register refusal returns a literal, and spillDetail carries the numbers
- compileSource: fill(r,g,b) fills every light
- compileSource: malformed control declarations fail with a diagnostic, never crash
- compileSource: out-of-range index is bounds-rejected at runtime
- compileSource: random16 works in any argument slot
- compileSource: setRGB(index, r,g,b) writes one pixel
- core compiler has no built-in functions of its own (empty table → all unknown)
- dividing by zero saturates toward the numerator's sign rather than faulting
- dividing the most negative value by minus one saturates rather than faulting
- dividing two fixed values keeps the fraction
- division binds tighter than addition and left to right with multiplication
- escape near the set boundary counts more steps than far outside
- escape reports the inside of the Mandelbrot set as zero and the outside as a count
- escape treats an absurdly distant coordinate as escaped rather than overflowing
- every comparison operator orders a negative below a positive
- every shipped script compiles
- every shipped script still compiles after a builtin is added
- mixing a whole number and a fixed value is refused with the conversion named
- multiplying two fixed values rescales the product
- parentheses group an expression ahead of division
- passing an argument costs about what writing a member costs
- return inside a loop leaves the whole function
- return leaves tick() early, and the statements after it do not run
- the builtin table has room for every name the light domain registers
- the fixed ops survive being spilled
- the host reads a value a script function returned
- the most negative int and fixed values can be written in an expression
- the remainder of two fixed values is itself fixed
- the two scalings do not mix in a local, in either direction
- toFixed refuses a literal outside the fixed range
- true and false initialize a bool and nothing else
- two functions each get the whole frame rather than sharing one
- void, int and string are the return types; a member type is not one
- == is one token, not two assignments
- MoonLive animated fill derives color from the per-frame t
- MoonLive compiles and fills a buffer with the chosen color
- MoonLive controls: arena address is STABLE across a recompile and the slot value survives
- MoonLive controls: declaredControls + controlSlot seeded from the default
- MoonLive controls: free() releases the arena (no stale slot after release)
- MoonLive controls: widening or growing a member reseeds its whole extent
- MoonLive emitFill produces a non-empty routine
- MoonLive emitFill rejects a too-small buffer (degrades, no overrun)
- MoonLive emitFill/emitAnimatedFill reject a null output buffer (no crash)
- MoonLive line() clamps out-of-range endpoints to the canvas edge
- MoonLive line() draws a horizontal segment through the installed canvas
- MoonLive line() without a canvas draws nothing
- MoonLive recompile swaps the color; free returns to !ok
- MoonLive run is a no-op on sub-RGB buffers (cpl 1 and 2)
- MoonLive run on zero lights writes nothing (robust to empty)
- a broken script drops its controls instead of blanking their names
- a byte member cannot be initialized above 255
- a circle drawn through uv stays circular on a wide panel
- a class declaring more member data than the arena holds is refused
- a class holds kMaxCtrls members, and one more is refused by name
- a class that defines no moment a binding owns is still a valid script
- a compiled script reports its size, and its tightest budget only when it is filling up
- a control declared with min above max is refused, not published as unsettable
- a control range past its type is refused, not truncated
- a control takes any scalar member, but not a range its type cannot hold
- a coordinate far outside the grid saturates at that edge, not the opposite one
- a int array holds per-element values above 255
- a longer blend never reads as less merged than a short one
- a loop counter survives a call in the body
- a loop variable can be assigned in the loop body
- a member decides which branch a tick takes
- a member written by one function is read by another
- a member written by one tick is read by the next
- a modifier writes its coordinate without naming a destination slot
- a scalar member cannot be indexed
- a script asks its layer to fade, and the amount arrives
- a script paints from the active palette
- a script runs the entry point the host asked for, not whichever came first
- a script that draws with nothing installed does not consume a per-thread slot
- a shape's outside stays dark once the distance passes its edge
- a sink slot stays owned while any of its sinks is still installed
- a system variable cannot be assigned
- a whole array cannot be assigned in one statement
- an array element written in one loop is read in the next
- an array index may be an expression
- an array larger than the arena is refused at compile time
- an if condition may be an expression on both sides
- an if inside a for runs the body every iteration
- an int member accepts an initializer a byte could not hold
- an int member crosses the 255 boundary without wrapping
- an int member holds a value above 255
- an int member is published as a control spanning its full range
- an int member starts at the value it was initialized to
- an out-of-range array index is clamped, not written past the end
- an out-of-range array read is clamped and leaves system variables intact
- an over-large fade amount clamps to full rather than wrapping
- array contents survive from one tick to the next
- asking for an entry point a script does not define runs nothing
- assigning to an undeclared name is refused
- blending two shapes with smin produces one surface, not two
- calling a function no one declared is a compile error
- elapsed time survives a call that happens before it is read
- every scalar member takes a whole slot whatever its type
- fading from a script with no layer does nothing
- if/else takes exactly one branch
- if: every comparison is exact at its boundary
- member offsets advance by a whole slot in declaration order
- one class can serve several moments, and each is called on its own
- platform allocExec returns usable executable memory, freeExec releases it
- polar builtins answer angle and distance from a center
- setRGB still names the light it writes
- smoothstep is a soft ramp rather than a hard threshold
- the builtin table has room and reports an overflow
- the old four-argument setXYZ is refused, not reinterpreted
- uv places the grid center at the origin, with the left half negative
- MoonLive compileSource degrades on a too-small code buffer
- MoonLive compiled fill is BEHAVIORALLY identical to the hand-encoded emitFill (golden)
- MoonLive compiled fill is robust: zero lights writes nothing
- MoonLive compiled setRGB writes one pixel; out-of-range is bounds-rejected
- MoonLive control survives a host call (kArg4 live across random16)
- MoonLive control: a declared control reads the arena live (no recompile on value change)
- a declared control still reads live at a squeezed budget
- a nested loop at a squeezed budget places every light exactly once
- a program that already fits is untouched by the register allocator
- a script renders identical pixels at a squeezed register budget as at the full one
- a spilled value survives a host call and is still correct afterwards
- a system variable read in a loop survives a host call in that loop
- a value live across a loop keeps its storage for the whole loop
- a value returned by a script function survives a squeezed register budget
- an impossible register budget refuses the compile instead of emitting wrong code
- a script dims, adds to and mixes a light that is already lit
- a script reads back the color of a light
- a scripted circle draws its rim and leaves the middle dark
- a scripted circle outside the grid lights nothing
- a scripted circle stroke wider than a length still draws a rim
- a scripted circle with a negative stroke width draws a thin rim, not a filled grid
- a scripted circle with a zero stroke width still draws
- a scripted line reaches both of its endpoints
- a thicker scripted circle covers more lights than a thin one
- a dimensions() declared void is ignored rather than read
- a dimensions() outside 1..3 falls back
- a freshly constructed MoonLive effect reports static tags
- a head's aim is not scaled by brightness
- a script aims each head with setPan and setTilt
- a script declares its dimensions and its tags
- a script may still declare a member called level
- a script that declares D1 is extruded across the width
- a script that declares neither keeps the scripted defaults
- an audio script runs on a device with no audio, and paints nothing
- an out-of-range audio band reads zero
- mh-aim.mle points every head where its sliders say
- mh-sweep.mle moves the rig and its formations differ
- setPan on a light with no motion channel writes nothing
- setPan past the last light is ignored
- a factory script resolves when the user has no copy of it
- a fork knows whether the shipped script has changed under it
- a script in neither directory is not found
- a user copy is reported as a shadow only when a shipped copy is under it
- a user's copy shadows the factory script of the same name
- deleting a user's copy restores the factory script
- forking a shipped script records what it was forked from, and reverting forgets it
- no script compiles in safe mode
- the compiler and the change-detector read the same file
- a comment changes nothing about what a script does
- a comment is whitespace, wherever it appears
- a script reads elapsed time, so it can animate
- a service script compiles against the service vocabulary
- every catalog entry's subfolder is where its script lives in moonlive/
- every compile error fits the status line whole, its position included
- every script example in the docs compiles
- every script in moonlive/ compiles
- every script reads the same system-variable vocabulary
- mod wraps a sweep, so an animation repeats instead of running off the end
- noise is smooth across neighboring points, and varies across the field
- sequential loops reuse the same register, so a script is not billed per loop
- the shipped catalog names every script in moonlive/
- the three roles are handed the same table
- every shipped script compiles to the IR it had before the compiler changed
- a Coord3D holds negative and large whole numbers
- a light's color and a palette color read back as one RGB
- a same-type call nested in another keeps the outer call's literal in the right type
- a script function takes and answers a struct
- a variable declared before a loop, changed in it, reads right after it
- abs, min, max and clamp work on whole and fixed values
- an RGB member keeps its value from one frame to the next
- an RGB value is made, changed and drawn as one color
- conditions join with && and ||, negate with !, and stop at the first answer
MoonModule¶
- a disabled module holds no dynamic memory
- a disabled module holds nothing beyond its objects
- a removed module leaves nothing behind
- every module honors the enabled flag
- every module object stays within the inline budget
- every registered module type is placed or listed as not placeable
- Bool control binding
- Control binding via ControlList
- ControlList clear and rebuild
- Module enabled property
- MoonModule appearsInUi defaults true, overridable false
- MoonModule dirty flag
- MoonModule lifecycle
- MoonModule name
- MoonModule parent
- MoonModule readBool/readUint8 return the value, or the default when absent
- MoonModule typeName is independent of name
- Progress control binding
- ReadOnly control binding
- Select control binding
- a progress control declares its unit to the UI, bytes unless told otherwise
- schemaSignature: recurses into children (a child schema change is caught)
- schemaSignature: value change is invisible, schema change is not
- affectsPrepare: Layout and Modifier opt in
- affectsPrepare: effects and Drivers do NOT rebuild
- leaf module tick default is a safe no-op (childCount_ == 0)
- per-child timing accumulates on the child, not the parent
- switching a module parks its workers first, and setting the same flag parks nothing
- tick default propagates to enabled children
- tick default skips disabled children
- tick default still ticks children that opt out of the enabled gate
- tick20ms and tick1s follow the same gating + dispatch rules
- two crashes in a row hold a module that asks for it and keep its saved flag, one crash does not
- moveChildTo: backward (toward start)
- moveChildTo: forward (toward end)
- moveChildTo: middle-to-middle shifts correctly
- moveChildTo: non-child rejected
- moveChildTo: noop when already at target
- moveChildTo: one position swap (up arrow / down arrow)
- moveChildTo: out-of-range index rejected
- replace lifecycle: fresh module is built, set up, allocated in order
- replaceChildAt: null replacement returns nullptr
- replaceChildAt: old child detached, fresh child parented
- replaceChildAt: out-of-range index returns nullptr, tree untouched
- replaceChildAt: swaps at the same position, siblings intact
MoonStatsReport¶
- a catalog name is matched against its own role's catalog
- a module added under a wired parent is still reported
- a module reports its type, not the instance name a user sees
- a refresh names no previous version, so it cannot read as an upgrade
- a refresh that did not send leaves the automatic report still due
- a report carries the frame rate it is given, zero included
- a script the user wrote is counted but never named
- a scripted module reports the shipped script it runs
- a stats retraction leaves a foreign status alone
- a stats verdict is retracted before the next one is formed
- a user-triggered refresh is its own event, carrying the same payload
- an upgrade is distinguished from a fresh install by the previous version
- no installation id appears until one is supplied
- the preview driver is boot wiring, so it is never reported
- the report carries memory and light count as numbers
- the report names modules by ROLE, not by how they were wired
- the usage report cannot carry identifying or secret values
- the usage report carries the hardware facts it exists to collect
MqttPacket¶
- MqttPacket: CONNECT drops a password when there is no username
- MqttPacket: CONNECT is byte-exact (clean session, no auth)
- MqttPacket: CONNECT with a retained Last Will is byte-exact
- MqttPacket: CONNECT with username + password sets the flags + payload
- MqttPacket: PINGREQ and DISCONNECT are 2-byte packets
- MqttPacket: PUBLISH is byte-exact (QoS0)
- MqttPacket: PUBLISH retain flag sets the fixed-header bit
- MqttPacket: SUBSCRIBE is byte-exact (one filter, QoS0)
- MqttPacket: fixed-header write refuses a buffer too small for its varint
- MqttPacket: inbound parser exposes a CONNACK's return code in its body
- MqttPacket: inbound parser handles a 2-byte remaining-length
- MqttPacket: inbound parser reassembles a PUBLISH (build→parse round-trip)
- MqttPacket: inbound parser reassembles across read() boundaries
- MqttPacket: remaining-length varint encodes/decodes at the boundaries
RtpMidi¶
- a clock sync carries its count and three 64-bit timestamps
- a data packet holds its messages with a zero delta time between them, and reads back
- a data packet that lies about its length or has no RTP version is read no further than it goes
- a received MIDI list honors delta times and running status, and skips a journal and SysEx pieces
- an RTP-MIDI invitation refused by one host is not reported as refused by the next
- an RTP-MIDI inviter whose peer went silent says goodbye, and the peer lets go
- an RTP-MIDI session connects over both ports, carries messages both ways, and invites again after a goodbye
- an RTP-MIDI session half open with one peer refuses a second
- an RTP-MIDI session takes one peer at a time
- an invitation has AppleMIDI's exact bytes, and reads back with its name
Scheduler¶
- Scheduler::firstByName returns the first match in tree-walk order
- Scheduler::freeName keeps counting past 'Foo-2'
- Scheduler::freeName refuses a suffix that does not fit a name
- Scheduler::freeName refuses an empty name and one too long for the buffer
- Scheduler::freeName returns an unused name as it is
- Scheduler::freeName suffixes a name that is taken
- $patch replace keeps exactly the listed children, in the document's order
- a control a script declares waits for the rebuild, and lands after it
- a different type replaces the module in place, under the same name
- a document leaves a secret out, and applying it leaves the secret as it is
- a document that fails leaves no write waiting for the rebuild
- a document that names an unknown type or container changes nothing, wherever it names it
- a mistyped control on a script the document leaves as it is is named at once
- a module can move to another branch under the same name
- a module written as a document applies back to the same state, rooted at its container
- a name the same document frees, by removal or a re-typed parent, can be created elsewhere
- a re-typed module frees the names under it for elsewhere in the same document
- a rebuild run at once satisfies the one the document requested
- a reorder alone is a change, saved like any other
- a root $ key is the file's own, so a preset file applies as it is
- a state document applied twice leaves the tree as the first application did
- a state document creates a module with its controls in one go
- a state document keeps the tree's rules: roles, unique names, name length
- a state document names the first failure and where it is
- a state document sets a control of an existing module and leaves the rest alone
- a state document that breaks a tree rule anywhere changes nothing
- a state document that does not parse changes nothing
- a stored file sets only the controls the device saves, and a request sets any
- key order is creation order
- null removes a module, and removing an absent one is no error
- replacing a module's children keeps the ones wired in code
- replacing a module's children keeps the unlisted ones where they are
- the answer to a document names the failure, escaped, and what waits for the rebuild
ScratchBuffer¶
- ScratchBuffer destructor frees and deregisters
- ScratchBuffer grow then shrink tracks dynamicBytes by delta
- ScratchBuffer is freed by the owner's release()
- ScratchBuffer multiple buffers on one module sum and release together
- ScratchBuffer resize allocates and reports bytes to its owner
- ScratchBuffer resize(0) frees and clears the owner's byte total
- ScratchBuffer sizes by element type
- ScratchBuffer zero-fills on allocation
Sort¶
- insertionSort handles empty and single-element arrays
- insertionSort is stable — equal keys keep input order
- insertionSort orders C-strings (the device-name use case)
- insertionSort orders ints ascending
- insertionSort with a custom (descending) comparator
SpscRing¶
- SpscRing delivers strict FIFO order across index wrap
- SpscRing hands an unbroken in-order sequence across two threads
- SpscRing overflow drops newest, reports the accepted count, and keeps queued data intact
- SpscRing treats null buffers as empty operations
ThreadSlot¶
- a thread finds the slot it claimed, and a read alone claims none
- two threads hold two slots, and a third finds the table full
Uncategorized¶
- scenario_Network_hardware_reconfigures_live
- scenario_Effects_teardown_under_a_running_pipeline
- scenario_Modifiers_reshape_the_mapping
- a freed null is not counted
- an allocation is visible in the reported total, and freeing it gives the bytes back
- the live-block count separates one big buffer from many small ones
- the peak survives the free that follows it
- a bright screen edge reaches the edge cell
- a cell samples at a stride rather than reading every pixel it covers
- a half-black half-white frame averages above the arithmetic mean
- a uniform frame averages to itself
- the cell ranges tile the whole source with no gap and no overlap
- a 32-character project name that fills its field stays terminated
- a name that fills its descriptor field unterminated is still compared within the field
- a recovery image is recognized under both names and never passes as an app
- a stranger's image is refused, however close its name
- a truncated stream reports what it can rather than reading past its buffer
- an HTML error page is refused before anything is erased
- an app image is accepted under the name it ships with and the name v6.0.0 shipped with
- an image for the wrong chip is refused
- an image with no description is refused
- every built image identifies as what it is
- a table carries either two OTA slots or one slot plus a recovery app, never a mix
- app partitions are 64 KB aligned, as the MMU requires
- every MoonBase factory slot can be erased and holds a MoonBase image
- every partition table describes a layout that fits its flash without overlaps
- the CSV number parser rejects signed and uint32-overflowing values
- a device with nothing above the picture leaves the request alone
- a frame interval becomes the rate it names
- a frame of any shape fills the whole picture that was asked for
- a frame the driver already stripped is left alone
- a header's length comes from its own flags
- a picture larger than the frame stays inside the source
- a smaller advertised mode is preferred over a larger one
- every header length is stripped, not only the longest
- payload headers are stripped at the stride the first one sets
- the mode asked for is the smallest that covers the picture
- the rate asked for wins over a faster one the device offers
- the row stride comes from the size the decoder wrote
- two modes of equal area do not mix their rates
- Layer: buffer persists across frames (no per-frame clear)
- Layer: collected fade resets after it is consumed
- Layer: multiple fade requests combine with MIN (gentlest wins, longest trail)
- Layer: prepare clears the buffer (a rebuild wipes stale pixels)
- a scripted effect fades its layer the way a compiled one does
- a trail decays with elapsed time, not with the frame count
- a trail survives many frames when the script fades only as it moves
UsbMidi¶
- a USB-MIDI packet carries one channel message, its CIN the status nibble
- a malformed or oversized message is not cut into packets
- a message is cut into USB-MIDI packets, a SysEx one ending with the bytes that are left
WledAudioSyncPacket¶
- FFT_Magnitude carries WLED units and round-trips back to ours
- a WLED magnitude above our range clamps instead of wrapping
- bands are clamped to 254, the way WLED sends them
- build -> parse round-trips every AudioFrame field
- build produces a 44-byte v2 packet with the exact WLED layout
- golden vector — the exact bytes on the wire (the compatibility contract)
- parse clamps NaN / out-of-range floats instead of undefined casts
- parse rejects wrong length, wrong header, v1, and null
WledPacket¶
- WledPacket marker is set only when stamped, and stays WLED-valid
- WledPacket::build produces a valid WLED header (token/id/size)
- WledPacket::isValid rejects short / wrong-magic / null input
- WledPacket::readName round-trips the device name
- WledPacket::readName truncates a long name to the buffer, never overruns
crc¶
- crc: CCITT-FALSE check vector
- crc: empty input is the init value
- crc: same bytes hash equal, a one-bit change differs
math16¶
- BeatPhase advances proportionally to elapsed time and rate
- BeatPhase holds still at rate zero and resets to zero
- BeatPhase keeps animating when frames are under a millisecond
- BeatPhase survives the millis wrap
- a beat completes one full cycle per beat, at any tempo
- a half-life decay loses exactly half its value over one half-life
- a triangle wave rises to full scale and falls back
- an in-out easing moves slowly at the ends and quickly in the middle
- atan16 handles the extremes of its input range
- atan16 is monotone around a full sweep
- atan16 puts the diagonals halfway between the axes
- atan16 spans the circle in the right quadrants
- atan16 stays within a fraction of a degree of the true angle
- dist16 does not saturate past the 8-bit ceiling
- dist16 handles the full int32 range without wrapping
- dist16 is a true radius, where dist8 approximates an octagon
- dist16 is exact for distances beyond 16 bits
- ease out starts fast and settles
- easing curves are monotone
- easing curves start at zero and finish at full
- halving twice as often dims at the same rate, so framerate cannot change a trail
- hashInt gives neighboring pixels unrelated values
- hashInt is a pure function of its inputs
- hashInt separates its axes, so x and y are not interchangeable
- hashInt spreads across its range
- isqrt64 finds a root across the whole 64-bit range
- isqrt64 roots values a 32-bit root cannot hold
- kaleido folds correctly for segment counts that do not divide the circle
- kaleido is the identity below two segments
- map32 can reach the last column of a grid
- map32 maps a range and clamps outside it
- map32 survives full-width 32-bit ranges
- peakHold holds a peak for many frames after a transient
- peakHold rises instantly and falls slowly
- sin16 and map32 evaluate at compile time
- sin16 is smooth between LUT entries, where sin8 would step
- sin16 stays within 0.5% of a true sine
- sin16 traces a full sine over one turn
- smoothFollow falls toward a lower target too
- smoothFollow makes progress at every nonzero rate, in both directions
- smoothFollow moves toward its target without overshooting
- smoothFollow never overshoots its target
- smoothFollow rate sets how quickly it converges
- two decay steps reach the same place as one step of twice the time
math8¶
- math8: Random8 is deterministic per seed and below(n) is bounded
- math8: atan2_8 and dist8 cover the basics
- math8: beat8 ramps 0..255 once per beat
- math8: beatsin8 oscillates within [low,high]
- math8: map8 reaches both range ends, including one-step spans
- math8: nscale8 scales a byte by n/256
- math8: qadd8 / qsub8 saturate, never wrap
- math8: sin8 / cos8 trace a sine over the 256-step circle
- math8: triwave8 is a symmetric triangle
noise¶
- 3D fbm varies along z, so z can drive time
- each octave of fbm adds finer detail
- fbm is smooth: neighbors resemble each other more than distant points
- fbm normalises its octave sum rather than accumulating
- fbm with no octaves is a flat field
- kaleido folds the circle into the requested number of wedges
- kaleido mirrors alternate wedges so the seams join
- kaleido repeats every wedge
- kaleido steps by one across every seam
- kaleido with fewer than two segments changes nothing
- one octave of fbm is plain noise
- turbulence folds the field at its midpoint
- warp displaces the field it samples
- warp stays a smooth field rather than becoming noise
- warp with zero strength is the field itself
- 16-bit fbm keeps its range too
- 16-bit interpolation stays exact across the full range
- 16-bit noise fills the range instead of stepping through 256 levels
- 16-bit noise is smooth where the 8-bit form would step
- a curl field has no sources or sinks, so what it carries cannot pile up
- every field kernel's 2D form is its 3D form with z at zero
- fbm keeps its full range however many octaves are summed
- noise: inoise8 is deterministic and the lower-D calls agree at zero on the extra axes
- noise: inoise8 spans a wide range across a field
- noise: inoise8 varies smoothly inside a cell
- the z axis actually changes the field, rather than being carried and ignored
oscillators¶
- a range given backwards runs the shape backwards
- a rate of zero holds the picture still
- a square wave is only ever fully on or fully off
- an oscillator stays inside the range the effect asked for
- an out-of-range oscillator index reads as zero rather than crashing the device
- changing the rate continues from where the phase stands, without jumping
- reset restarts the motion without losing the configuration
- the first frame establishes the time base instead of jumping the phase
- the four waveforms have the shapes their names promise
- two oscillators at the same rate hold their phase relationship indefinitely
parse¶
- a JSON integer value is read up to the character that ends it
- a quoted number and a 0x hex number read as the value they write
- a value too large to represent reads as the fallback instead of wrapping
- parseInt reads a key's integer value, and absent keys read as zero
- text that does not start with a number reads as the fallback, not as zero-by-accident
platform¶
- platform::TcpConnection::connectStart rejects a bad host; poll on no-fd is Failed
- platform::TcpConnection::connectStart/connectPoll is non-blocking
- platform::setTestNowMs freezes and restores millis()
- TryLock: LockGuard releases on scope exit, and no-ops when busy
- TryLock: a second THREAD is refused while held, and never blocks
- TryLock: acquiring excludes a second acquirer, release re-opens it
- worker seam: each notify wakes the task exactly once
- worker seam: stopPinnedTask joins: the fn has returned before it returns
- worker seam: waitNotify times out and returns false when not notified
sha256¶
- a multi-block message hashes correctly
- messages around the block boundary hash correctly
- the digest matches the published SHA-256 test vectors
- the hex helper truncates the digest to the requested length
Core · Services¶
AnalogService¶
- a pedal's usable travel is what maps, not the full sweep
- a resting input stops writing, so jitter does not flood the control
- a reversed min/max pair means inverted, rather than being an error
- a saved analog row naming a control off the surface is left unassigned, and the status says so
- an analog input drives a control across its travel
- an analog row pointed at a pad refuses, rather than firing it every tick
- an analog row refuses a field it would silently ignore
- an analog row scales into whatever range its surface control holds
- an inverted input reads the other way round
- an out-of-range pin is refused rather than narrowed into a different pin
- an unconfigured or unassigned row does nothing, quietly
- the first reading is taken whole, so a pedal does not sweep up from zero on boot
AudioService¶
- scenario_Services_audio_drives_the_effects
- a picked audio device is remembered by name, not by its position in the list
- a silent capture device raises no I2S wire status
- audio capture enumeration offers 'default' first, from platform-owned storage
- audio capture lifecycle is clean on any host: init, read, deinit, repeat
- audio capture refuses an out-of-range device index
- a microphone asks for no codec until one is named
- changing the codec or its address re-initializes the microphone
- the codec is reported on the bus only while the local microphone uses one
- AudioService Local (not sending): no socket, reports off
- AudioService Local+send on a capture host: capture and broadcast coexist
- AudioService Local+send unicast: sends to its hosts, and asks for them when the list is empty
- AudioService Local+send → Simulate: send stops, no socket
- AudioService Local+send: broadcasts are throttled to ~kSyncSendIntervalMs
- AudioService Local+send: lazy-opens once and reports sending
- AudioService Receive: a failed bind backs off instead of retrying every tick
- AudioService Receive: a failed group join is retried once silent, and the warning clears
- AudioService Receive: a failed group join still listens for unicast, and says so
- AudioService Receive: a localhost WLED packet drives frame_, then holds it and reports listening
- AudioService Receive: a silent receiver joins the group again every rejoin interval, a receiving one does not
- AudioService: leaving local clears an outstanding mic diagnosis, in every mic-less mode
- AudioService: switching out of Local mode clears the mic status
- desktop audioFft maps silence to all-zero bins
- desktop audioFft matches the DFT reference on random input
- desktop audioFft puts a pure tone's energy in the exact bin
- AudioBands: a low tone lands in a low band, a high tone in a high band
- AudioBands: a single tone concentrates energy, not smears it everywhere
- AudioBands: noiseFloor gates a low idle spectrum to zero, gain scales it back
- AudioBands: silence yields all-zero bands and no peak
- AudioBands: the reported peak frequency tracks the played tone
- AudioBands: zero / degenerate input never crashes
- a band rises to a transient at once and falls back slowly, the PPM ballistic
- a quietly played band still fills the display, so soft passages keep their dynamics
- a restarted source reports no onset from the block that preceded it
- a room below the display window shows nothing on the spectrum
- a room below the floor reads silent, however hard the learner is asked to level
- a small FFT still yields sixteen usable bands, because a fixture may run one
- a spectrally tilted rig reads flat at a high ratio, once the learner has settled
- an onset fires once per hit, not once per block the hit lasts, and not on a swell
- at ratio 1:1 the conditioner changes nothing, so the music's own balance is untouched
- band edges rise with frequency, so a band is a range rather than a reshuffle
- each band learns its own floor, so a hum in one band does not raise the others
- equal rise and fall reduce to a symmetric follower, so the ballistic is a superset
- every band gets its own ballistic, so a hit in the bass does not smooth the treble
- every band owns at least one FFT bin, so no band is dark whatever the music
- in automatic mode a quiet room and a loud one both fill the level meter
- learning off freezes the tables, the deterministic mode a show wants
- maxGain caps the lift, so a silent band is never amplified into its own noise
- spectral flux reads a rise, ignores a fall, and is zero on a steady spectrum
- the ballistic reaches its target exactly, so a held level does not sit one short
- the low bands keep the resolution the FFT can actually deliver
- the peak releases over seconds, not blocks, so one loud bar does not re-level the display
- AudioLevel: DC bias does not change the level of a sine
- AudioLevel: a high noiseFloor (dB floor) gates a modest signal to zero
- AudioLevel: a loud sine reads a higher level than a quiet one
- AudioLevel: empty / null input is silence, never a crash
- AudioLevel: higher gain (narrower dB window) reads a higher level
- AudioLevel: isqrt64 matches floor(sqrt) on a spread of values
- AudioLevel: pure DC reads zero (DC offset stripped)
- AudioLevel: silence reads zero
- AudioService + audio effects are registered and createable (boot-loop guard)
- AudioService::latestFrame is never null (silent frame with no active mic)
- DcBlocker: a constant DC offset is filtered out
- DcBlocker: an audio tone passes through (DC removed, AC kept)
- DcBlocker: reset clears state, null-safe
- AudioService: a DISABLED module does not win the mic election at boot
- AudioService: a fresh, unconfigured module is idle (pins default unset)
- AudioService: release clears the active mic (latestFrame falls back to silence)
- AudioService: setup/release is repeatable with no residual state
- AudioService: two mics — first wins, survivor re-elects, any order stays coherent
ButtonService¶
- a bounce shorter than the debounce window is not a press
- a button bound to an empty pad reports it rather than firing something else
- a button fires the pad in that grid position, so a preset has a physical key
- a delta row nudges its target, clamped by the control
- a momentary row writes while held and clears on release, which is what a pedal needs
- a press toggles the control its row targets, once per press
- a row with no pin, or no target, is a valid state and does nothing
- a saved row naming a control off the surface is left unassigned, and the status says so
- a target round-trips through the type and number the editor shows
- an active-high row reads the opposite level
- editing the type or the number re-composes the target, so they cannot disagree
- rows are added and deleted at runtime
- two buttons on two pins act independently
GamepadService¶
- GamepadService drives the surface through its default rows
- GamepadService ignores a malformed state
- GamepadService ignores a stick inside its deadband
- GamepadService learns the next input
- GamepadService takes a first state's sticks as rest and its buttons as presses
InfraredService¶
- a code that is not a number is refused rather than binding something else
- a delta row nudges its target and stops at the control's own bounds
- a learned code toggles the control its row targets
- a row with no target does nothing at all
- a saved row off the surface is still reported once the receiver is ready
- a set row is refused on a remote, which has no release to clear it
- a target off the surface is refused, and the row keeps driving nothing
- an explicit false disarms a row, so a learn can be canceled
- an unlearned code is reported and changes nothing
- arming a row disarms any other, so one code cannot bind twice
- learning binds the next code to the armed row, and only that row
- one remote key binds to one row, so a re-learned key moves rather than duplicates
- rows are added and deleted at runtime, which is what a fixed action table could not do
- the pin state decides what the service reports about itself
MoonLiveService¶
- a saved value of a script's own control survives the boot
- a script cannot reach past the control surface
- a script declares a dropdown with addSelect, its value the index of the name picked
- a scripted service declares its own controls, which a user can set
- a scripted service reads a pin and drives the control surface
- a scripted service reads an analog pin and scales it itself
- a second dropdown on the same member is refused without spending the room its names would take
- a service with no script, or a broken one, is a valid state and ticks harmlessly
OscService¶
- A float 0 to 1 and an int 0 to 255 mean the same control value
- A float 1.0 is ON for a switch, not the byte 255
- A leading string argument is skipped to reach the number
- A malformed datagram is refused, never read past its end
- A message with no arguments parses, carrying no value
- A string argument with an unpadded tail is rejected, not read past
- Addresses of every length parse, including the padding boundary
- An int argument parses and reads back as both forms
- The address contract: what a controller can send
- a switch reads any nonzero value as on, however small
- an encoded message round-trips through the parser at every padding boundary
- encoding refuses a buffer that cannot hold the whole message
- golden vector: a pad's state is an int on /mm/padstate
- golden vector: the exact bytes of an OSC message with a float
- OSC feedback asks for every surface value again every 30 seconds, and gets one per tick
- OSC feedback to a mistyped host list says why rather than sending to nobody
- OSC multicast feedback without a group says so, and the default group needs nothing filled in
PictureService¶
- a frame the decoder refuses publishes nothing, and recovery publishes again
- a new picture size publishes a new generation
- a picture written from another task is always read whole, with two pictures
- switching away from the capture stops its task
- the capture status counts the frames that landed and those lost
- the test pattern publishes the test card
- the usb capture source publishes the grabber's frames at the size the picture is set to
- the usb capture source without a grabber reports it
- the usb capture with one picture publishes, and the status says it runs on one
- with no Picture service the frame is empty
- without room for a second picture the service degrades to one, locked while written
SdCardService¶
- SdCardService is a Service
- a P4 opens on its one SD board preset, every other build on Custom
- a board preset writes its wiring and the mount follows; Custom keeps it to edit
- a card inserted after a failed mount mounts when the module is switched off and on, never on a timer
- a card that answers but does not read says so, and format does not offer to fix it
- a card without a FAT filesystem says so, and format makes one and mounts it
- a format runs in the background: the card is busy and the module says formatting until it finishes
- a mount clears the temporary files a failed write left in the staging folder
- a pin edit remounts the card live, the same contract I2cBusModule's sda/scl have
- a pull mid-transfer waits for the busy guard, same as a manual unmount would
- a pulled card is noticed within a second: tick1s auto-unmounts with no pin touch
- an SPI preset sets the bus, warns about long writes once mounted, and the mount needs its CS
- an unrelated prepare does not remount when the pins have not changed
- each chip offers only the SD board presets it can wire
- every SD pin affects prepare, so an edit remounts the card; format does not
- format refuses on an empty slot: only a card without a filesystem is formatted unmounted
- format refuses while a download stream is open, and succeeds once it closes
- format refuses while unmounted, and wipes the card while mounted
- pins that match a board preset open on its name rather than on Custom
- prepare mounts once the required pins are set, and the gauge appears
- prepare refuses to mount with no pins set
- release unmounts; it is harmless when nothing was mounted
- the pins show under Custom, the default, and hide under a board preset that sets them
- tick1s is a no-op while unmounted, and refreshes the gauge once mounted
Services¶
- Services accepts service-role children; System accepts none
- Services is a thin grouping node — a service child attaches and ticks
Core · System¶
AccessPointModule¶
- scenario_AccessPoint_first_setup_from_its_own_network
- scenario_AccessPoint_protected_and_always_on
- a client is on the access point by its own address, whichever of the device's addresses it asked
- a join asked for from the access point during a retry still holds it open
- a join from the access point holds it open with links to the new address
- a network in range offers the device's .local link before the join
- a password is written empty while secrets are hidden, and as before once the scope ends
- a recovered network keeps the access point closed when first setup only turns into on failure
- a request through the access point for another host is redirected
- an open access point gives no warning, even once a network is known
- credentials arriving while the access point runs join beside it, then it closes
- four quick switch-ons open the access point for that boot without a password and keep the networks, three do not
- switching first setup only to on failure opens the access point without a restart
- the DNS responder answers an A query with the access point's address
- the DNS responder answers another type with no records
- the DNS responder drops a malformed or oversized packet
- the access point broadcasts MM- and four MAC digits, whatever the device is named
- the access point for the first setup only opens while nothing is configured
- the access point for the first setup only stays closed once a network is known
- the access point is open on a first setup and carries the first known network's password after
- the access point opens on failure and closes once a network joins
- the access point re-opens when the first known network's password changes
- the access point set to always opens once the first join settles, not during it
- the access point set to always stays up while connected
- the fallback waits to retry a known network while a phone is on the access point
- the handoff holds the access point two minutes even when the phone leaves
ControlModule¶
- scenario_Control_a_palette_preset_changes_only_the_palette
- scenario_Control_presets_capture_and_restore
- ControlModule aims a pad at one save only
- ControlModule an unassigned fader drives nothing
- ControlModule applies a palette-only preset over a running look and changes nothing else
- ControlModule bumps its revision on every preset-set change
- ControlModule deletes a preset by deleting its file
- ControlModule drops a pad's aim when the save is refused
- ControlModule empties its pads when the preset folder goes
- ControlModule exposes eight switches, ahead of the encoders and faders
- ControlModule exposes only look-only presets to external surfaces
- ControlModule fader 1 drives the global brightness
- ControlModule finds a preset's row by its name, and applying the row applies the preset
- ControlModule follows a preset written or removed from outside
- ControlModule keeps a preset's pad when a card save overwrites it
- ControlModule keeps a renamed preset active under its new name
- ControlModule keeps one active preset per captured role
- ControlModule keeps the pad order the user arranged
- ControlModule lists an older flat preset and refuses to apply it
- ControlModule lists one row per preset file
- ControlModule marks the applied preset as the active pad
- ControlModule moves a pad in a hand-spaced or pad-only preset file without stacking pads
- ControlModule names the containers a pad saves
- ControlModule persists the look a preset applied
- ControlModule refuses a preset name that could escape its folder
- ControlModule refuses a preset setting a container this device does not have
- ControlModule refuses to rename a preset over an existing one
- ControlModule refuses to save a new preset onto an occupied pad
- ControlModule refuses to save a preset with no name
- ControlModule renames a preset by renaming its file
- ControlModule replaces only the role a preset carries
- ControlModule reports the roles a preset covers
- ControlModule saves a look and puts it back
- ControlModule saves a preset as a state document
- ControlModule saves a preset onto the chosen pad
- ControlModule saves one card as a preset and puts back only that module
- ControlModule shows a failing preset's full path
- ControlModule shows what each preset captures
- ControlModule sizes the Home Assistant look list to the presets that exist
- ControlModule stops showing a deleted preset as active
- ControlModule survives a corrupt preset file
- ControlModule switch 1 drives the global on/off
- ControlModule writes a preset that is well-formed JSON
- ControlModule writes none of its save form to flash
- OSC feedback never sends a value back to the host it came from, and sends a change made here
- a MIDI batch written by the browser is decoded on the next tick
- a MIDI desk shows the surface: fader motors, SELECT lights and knob rings
- a MIDI desk's SELECT button flips the surface's switch
- a MIDI desk's fader moves the surface's fader
- a MIDI desk's fader touch holds its own motor, not the other surfaces
- a MIDI desk's knob ring fills over the range of what the knob drives
- a MIDI desk's knob ring follows a new target with the same value
- a MIDI desk's knob turns the surface's encoder by its steps
- a MIDI desk's touch before it attaches holds back no surface
- a MIDI service on the network is announced over Bonjour only while it waits to be invited
- a Mackie desk shared over the network shows the display line of the device it drives
- a Mackie desk shared over the network turns a knob down as well as up
- a Mackie desk's LCD shows the Control card's display line
- a control touched on one surface still reaches the others as it moves
- a desk on the USB port is greeted once, sent only changes, and not greeted again when the tree is prepared
- a desk shared from a device's USB port drives another device's surface over RTP-MIDI, both ways
- a fader follows its target when something else moves it
- a fader position sent to a MIDI desk decodes back to the same value
- a surface control drives whatever it is assigned to
- a surface control follows the control it drives, so the two never disagree
- a surface does not rewrite a target that already holds the value
- a switch drives a button on the press alone
- a switch follows the control it drives, including at startup
- a switch on enabled writes it only when it changes
- a target changing underneath reaches the surfaces within a 20 ms tick
- a touched control is not driven on that surface, and resyncs when released
- a written fader reaches the surfaces without waiting for the mirror pass
- an APC40 is greeted into the mode where the host sets every light
- an APC40 pad holding a preset that cannot apply reports that, not an empty pad
- an APC40 shows the surface: activator lights and knob rings
- an APC40's top-left pad applies the first preset, and its pads show which are stored and applied
- an APC40's track faders, track knobs and activator buttons drive the surface
- an OSC pad press applies the preset on that pad, and its state goes out on /mm/padstate, which presses nothing
- an encoder detent steps its target, which owns the value and its bounds
- attaching a surface seeds it with the current state
- automap puts the effect enabled last on the desk from the second slot, and a hand-assigned slot wins
- only the bound encoder drives anything
- the USB port has one owner: a second MIDI service waits while the first holds it, and takes it once given back
- the display strip names what an encoder selected, not its number
- the mirror sends a control only when its value changed
DevicesModule¶
- DevicesModule: a cached device drops once past the probation window
- DevicesModule: a cached device survives just under the probation window
- DevicesModule: a live-confirmed device drops once past kStaleMs (24h)
- DevicesModule: a live-confirmed device survives well past probation (to 24h)
- DevicesModule: a restored MoonLight device is not downgraded by a WLED packet
- DevicesModule: restore tolerates an empty / malformed cache
- DevicesModule: a DISABLED module does not claim the active_ seat at boot
- DevicesModule: a MoonLight device is not downgraded by a later WLED packet
- DevicesModule: a MoonLight-marked packet lists a MoonLight device
- DevicesModule: a group that stops arriving after it worked is caught
- DevicesModule: a peer heard both ways, or not at all, proves nothing
- DevicesModule: a peer heard only over broadcast proves the network drops multicast, and its multicast copy clears it
- DevicesModule: a peer rename updates the existing row's name
- DevicesModule: a peer without multicast does not undo another peer's multicast every round
- DevicesModule: a plain WLED packet lists a WLED device with its name
- DevicesModule: a short / garbage datagram is ignored, never listed
- DevicesModule: distinct devices don't cross-contaminate name or type
- WledPacket: either copy of a MoonLight packet carries the marker
- DevicesModule: a Hue bridge is listed with its color count
- DevicesModule: a corrupt persisted color clamps to the valid range, row still restores
- DevicesModule: a persisted Hue bridge restores as a Hue row with its count
- DevicesModule: upsertHueBridge is idempotent, updates count in place
EthernetModule¶
- Desktop Ethernet seam is a safe no-op
- Desktop ethConfigDefault is ethNone (no Ethernet)
- Desktop static-addressing seam is a safe no-op
- EthPhyType enum values match the dropdown/dispatch contract
- Ethernet IP settings changed while unplugged apply at once, both ways
- Ethernet is opt-in: ethNone is the zero value (unset ethType → no PHY)
- Ethernet never applies a Static setting with its gateway outside the subnet, and names it
- Ethernet switched back to DHCP waits for the lease before cascading
- Ethernet's IP settings: DHCP by default, Static reveals the fields
- Static mode pins its address over a self-assigned Ethernet one
- Static mode pins the static IP during STA bring-up (WaitingSta)
- a chip with one buildable preset defaults to it, not to Custom
- a self-assigned Ethernet address keeps the cascade waiting on a WiFi build, and a lease ends it
- changing a static link-local address keeps Ethernet connected
- each chip offers only the Ethernet boards it can wire
- ethBoard seeds from the pins after a restore, not once before it
- ethPhyAddr-style control: signed int16, -1 sentinel in range, number field
- fixedPins never writes past the capacity it is given
FileManagerModule¶
- scenario_FileManager_writes_and_reads_back
- A file written into a folder that does not exist yet creates the folder, through both writes
- FileManager: a '..' traversal never escapes root (the seam's confinement)
- FileManager: delete of a non-empty folder is rejected, not a crash
- FileManager: delete removes a file too
- FileManager: fsWriteStream commits atomically (no torn file)
- FileManager: fsWriteStream discards on abort (incomplete upload)
- FileManager: fsWriteStream writes a multi-chunk NUL-containing payload in full
- FileManager: mkdir creates a dir at the target path; delete removes it
- FileManager: mkdir nested under an existing dir
- HTTP header names match case-insensitively, so any client's Content-Length counts
- HttpServer::parseFilePath accepts a valid path and roots a relative one
- HttpServer::parseFilePath rejects traversal, empty, missing, and overlong
- fsSize reports a file past 2 GiB correctly, not as negative
- removeTree deletes a folder and everything under it
- removeTree deletes a plain file
- removeTree refuses a tree deeper than its bound
- removeTree refuses to wipe the flash root
- the factory reset deletes every setting and preset and keeps the other files
FilesystemModule¶
- A config file in the flat format older builds wrote loads at boot and is saved back as a document
- A settings directory that does not exist yet is created when the filesystem mounts
- A settings directory that rejects a write fails the mount, not just a missing one
- A settings location that cannot be written to fails the mount, not every later save
- FilesystemModule Int16 controls round-trip preserves the saved value
- FilesystemModule does not replace code-wired child on type mismatch
- FilesystemModule preserves code-wired children when JSON predates them
- FilesystemModule restores a control that only exists after prepare()
- FilesystemModule restores a user module recorded after reordered code-wired siblings
- FilesystemModule restores a user-module reorder in the saved order
- FilesystemModule restores a value-dependent control across reload (the peripheral/clockPin bug)
- FilesystemModule round-trip
- FilesystemModule round-trips a config larger than the old 2 KB cap
- FilesystemModule singleton survives probe construct+destruct
- FilesystemModule skips an unknown type mid-list and keeps the user module after it
- FilesystemModule structural load makes the live tree match the document
- FilesystemModule writes valid JSON with children
- FilesystemModule: an unknown entry before a wired child does not duplicate the wired child
- MM_DATA_DIR chooses the settings directory over any other rule
- The device's own config file holds a secret, and a document written for sharing leaves it out
- A continuous writer cannot defer a pending save forever
- A live control is left out of the saved file, and its neighbors still save
- Writing a live control does not mark its module dirty, so a slow save still lands
- a requested config apply lands on the next tick, not on the requesting task
- a restored value for a prepare-time control lands after the next prepare
- a restored value that differs from the live one reaches the module once
- a saved subtree keeps the names its modules were given
- a stored document recreates children the live tree no longer has
- a stored document removes children it does not describe
- a stored document restores a tree that changed since it was captured
- a stored document skips an unknown module type and applies the rest
- a stored document survives a corrupt document
- a stored document whose name clashes elsewhere takes a free name
- a value that lands after the prepare reaches the module, and an unchanged one does not
- a written config file applies to the running tree without a reboot
- writeStateMember writes a subtree a caller can keep
FirmwareUpdateModule¶
- scenario_Firmware_MoonBase_joins_the_apps_network
- scenario_Firmware_MoonBase_opens_the_devices_access_point
- scenario_Firmware_reports_what_is_running
- FirmwareUpdateModule OTA status routes through the status slot
- FirmwareUpdateModule firmware control populated
- a MoonBase that does not match the app says so, and only then
HttpServerModule¶
- scenario_State_one_document_adds_an_effect_with_its_controls
- a burst of file writes costs one re-derive, not one per file
- a file write with no scheduler is a no-op, not a crash
- a module that notifies its values changed is patched alone, ahead of the second
- a replaced module is named by the caller, then by its old custom name, then by its type
- a written file asks the tree to re-derive
- apply-core: a control validator rejects bad input on the set path
- apply-core: a delete removes the module, and refuses a top-level one or one the user cannot remove
- apply-core: a parent refuses a child whose role it does not accept
- apply-core: a replace swaps the type in the same place, named by the caller, its custom name or the new type
- apply-core: an add creates a child and refuses a name some module holds
- apply-core: applyAddModule reports the created name, disambiguated
- apply-core: applySetControl writes a value, rejects out-of-range / unknown
- apply-core: applyWledState picks a palette by index and by color through the light output
- apply-core: applyWledState sets on + bri independently (no brightness clobber)
- apply-core: enabled toggle requests a full resync; a plain value change does not
- buildStatePatch: a changed control value yields a one-entry patch
- buildStatePatch: a status change rides the patch (no resync needed)
- buildStatePatch: unchanged tree yields an empty patch
- control writes over /ws: frames sharing a read all apply, and a cut frame waits for the rest
- schema-changed hook: rebuildControls() resyncs ONLY on a real schema change
- the frame walker hands a Ping and a Close to the control callback, in order with the text
- the preview uplink parser refuses oversized and extended-length frames
- the preview uplink parser reports a frame's length so a coalesced read can be walked
- the preview uplink parser unmasks exactly one small client data frame
- HttpServer: a connection that never sends a request is closed once its parking time is up
- HttpServer: a request that arrives after the connection was accepted is served, not dropped
- a firmware upload without a Content-Length is refused with 411, as every upload route refuses it
- a module route whose name does not fit is refused with 400 on move, replace, delete and read alike
- a module whose name holds a space is moved, replaced and deleted through its percent-encoded path, as it is read
- DELETE /api/dir on /sd itself is refused, leaving the card untouched
- DELETE /api/dir on the card root spelled with extra slashes is refused too
- DELETE /api/dir removes a file and a non-empty folder on the card, via removeTree
- a path traversal through /sd is rejected, the same shared guard flash uses
- an SD download larger than one tick's chunk streams across several ticks and arrives whole
- an SD download with no free transfer slot answers 503, not a broken 200
- an SD upload declaring more than FAT32's 4 GiB-1 ceiling is refused with 413
- an SD upload larger than one tick's chunk streams across several ticks and reads back whole
- an SD upload past the card's free space is refused with 507, immediately
- an SD upload spanning several chunks drains in far fewer ticks than chunks
- an SD upload that stalls mid-transfer is closed and its slot frees for the next one
- an upload to /sd without a mounted card answers "no card mounted", not a generic failure
- an upload to a relative sd/ path gets the card's size limit
- release mid-transfer closes the slot's stream and socket, leaking neither
- the root listing advertises /sd as a folder, so the tree can reach it
- the root listing does not advertise /sd once the card is unmounted
- a Close is answered with a Close echoing its status code, and the connection ends
- a Ping is answered with a Pong carrying the same payload
- a Ping while the full state streams is answered between frames, and the document stays whole
- a client asking for its view again gets it, carrying the id it asked with
- a client is patched only with the modules it subscribes to
- a client that never subscribes is sent the WLED frame and no module state
- a client that subscribes is sent a full state, and the client already there is not
- a full state written in fragments of any size equals the state written at once
- a module outside the subscription is sent as its outline, its shape kept and its controls left out
- a subscribed module deep in the tree brings its own controls and not its parent's
- a subscription change resyncs that client alone, and the same subscription again does not
- a subscription entry matches a module's type as well as its name
- a tree changed between fragments still ends the document in valid JSON
- one module's JSON is its entry in the full state, children included
- patches and the WLED frame wait while a client's document is under way
- the full state streams as one fragmented message that reassembles to the subscribed document
- the subscribe message names modules, and a star subscribes to the whole tree
- a flash upload larger than one chunk streams across ticks and lands whole, answered once it commits
- a flash upload over the flash size bound is refused with 413 before any byte lands
- a flash upload whose client stalls never holds a tick, and once the stall guard trips leaves no file and frees its slot
I2cBusModule¶
- a scan names each device by the module that drives it, and flags an expected one that does not answer
- a scan without a bus names the missing pins, and with one reports what answered
- moving the bus to other pins counts as a new bus, and the same pins do not
- releasing an instance that never opened the bus leaves the live bus open
- the bus opens once both pins are set, and closes when one is unset
MoonCloudModule¶
- scenario_MoonCloud_consent_is_off_until_asked
- a late acknowledgement of an earlier request leaves the newer one waiting
- a post waits in the outbox as its id, path and body, and the next is refused until the page empties it
- a request too large for the outbox is refused rather than truncated
- after a reboot the ids count on from the last acknowledged one
- the relay's controls exist only on a board that relays, hidden, the request itself never saved
MoonStatsModule¶
- a report handed to the page counts as sent only once the page delivers it
- a report is due only after the user consents
- a report schedules the save that records it
- a restart sends nothing once the running version has been reported
- an install and an upgrade are distinguished by the recorded version
- an upgrade sends one report, and the boot after it sends none
- declining survives a reboot and an upgrade
- no installation id is produced without consent
- saying yes, then no, then yes again sends only the first report
- the consent explanation is a status that follows the answer
- with no access point, a consented device has a report due
- withdrawing consent stops a report that would otherwise be due
MoonTalkModule¶
- a message is published by the send button, not by typing
- naming is a second consent, off by default
- send with an empty message does nothing
- send without consent publishes nothing and keeps the text
- the device name comes from the System module
- the device name is looked up rather than stored
- the talk consent explanation is a status that follows the answer
MqttModule¶
- MqttModule publishes state when only the applied look changed
- MqttModule re-announces the effect list when a preset appears mid-session
- MqttModule: CONNACK publishes a retained HA discovery config
- MqttModule: a PUBLISH on an unrelated topic is ignored, not a crash
- MqttModule: a PUBLISH split across feeds still routes (fragment reassembly)
- MqttModule: brightness/set rescales 0-100 to 0-255
- MqttModule: ha/set is key-order-independent
- MqttModule: ha/set with only state leaves brightness untouched
- MqttModule: ha/set {brightness} maps 0-255 with no rescale
- MqttModule: ha/set {state} drives Drivers.on
- MqttModule: hsv/set maps a hue to the nearest palette + value to brightness
- MqttModule: on/set drives Drivers.on
- MqttModule: retract frees the discovery buffers even while disconnected
- MqttModule: topic identity is MAC-stable, not affected by a device rename
NetworkModule¶
- scenario_Network_identity_and_discovery
- a Static setting a network can use is used, gateway and DNS optional
- a Static setting can start from another interface's network, its address left to choose
- a Static setting that cannot work is named, rule by rule
- an edit that leaves the addressing in effect unchanged applies nothing
- both addresses of a point-to-point /31 are usable
- the app's saved config carries every key MoonBase reads
- the config scraper finds a key at any depth, and only whole keys
- the config scraper reads a child's key by the child's type, among its own members
- the config scraper reads each interface's IP settings, by the app's rule
- the config scraper reads every known network in order, and a control byte as the app escapes it
- the config scraper still reads the flat format older builds wrote
- the two boot images serve the OTA routes under the same names
- Ethernet takes over from WiFi once it has an address
- NetworkModule mode control reflects current state
- NetworkModule::setWifiCredentials accepts long SSID without crash
- NetworkModule::setWifiCredentials remembers the network at the top and saves it
- NetworkModule::setWifiCredentials with null SSID is a no-op
- NetworkModule::setWifiCredentials with null password treats it as empty
- WiFi rssi/txPower controls hidden until the radio is up
- a DHCP network joined after a Static one leases its own address
- a failed join keeps the typed password for a retry
- a join asked for after a long wait is tried, not failed at once
- a join asked for while Ethernet carries the device is refused with the reason
- a known network asked for from the card that fails is not tried again in the same round
- a link-local address counts on a WiFi build only when it is the static one the user set
- a link-local address counts only on a build without WiFi, and a lease counts everywhere
- a link-local address is recognized by its 169.254 prefix alone
- a network added above the joined one leaves the joined one marked
- a scan between two attempts still leaves every known network its try
- a scanned network that does not join says why and is not remembered
- a second Connect during a join replaces the join
- a station that joined waits for its lease before the next network is tried
- a station that rejoins after a dropout waits for its lease
- a switch back to DHCP waits for the lease rather than falling back to the access point
- connecting to a scanned network joins it now and remembers it once joined
- credentials arriving while Ethernet carries the device are only remembered
- editing the joined network's IP settings applies them live
- forgetting the network the device is on leaves it for the next known one
- parseDottedQuad accepts valid dotted-quads and rejects junk
- the cascade tries the networks in range first, then the rest
- the local name is advertised again after switching from WiFi to Ethernet
- the station tries each known network in priority order before the access point
- a 6.0 config on a named Ethernet board takes the board's map, as 6.0 did at boot
- a config from 6.0 moves its network onto the Ethernet and WiFi cards, once
PinsModule¶
- PinsModule lists the pins a module holds without a control naming them
- PinsModule: a CSV with an out-of-range pin claims nothing, and never a false GPIO
- PinsModule: a child module's pins are walked (depth-first), not just the roots
- PinsModule: a claim on a reserved pin is flagged severity error
- PinsModule: a claim survives its owning module being destroyed (no use-after-free)
- PinsModule: a claimed pin with live state emits level + drive columns
- PinsModule: a conflict promotes a strap warn to error (severity is the max)
- PinsModule: a disabled module's pins are released from the map, re-claimed on enable
- PinsModule: a driven role on a strap pin is flagged severity warn
- PinsModule: a driven role on an input-only pin IS flagged warn
- PinsModule: a pin with no live state (valid=false) omits the live columns
- PinsModule: a safe pin carries no severity field
- PinsModule: an input role on an input-only pin is NOT flagged
- PinsModule: collects set Pin controls with name-derived roles, skips -1
- PinsModule: dir column reflects the live pad direction (out/in/both/off)
- PinsModule: dir is shown as info, NOT a warning — a driven role with output off is unflagged
- PinsModule: disabling a PARENT frees its children's pins (effectivelyEnabled cascade)
- PinsModule: exposes a single read-only pins list, fixed System module (Generic role)
- PinsModule: parses the LED-driver pins CSV into per-lane claims
- PinsModule: rows are GPIO-ordered and a double-claim stays visible in the detail
SystemModule¶
- scenario_System_identity_survives_reboot
- Service role name
- SystemModule MAC-to-deviceName
- SystemModule accepts back the unset deviceModel a fresh board reports
- SystemModule accepts no user-added children
- SystemModule bootReason control populated
- SystemModule deviceName control
- SystemModule falls back to the MAC name when deviceName is all-invalid
- SystemModule leaves a valid deviceName unchanged
- SystemModule propagates lifecycle to a wired-by-code child
- SystemModule sanitises a live deviceName edit
- SystemModule: a persisted firmware name is overwritten by the compile-time one
- defaultDeviceName: MM- and the last two MAC bytes, and nothing past a short buffer
- the factory reset button asks for a second press, and safe mode says why it started
- sanitizeHostname leaves a valid hostname unchanged (idempotent)
- sanitizeHostname replaces spaces with a single dash
- sanitizeHostname strips punctuation and other invalid chars
- sanitizeHostname trims leading and trailing dashes / invalid runs
- sanitizeHostname yields empty for all-invalid input (caller falls back)
TasksModule¶
- TasksModule: MoonLight tasks sort to the top, system tasks below, alphabetical within
- TasksModule: a fixed System module (Generic role, no delete affordance)
- TasksModule: exposes a tasks list + core0/core1, no separate modules list
- TasksModule: the empty desktop snapshot is safe (no RTOS on host)
- TasksModule: the render task's detail nests the modules + the ∑/tick cross-check
- TasksModule: the tasks list renders the injected RTOS tasks with their fields
WiFiModule¶
- scenario_WiFi_known_networks_fall_back_in_order
- a full known list never forgets the network carrying the device
- a known network with an unusable Static setting says so on its row and joins by DHCP
- a known network's fields carry their defaults, the joined one's address fields its lease
- a known network's password reaches the page obfuscated
- a scan lists the networks in range as a WiFi picker shows them
- a scan that never finishes ends after its time
- a scan the radio cannot start yet waits for it and starts once it can
- a scan the radio refuses for a scan's whole time ends with a reason
- an open known network is marked open, and only the saved file carries passwords
- each known network has its own IP settings
- known networks are added, edited, reordered and forgotten from the card
- known networks survive a save and a restore
- the join order puts the networks a recent scan saw first, each part in list order
- the joined network switched to Static starts from its lease
- the known list stops at its cap, and a new network replaces the lowest-priority one
Light · Drivers¶
HlsDriver¶
- HlsDriver accepts a scaled frame that exactly meets the encoder's limit
- HlsDriver auto-scale clears the minimum on both axes, however thin the wall
- HlsDriver blows a small wall up to the encoder's minimum, keeping its shape
- HlsDriver drops frames the encoder refuses, and says so
- HlsDriver encodes at the layout's size, not a control's
- HlsDriver encodes no faster than targetFps
- HlsDriver gives up visibly when the encoder dies on every restart
- HlsDriver hands ffmpeg the exact live-HLS invocation
- HlsDriver holds the exact frame rate over a full second
- HlsDriver keeps its schedule after a late tick
- HlsDriver leaves a large enough wall at 1:1
- HlsDriver pipes the grid pixel-exact
- HlsDriver refuses a scaled frame too large for the encoder, never wrapping into one
- HlsDriver releases and re-prepares cleanly
- HlsDriver reports a missing ffmpeg instead of failing
- HlsDriver restarts a dead encoder and resumes streaming
- HlsDriver resyncs after a stall without sending a duplicate frame
- HlsDriver upscales by whole blocks, inventing no colors
- platform encoder is claimed by one module at a time
Hub75Driver¶
- Hub75Driver offers the clock edge, defaults to rising, and re-inits on a change
- Hub75Driver refuses a line on a flash/PSRAM pin and names it, rather than resetting
- Hub75Driver reports a published board's lines as fixed pins while their controls hide
- Hub75Driver reports no fixed pins for an editable board: its controls carry the claim
- PinsModule flags a HUB75 board line colliding with an LED lane
- HUB75 encode: a color bit lands on its own line, for both half-panels
- HUB75 encode: a one-column row is its latch word
- HUB75 encode: a panel driving four rows per address step encodes every pair
- HUB75 encode: a row clocks exactly its width, so the last column reaches the panel's last pixel
- HUB75 encode: an unusable geometry writes nothing
- HUB75 encode: below a quarter brightness the lowest 6-bit plane of a 64-column row is dark
- HUB75 encode: brightness shortens every plane's window, keeping the values
- HUB75 encode: depth below 8 keeps the HIGH bits
- HUB75 encode: each plane is lit twice as long as the one below, the top plane for the whole row
- HUB75 encode: the DMA wrap lights the last row once, from row 0's data words
- HUB75 encode: the OE bit is set where the panel is lit, so an all-zero idle bus is dark
- HUB75 encode: the address on a data word is the row in the LATCH, one behind
- HUB75 encode: the frame is bit-plane major, one word per column
- HUB75 encode: the last column's word latches, dark
- HUB75 frame size is what decides the peripheral
HueDriver¶
- HueDriver: RGB→HSV maps the primaries to the right Hue wheel positions
- HueDriver: a black pixel becomes on:false
- HueDriver: a colored pixel becomes an on/bri/hue/sat state body
- HueDriver: bodyLooksComplete is the truncation signal for the grow-and-retry fetch
- HueDriver: parseLights keeps only color-capable, reachable lights
- HueDriver: room/light selection filters the driven set
- HueDriver: status reports the driven-of-total light count
- HueDriver: unchanged color is not resent, a changed one is
NdiDriver¶
- NdiDriver applies the driver's own brightness correction
- NdiDriver blanks the tail when the layer is smaller than the frame
- NdiDriver holds its frame rate to the fps ceiling
- NdiDriver names the source after the device when left blank
- NdiDriver reports a missing NDI runtime instead of failing
- NdiDriver sends one pixel per light at the layer's size
NetworkSendDriver¶
- scenario_NetworkSend_one_sender_many_receivers
- NetworkSendDriver grows corrected_ in onCorrectionChanged on RGB → RGBW
- NetworkSendDriver onCorrectionChanged is a no-op when outChannels unchanged
- NetworkSendDriver sizes corrected_ in prepare, not in tick
- A DMX universe carries whole fixtures, never a split one
- ArtNet packet header format
- ArtNet packet length field is big-endian
- ArtNet packet with non-zero universe
- ArtNet universe splitting for 256 RGB lights
- DDP packet header format
- E1.31 packet header format
- NetworkSendDriver: E1.31 multicast on a network that drops it says to use unicast
- NetworkSendDriver: E1.31 multicast sends the whole window as one stream, whatever the hosts say
- NetworkSendDriver: a correction that grows the buffer under a paced send restarts the task safely
- NetworkSendDriver: a lightsPerHost list that doesn't match the tube count still slices sanely
- NetworkSendDriver: a long packet gap is shortened so the frame fits its fps period
- NetworkSendDriver: a malformed hosts entry idles the driver, publishing no partial destination list
- NetworkSendDriver: a named host waits for its address while the others send
- NetworkSendDriver: a packet gap of 0 sends the frame as one burst from the tick
- NetworkSendDriver: a packet gap spaces a frame's packets, sent from a task
- NetworkSendDriver: a protocol without the chosen mode falls back to unicast
- NetworkSendDriver: a range fans the window out over its tubes, one slice each
- NetworkSendDriver: lightsPerHost follows the ledsPerPin idiom
- NetworkSendDriver: no destination by default: it idles, and says why
- sACN multicast puts the universe number in the destination address
PanelCardDriver¶
- PanelCardDriver attempts recovery once per wedge
- PanelCardDriver claim survives a re-prepare without stacking
- PanelCardDriver claims the ethernet link while enabled
- PanelCardDriver counts send failures by cause
- PanelCardDriver does not latch a frame that never reached the wire
- PanelCardDriver keeps a failed restart visible
- PanelCardDriver puts rendered pixels on the wire
- PanelCardDriver rate-limits to its fps setting
- PanelCardDriver reports a failing transmit path instead of a healthy link
- PanelCardDriver reports a wedge that survives its restart
- PanelCardDriver sends a pre-v13 card one brightness and one sync
- PanelCardDriver sends nothing when the buffer covers no row
- PanelCardDriver sends nothing when the window is empty
- PanelCardDriver sends one frame per row then one sync
- PanelCardDriver sends the wall a PanelsLayout describes
- PanelCardDriver sizes its buffer in prepare, not in tick
- PanelCardDriver splits a row wider than one packet
- PanelCardDriver stops at the last row its buffer covers
- PanelCardDriver survives a failing link
- PanelCardDriver widens the row when a PanelsLayout chains panels across
- a NIC that disappeared falls back to capture-only, not to whoever took its row
- a NIC whose link speed changed is still the same NIC
- a persisted interface label matches its adapter despite a changed speed
- a reordered NIC list keeps the selected adapter, not its old row
- a vanished persisted NIC degrades to capture-only, never a crash
- an interface label with JSON metacharacters keeps the schema parseable
- resetting the interface test seam with nullptr restores real enumeration
- the interface Select lists detected NICs, none first, bind names behind the rows
- Panel brightness frame is 77 bytes on three channels
- Panel frames carry the fixed MAC pair the cards filter on
- Panel full row packet fits the driver's frame buffer
- Panel payload starts at byte 13, inside the EtherType field
- Panel row number above 255 is big-endian across two bytes
- Panel row wider than one packet carries a pixel offset
- Panel sync frame is 112 bytes and carries brightness
ParallelLedDriver¶
- scenario_peripheral_grid_sweep
- scenario_peripheral_switch
- ParallelLedDriver async degrades to synchronous when second buffer won't fit
- ParallelLedDriver double-buffer alternates and defers the wait
- ParallelLedDriver doubleBuffer toggles allocation and path
- ParallelLedDriver drives an N-channel (>4) correction without overflow
- ParallelLedDriver frameTime tick1s is safe before the first transfer
- ParallelLedDriver reinit drains both in-flight buffers
- ParallelLedDriver single-buffer mode waits every frame on buffer 0
- ParallelLedDriver: a peripheral that can't double-buffer stays single-buffer with doubleBuffer ON
- ParallelLedDriver: a timed-out wait never re-encodes into the live buffer
- ParallelLedDriver: give-up self-recovers on a periodic retry, no reinit needed
- ParallelLedDriver: a live peripheral swap quiesces the render worker before freeing the backend
- ParallelLedDriver: a peripheral swap does not double-free or dangle the control list
PreviewDriver¶
- A rig with no moving heads sends no aim message at all
- PreviewDriver adopts the client-requested stride, and only then
- PreviewDriver buffered send uses the sparse driver buffer, not the dense box
- PreviewDriver cancels an in-flight buffered send on rebuild (resize safety)
- PreviewDriver coordinate table carries the real lights, not the box
- PreviewDriver dense downsample packs colors by closed-form index, in lattice order
- PreviewDriver downsamples on a regular spatial lattice when a client asks coarser
- PreviewDriver gates the next frame on the buffered send draining (adaptive fps)
- PreviewDriver ignores an out-of-range stride request
- PreviewDriver keeps a sparse large-box layout at full resolution
- PreviewDriver keeps a sparse light whose axes share a factor above the cache's cap
- PreviewDriver keeps its index cache filled when a sparse layout shrinks
- PreviewDriver per-frame RGB count matches the coordinate table
- PreviewDriver returns the memory a larger layout took when the layout shrinks
- PreviewDriver routes a dense full-res frame through the resumable buffered send
- PreviewDriver sends the coordinate table only when a client asks, never on its own
- PreviewDriver sends the table whole and strides only the colors
- PreviewDriver small grid sends all lights exactly
- PreviewDriver streams aim for a rig whose fixtures move
- PreviewDriver strides a mapped layout's colors without re-walking the layout
- PreviewDriver targetFps default
- PreviewDriver tolerates the active Layer being deleted
- a table request outranks frames, is retried while refused, and frames then resume
- a wedged link never blocks a tick, never closes a client, and resumes when it drains
PwmLightDriver¶
- a dim fade under the CIE curve keeps a distinct duty per level
- a duty is written only when it changes
- a fine role's pin stays dark on the wide path
- each light's corrected channels become the duties of its pins
- more pins than the chip has channels is an error
- pins past the last whole light idle and the status says so
- the channels' pulses start at evenly spaced phases
- the frequency follows from the resolution
- the pins make as many lights as the profile's channels divide them into
RmtLedDriver¶
- RmtLedDriver grows the symbol buffer when the grid grows
- RmtLedDriver keeps the symbol buffer across a pins change
- RmtLedDriver keeps the symbol buffer across a rebuild (reinit must not free it)
- RmtLedDriver loopback re-parses pins before testing (no stale-pin verdict)
- RmtLedDriver loopbackRxPin tracks the loopbackTest toggle
- RmtLedDriver loopbackTxPin tracks the loopbackTest toggle
- RmtLedDriver releases the symbol buffer on release
- RmtLedDriver setup/release is repeatable with no residual state
- RmtLedDriver shows 'driving N of M' as the resting status after a build
- RmtLedDriver sizes symbols to the driven lights, not the whole window
- RmtLedDriver sizes the symbol buffer in prepare
- RmtLedDriver: a DISABLED driver does not acquire through the boot sweep
- RmtLedDriver: disabling releases the resource, re-enabling re-acquires (applyState)
- RmtLedDriver idles with a status error on a bad pin list
- RmtLedDriver re-slices when the source buffer changes
- RmtLedDriver slices the buffer across pins (even split)
- RmtLedDriver slices the buffer per ledsPerPin
- RmtLedDriver tick is crash-safe for every pin configuration
- RmtLedDriver window: a size-1 window at 0 is the onboard-LED case
- RmtLedDriver window: a start past the buffer end yields an empty slice
- RmtLedDriver window: count 0 means the rest of the buffer from start
- RmtLedDriver window: ledsPerPin distributes over the window, not the whole buffer
- RmtLedDriver with the empty default pins idles cleanly (no pin assumed)
- assignCounts broadcast clamps to the remaining buffer
- assignCounts broadcasts a single value to every pin
- assignCounts clamps a pin to the WS2812 ceiling and warns (drives 2048, not zero)
- assignCounts clamps so the sum never exceeds the buffer
- assignCounts handles a zero-light buffer (0×0×0 grid) as all-zero
- assignCounts ignores extra counts beyond the pin list
- assignCounts maps a short list, even-splits the unlisted remainder
- assignCounts rejects a bad token
- assignCounts takes explicit per-pin counts
- assignCounts with an empty list splits evenly, last pin takes the rounding remainder
- custom timing is taken as the user typed it
- custom timing that no chip could decode is ordered rather than emitted
- parsePinList accepts a comma-separated list, in order
- parsePinList accepts a single pin and spaces around tokens
- parsePinList enforces maxPins (the chip's TX-channel cap)
- parsePinList expands inclusive ranges and mixes them with single pins
- parsePinList rejects a backwards range, a range duplicate, and a range over the cap
- parsePinList rejects an out-of-range pin (the gpio_func_sel crash guard)
- parsePinList rejects bad input with a static error message
- parsePinList rejects duplicate pins
- selecting 400 kHz doubles the bit cell, which is what a 12V WS2811 strip decodes
- switching timing takes effect without reconfiguring the pins
- the WS2811 fast mode keeps the 1.25 us cell with narrower pulses
- the default timing is the one that drives WS2812B and SK6812 alike
- window: default kWindowAll resolves to ALL lights on a >65535 buffer
- GRB ordering comes from Correction: the driver ships whatever bytes it produced
- a light's channels go out in wire-byte order, 8 bits each
- an RGBW preset puts four bytes per light on the wire
- wire bytes expand MSB-first, 0 and 1 bits get the right pulse widths
RtspDriver¶
Light · Effects¶
AudioSpectrumEffect¶
AuroraEffect¶
- scenario_Aurora_fps
- Aurora renders on a grid too small to have a center
- Aurora renders on a strip, a panel and a cube alike
- Aurora renders the same picture whether or not the polar table is available
- a still speed holds the picture, so a fixture can be frozen
- curtains appear rather than an even wash of light
- each layer adds structure, so the cost knob buys something
- each projection composes the volume differently
- on a volumetric fixture the curtains have depth instead of one repeated slice
- the composition keeps moving, and no two moments look alike
- the contrast control decides how much of the field lights up
- the kaleidoscope fold makes the composition symmetric
BeatRipplesEffect¶
- ripples walk the whole palette, and each stone shifts it
- still water shows its ripples, and they spread
BlurzEffect¶
- BlurzEffect geqScanner sweeps the dot to a new position each frame
- BlurzEffect lights the buffer when fed a signal
- BlurzEffect stays black without an audio frame
- BlurzEffect survives degenerate grid sizes
BouncingBallsEffect¶
- BouncingBallsEffect balls rise above the floor as time advances
- BouncingBallsEffect lights the bottom row on the first frame
- BouncingBallsEffect survives a 0x0x0 grid
- BouncingBallsEffect with zero balls leaves the buffer black
BreakoutEffect¶
- Breakout deals a full wall on the first frame
- Breakout deals a new wall when rows changes
- Breakout fits the wall to a short grid
- Breakout hands a written paddle to the player
- Breakout knocks bricks down and keeps the ball on the court
- Breakout's descending wall gains rows over time
ColorTrailsEffect¶
- an outward radial flow carries color further from the center than an inward one
- every flow renders something rather than draining the picture to black
- the center cell has no direction to move in, and does not divide by zero
DemoReelEffect¶
- DemoReelEffect cycles registered effects and renders each
DistortionWavesEffect¶
- DistortionWavesEffect produces spatial variation
- DistortionWavesEffect speed 0 is frozen (stable across ticks)
- DistortionWavesEffect survives a 0x0x0 grid
- DistortionWavesEffect writes non-zero RGB data
FireEffect¶
- FireEffect allocates heat buffer when enabled
- FireEffect frees heat buffer when disabled
- FireEffect renders non-zero buffer after enough sparks
FireworksEffect¶
FixedPointEffect¶
- a demo switch does not join the old figure to the new one
- every demo draws something on a panel
- the all setting reaches every demo, and only the demos
FixedRectangleEffect¶
- FixedRectangleEffect defaults light the origin corner and fill a small grid
- FixedRectangleEffect honours a non-zero origin
- FixedRectangleEffect lights only cells inside the configured rect
- FixedRectangleEffect survives a degenerate grid
FluidEffect¶
- scenario_Fluid_solver
FreqMatrixEffect¶
- FreqMatrixEffect paints a lit source pixel from a live tone
- FreqMatrixEffect paints black on silence
- FreqMatrixEffect scrolls the painted pixel one step along Y
- FreqMatrixEffect survives degenerate grid sizes
FreqSawsEffect¶
- FreqSawsEffect keepOn draws bands even with no audio
- FreqSawsEffect reacts to a fed audio frame
- FreqSawsEffect runs at degenerate grid sizes without crashing
- FreqSawsEffect silence with keepOn off leaves the buffer dark
FseqPlayerEffect¶
- a channelDataOffset past the buffered header region is skipped, landing frame data correctly
- a header with 0 channels or 0 frames fails to load with its own status, not out of memory
- a manual-mode load never warns about the file's own rate, however fast it is authored
- a playlist row whose file no longer scans is rendered with no selection, not the first file
- a playlist row's own play field jumps to that row and starts a fresh pass
- a released player holds no scan cache or buffers, keeps its playlist, and rebuilds the cache when enabled again
- a scanned name past kPathCap is skipped, not truncated into a different file, with a named warning
- a stopped player owns its background (clears to black), but a paused one re-blits the same frame
- a zlib file declaring no compression blocks is refused by name, before the platform check
- a zlib file fails to load on desktop with its own status, not a generic read failure
- choosing fpsSource=manual shows the fps slider, and choosing file hides it again
- deleting and moving playlist rows keeps each row's own path
- fpsSource=file caps a rate faster than 50fps rather than honoring it
- fpsSource=file is the default, and reads the track's own stepTimeMs directly
- fpsSource=manual: the fps slider sets the playback rate directly, ignoring the file's own step time
- loading a file-sourced track faster than 50fps warns, naming the rate it asked for
- loopAll advances to the next real track and keeps playing
- loopOff stops once every row has already played this pass, with no further read needed
- loopOne restarts the same track at frame 0 without advancing the cursor
- onFileChanged rescans only for a .fseq path, skipping an unrelated upload
- pause only takes effect while playing
- play with an empty playlist does nothing
- play with no real file to open fails gracefully rather than pretending to start
- playing a flash-found file decodes and blits its frames, same as an SD one
- playing a real file decodes and blits its frames in order
- playing on a serpentine layout keeps the frame in wiring order
- prepare() starts playing on its own once there is a playlist to play
- previous and next move the cursor only, while the transport is stopped
- release() forces a clean stop, so a later re-enable never resumes into freed buffers
- rescan finds a real .fseq file at the flash root, with no SD mount at all
- rescan finds a real .fseq file, case-insensitively, in a nested folder, skipping dotfiles
- setup scans silently, with no SD-specific check or status
- setup with a mounted but empty card scans cleanly to an empty playlist
- shuffle's cursor never repeats the row it just left
- stop resets the frame position and only applies while playing or paused
- the playlist is an editable list: add, delete, move, edit
- the recursive scan's path join never doubles the slash at the SD root, and truncates safely
GEQ3DEffect¶
- GEQ3DEffect draws a bar where the audio band is energised
- GEQ3DEffect handles a grid narrower than numBands
- GEQ3DEffect renders black on silence
- GEQ3DEffect survives a zero-size grid
GEQEffect¶
- GEQEffect colorBars colors bars per column
- GEQEffect fills columns from the floor upward
- GEQEffect stays black without an audio frame
- GEQEffect survives degenerate grid sizes
GameOfLifeEffect¶
- GameOfLife: a 2x2 block is a stable still life
- GameOfLife: a 3-cell blinker flips orientation each step
- GameOfLife: a dense 3D neighborhood never indexes the B/S tables out of bounds
- GameOfLife: a lone cell dies and an empty grid stays empty
- GameOfLife: ruleset parser reads B/S sets
LavaLampEffect¶
LissajousEffect¶
- LissajousEffect leaves untouched pixels black
- LissajousEffect on a 0x0x0 grid does not crash
- LissajousEffect on a 1x1 grid maps the curve to the origin light
- LissajousEffect traces a lit curve on the grid
MetaballsEffect¶
- MetaballsEffect produces spatial variation
- MetaballsEffect writes non-zero RGB data to buffer
MoonLiveEffect¶
MovingHeadEffect¶
- a 1 x N fixture chain still varies head by head
- a 3D rig varies through depth instead of repeating each slice
- a head with no gobo channel is not written past its own channels
- a head's gobo and rotate carry the values the effect asks for
- every fixture in a 2D rig gets its own aim, not a copy of its row's first head
NebulaEffect¶
NetworkReceiveEffect¶
- scenario_NetworkReceive_two_boards
- ArtNet OpDmx build→parse round-trip
- ArtNet OpDmx parse rejects malformed packets
- NetworkReceiveEffect clamps payloads to the buffer
- NetworkReceiveEffect counts the packets and frames of the last second
- NetworkReceiveEffect does not touch the layer buffer on a tick with no packet
- NetworkReceiveEffect holds the last frame across ticks without new packets
- NetworkReceiveEffect mapped by the receiver lets the layer apply this board's wiring
- NetworkReceiveEffect mapped by the receiver names only the sender in its status
- NetworkReceiveEffect mapped by the sender on an identity layout is the same copy as mapped by the receiver
- NetworkReceiveEffect mapped by the sender reaches each light as sent, on a 3D layout
- NetworkReceiveEffect mapped by the sender reaches each light as sent, on a serpentine grid
- NetworkReceiveEffect mapped by the sender says in its status whether a frame is one copy or mapped twice
- NetworkReceiveEffect places universes at consecutive 510-byte offsets
- NetworkReceiveEffect receives over localhost UDP
- NetworkReceiveEffect reports its counts per second of elapsed time
- NetworkReceiveEffect respects universe_start
- NetworkReceiveEffect staging buffer lifecycle
- NetworkReceiveEffect tolerates a zero-light grid
- a frame goes to the output as it is only when nothing on its layer transforms it
- a held frame survives a sibling effect fading the layer
- a mirror on a sender-mapped serpentine grid mirrors the frame
- a sender-mapped frame beside another effect still reaches each light as sent
- a transpose on a sender-mapped 3D layout transposes the frame
- DDP build→parse round-trip
- DDP parse rejects malformed packets
- E1.31 build→parse round-trip
- E1.31 parse rejects malformed packets
- NetworkReceiveEffect receives all three protocols at once over localhost
- applyBytes places, clamps, and survives hostile offsets
- applyDmx honours channels_per_universe
- buildArtPollReply lays out the reply controllers parse
- cross-protocol datagrams are rejected
- isArtPoll recognizes polls and nothing else
NoiseEffect¶
- Noise renders on a strip, a panel and a cube alike
- Noise survives a degenerate grid rather than faulting
- both motions paint a field rather than a flat wash
- drift moves the field across the fixture; morph changes it in place
- morph shows the same field in every slice, because time is its third axis
- on a volumetric fixture drifting slices differ from each other
- the two motions are genuinely different fields
NoiseMeterEffect¶
- NoiseMeterEffect fades to dark without an audio frame
- NoiseMeterEffect fills the column from the floor upward
- NoiseMeterEffect survives degenerate grid sizes
- NoiseMeterEffect width 0 keeps the meter dark despite loud audio
PacmanEffect¶
- Trading ghosts for Pacmen changes what is drawn on the wall
PaintBrushEffect¶
- PaintBrushEffect draws lit strokes from a live audio frame
- PaintBrushEffect minLength gate suppresses all strokes
- PaintBrushEffect stays black on silence
- PaintBrushEffect survives degenerate grid sizes
ParticlesEffect¶
- ParticlesEffect allocates trail buffer when enabled
- ParticlesEffect frees trail buffer when disabled
- ParticlesEffect renders non-zero buffer after one frame
PictureEffect¶
- PictureEffect area sampling averages in linear light where point sampling picks one pixel
- PictureEffect does not sample again without a new picture
- PictureEffect edgeDepth gives the outermost lights the depth they ask for
- PictureEffect edgeDepth of 0 samples precisely what it did before
- PictureEffect fits a wide picture with black bands, fills it cropped, and stretches it
- PictureEffect on a 3D layout shows the picture on every slice
- PictureEffect shows a picture the grid's size pixel for pixel
- PictureEffect smoothing arrives exactly, and no time passed moves nothing
- PictureEffect smoothing closes half the gap in one half-life
- PictureEffect smoothing settles at the same speed at any frame rate
- PictureEffect snapAbove lands a cut whole instead of easing into it
- PictureEffect without a Picture service says so and shows black
- PictureEffect without smoothing follows the picture tick for tick
PlasmaEffect¶
- PlasmaEffect produces different output than NoiseEffect
- PlasmaEffect produces spatial variation
- PlasmaEffect writes non-zero RGB data to buffer
PolarNoiseEffect¶
PongEffect¶
- Pong hands a written paddle to the player, and takes it back when the input goes quiet
- Pong scores misses
- Pong starts a new game at 11
- Pong's ball stays out of the paddle columns
PraxisEffect¶
- PraxisEffect fills every pixel from the palette
- PraxisEffect survives degenerate grid sizes
- PraxisEffect varies color across the grid
PulseEffect¶
RadialSpectrumEffect¶
RainbowEffect¶
- RainbowEffect different positions produce different hues
- RainbowEffect pixel 0,0 produces valid RGB
- RainbowEffect writes non-zero RGB data to buffer
RandomEffect¶
- RandomEffect lights exactly one light per frame
- RandomEffect scatters color across many lights over many frames
- RandomEffect survives degenerate grids
RingsEffect¶
RipplesEffect¶
RubiksCubeEffect¶
- RubiksCubeEffect only paints the six face colors
- RubiksCubeEffect paints the cube on the first frame
- RubiksCubeEffect survives a 0x0x0 grid
- RubiksCubeEffect with turnsPerSecond=0 still draws but never turns
SineEffect¶
- SineEffect amplitude 0 yields a black buffer
- SineEffect survives a 0x0x0 grid
- SineEffect varies across the x axis (R channel follows x)
- SineEffect writes non-zero RGB data
SolidEffect¶
- SolidEffect mode 0 fills the buffer with one uniform color
- SolidEffect mode 0 scales the flat color by brightness
- SolidEffect mode 0 writes the white channel on an RGBW layer
- SolidEffect survives a 0x0x0 grid across all color modes
SpaceInvadersEffect¶
- Space Invaders hands the cannon to the player, one shot at a time
SphereMoveEffect¶
- SphereMoveEffect leaves most of a large volume dark (thin shell, full clear)
- SphereMoveEffect only writes non-black palette colors
- SphereMoveEffect reports 3D dimensions
- SphereMoveEffect survives degenerate grids
SpiralEffect¶
- LavaLampEffect spatial variation
- LavaLampEffect writes non-zero RGB
- RingsEffect spatial variation
- RingsEffect writes non-zero RGB
- RipplesEffect spatial variation
- RipplesEffect writes non-zero RGB
StarFieldEffect¶
- StarFieldEffect at speed 0 leaves the buffer black
- StarFieldEffect paints greyscale stars once the throttle elapses
- StarFieldEffect survives a 0x0x0 grid
- StarFieldEffect with usePalette lights stars from the palette
StarSkyEffect¶
- StarSkyEffect always keeps at least one star
- StarSkyEffect lights pixels on a populated grid
- StarSkyEffect runs at degenerate grid sizes
- StarSkyEffect white stars paint greyscale pixels
TetrixEffect¶
- TetrixEffect keeps its landed stack visible
- TetrixEffect lights up with palette color after the start delay
- TetrixEffect renders black during the start delay
- TetrixEffect survives degenerate and minimal grids
TextEffect¶
- TextEffect: default hue is mid-palette (128), not black-at-0
- TextEffect: newline wraps to a second row
- TextEffect: scroll animates and is safe at any size
- TextEffect: static text draws glyph pixels, empty draws nothing
TrailsEffect¶
- Trails reshaped to the same light count starts from black rather than the old layout's tail
- a longer persistence leaves more of the tail behind, so the control buys reach
TruchetEffect¶
TunnelEffect¶
VuMetersEffect¶
WaterRippleEffect¶
WaveEffect¶
- WaveEffect: a zero-height grid never reads out of bounds
- WaveEffect: every type stays within the grid bounds
- WaveEffect: sawtooth ramps 0→top across the phase
- WaveEffect: sine sits mid at the zero crossings
- WaveEffect: square is low then high
- WaveEffect: triangle peaks in the middle and returns
Light · Internals¶
BlendMap¶
- a mapping larger than its destination writes only what the buffer holds
- a multiply layer keeps the backdrop under white, hides it under black and dims it under gray
- a separable mode at partial opacity mixes its result with the backdrop
- a separable mode through a LUT matches the one without
- blendMap 1:N mapping duplicates pixels
- blendMap additive clamping (overwrites=false)
- blendMap additive scales source by opacity then clamps
- blendMap alpha at opacity 0 leaves dst unchanged
- blendMap alpha at opacity 255 == overwrite
- blendMap alpha-over blends src over dst by opacity
- blendMap clearFirst=false accumulates onto existing frame
- blendMap identity (no LUT) copies buffer
- blendMap is identical for single-alloc and paged LUTs
- blendMap no-LUT additive clamps at 255
- blendMap no-LUT additive scales by opacity then clamps
- blendMap no-LUT alpha at opacity 0 leaves dst unchanged
- blendMap no-LUT alpha at opacity 255 == overwrite
- blendMap no-LUT alpha-over blends 1:1 by opacity
- blendMap overwrite path clears untouched cells (sparse mapping)
- blendMap overwrite path: last write wins, no add
- each layer blendMode option selects its own blend op
- each separable blend mode computes its specification formula
- exclusion stays in range and within one step of its formula for every input pair
Canvas¶
Correction¶
- A 16-bit RGBW strip derives its white and writes it to 16 bits
- A 16-bit strip writes each color as its high byte, then its low byte
- A motion preset widens the light enough to carry pan and tilt
- A non-zero value never curves down to black
- A plain RGB fixture reports no motion channels
- A preset's master dimmer channel is driven, so the fixture actually lights
- An 8-bit profile has no fine roles, and a missing table degrades to 8 bits
- At low brightness a 16-bit channel keeps every level the 8-bit table merges
- Brightness scales before the curve, so black stays black and the top tracks the slider
- Correction Accurate: Yellow/UV use pre-subtraction RGB, not post-White
- Correction BGR preset: full reverse
- Correction GRB preset: channels reordered, 3 output channels
- Correction GRBW preset: reordered RGB + white
- Correction RGB preset: apply is identity at full brightness
- Correction RGBW preset: 4 channels, white = min(r,g,b)
- Correction brightness LUT: full brightness is identity
- Correction brightness LUT: half brightness halves each value (scale8)
- Correction roles array: absent color role is not emitted
- Correction roles array: arbitrary Custom wiring derives correct offsets
- Correction roles array: non-color role reserves a channel apply() skips
- Correction whiteMode Accurate: white subtracted from RGB
- Correction whiteMode None: white channel forced to 0, RGB intact
- Correction: UV dark on warm colors; whiteMode None zeroes WW/Y/UV
- Correction: WarmWhite/Yellow/UV synthesised from RGB via whiteMode
- Correction: brightness applied BEFORE white derivation
- Correction: rebuild switches output channel count RGB<->RGBW
- Dimmer fine is driven wide open with its coarse byte
- Each moving-head formation aims the rig differently
- Linear is an exact identity at full brightness, for a device that corrects itself
- Motion channels pass through to the fixture, unscaled by brightness
- Pan and tilt channels are left alone by the color path
- Pan fine and tilt fine repeat the aim's byte, so a full aim reaches the full range
- RGBW white is derived in linear light, then curved once
- The CIE curve keeps the endpoints and bends the middle down
DriverBase¶
- A driver's fixture profile survives a reboot by name
- A restored older backup applies its fixture profile by name
EffectBase¶
- time-driven effects render byte-identical frames (migration guard)
- an effect reaches past the first tube of a 1x60x10 rig
- every effect holds its memory again after a release and a re-prepare
- every effect owns its background rather than inheriting the last frame
- every effect renders at any channel count
- every effect survives degenerate grid sizes
Fluid¶
- Fluid reshaped to the same light count starts from black rather than the old layout's dye
- Fluid ticks dark rather than crashing on the frame between a release and its prepare, then renders again
- a fluid at rest stays at rest, so an idle fixture does not drift
- a fluid whose grids were released reports itself not ready, and resizing to the same shape rebuilds them
- a grid too small to have an interior is refused rather than half-built
- a jet carries the medium downstream, so a push is felt where it points
- a long stall leaves a plausible field rather than infinities
- each step drives the flow further toward divergence-free
- more solver iterations leave less divergence, so the cost knob buys correctness
FseqFile¶
- a buffer truncated at every interesting point reports Truncated rather than reading past it
- a compression block table displaces the sparse range table, and parsing follows it
- a legacy FSEQ magic is accepted alongside the current PSEQ magic
- a real xLights v2 export's header parses with the right block count
- a reserved compression type byte is treated as unsupported rather than silently accepted
- a v1 header with 0 channels is Empty, not a crash waiting to happen
- a v1 header with 0 frames is Empty
- a v2 header with 0 channels or 0 frames is Empty before any v2-only field is read
- a v2 header with no compression and no sparse ranges behaves like v1
- a valid v1 header parses the fields the player needs
- a wrong magic is refused
- an unknown major version is refused
- blockEntry reads one table entry's first frame and compressed length, and refuses an entry past the buffer
- channelDataOffset overlapping a v2 file's compression/sparse tables is refused
- channelDataOffset overlapping the v1 fixed header is refused
- expandSparseFrame clips to the output capacity rather than overflowing it
- expandSparseFrame places each range at its absolute channel offset, leaving gaps alone
- more sparse ranges than the cap are clipped, not overflowed, and reported non-fatally
- sparse ranges land in the header in file order
- validateBlockRange accepts a well-formed entry and computes its decompressed length
- validateBlockRange refuses a block starting at or past the file's own frameCount
- validateBlockRange refuses a block table that does not strictly advance
- zlib compression parses cleanly, since a ROM decoder exists for it
- zstd compression is its own error, not a generic failure
GpioDmaPeripheral¶
- GpioDmaPeripheral 16 lanes double the frame (16-bit slots)
- GpioDmaPeripheral allocates a real host bus the driver can encode into
- GpioDmaPeripheral counts its bus in the driver's memory readout
- GpioDmaPeripheral frees its frame buffer as soon as the transfer starts
- GpioDmaPeripheral keeps one buffer even when doubleBuffer is asked
- GpioDmaPeripheral loopback refuses a receive pin the platform reserves
- GpioDmaPeripheral loopbackTxPin tracks the loopbackTest toggle
- GpioDmaPeripheral refuses a frame the heap cannot hold with the lights that fit
- GpioDmaPeripheral registers under the GPIO-DMA label
- GpioDmaPeripheral rejects more than 16 lanes
- GpioDmaPeripheral setup/release is repeatable
- GpioDmaPeripheral slices lanes and sizes the frame by the longest
Hub75Slots¶
I80Peripheral¶
- I80Peripheral allocates a real host bus the driver can encode into
- I80Peripheral bad pins → status error → recovery
- I80Peripheral counts the DMA buffers in its memory readout
- I80Peripheral drives any pin count; the bus rounds up around it
- I80Peripheral even split over the default 8 lanes
- I80Peripheral frame grows on RGBW preset
- I80Peripheral gives the host bus two distinct buffers when asked
- I80Peripheral hides pinExpander where the chip can't host it
- I80Peripheral idles with a named status when DC is unset, on every chip
- I80Peripheral loopbackRxPin tracks the loopbackTest toggle
- I80Peripheral loopbackTxPin tracks the loopbackTest toggle
- I80Peripheral on an LCD_CAM chip idles with a named status when WR is unset
- I80Peripheral refuses a WR/DC pin this chip package does not have
- I80Peripheral setup/release is repeatable
- I80Peripheral slices lanes and sizes the frame by the longest
- I80Peripheral tolerates a zero-light buffer
- I80Peripheral warns (does not idle) when a data pin is on clockPin/dcPin
- I80Peripheral with the empty default pins idles cleanly
- I80Peripheral: the DMA-fit gate is inert off the classic i80 (budget 0)
- I80Peripheral::frameFitsDmaBudget rejects only over a finite budget
- a bus pin wired to flash is refused, not routed
- a full-width bus still refuses a WR or DC pin wired to flash
- a given-up driver recovers when the bus is fixed
- a peripheral that routes its own pins is handed only the lanes it drives
- a persistently dead bus is given up on, so it cannot starve the device
- bus pin list is padded to the full bus width, in both modes
- pinExpander degrades to direct on a peripheral that can't host it, keeping the saved value
- shift register: 48 strands on 6 pins is still an 8-bit bus
- shift register: a timed-out transfer never gets its buffer re-encoded
- shift register: a transfer that never completes does not wedge the driver
- shift register: direct mode still drives one strand per pin
- shift register: driver clamps to pins x ledsPerPin, ignoring a larger layout
- shift register: driver refuses a latch that collides with a data lane
- shift register: frame is set by strand LENGTH and the fan-out, not by strand COUNT
- shift register: lanes = pins x 8
- shift register: latchPin colliding with a data pin is a config error
- shift register: the expander needs a latchPin
- shift register: the fan-out costs 8x the DMA frame
- shift register: the loopback frame has room for its closing latch word
- shift register: the loopback test frame is 8x the direct-mode frame
- shift register: toggling the expander live reconfigures cleanly
- streaming ring: a sliced encode is byte-identical to the whole-frame encode
- the i80 clock and DC defaults never land on a chip's flash pins
- LCD encoder 16-lane: RGBW row is 96 uint16 slots, all written
- LCD encoder 16-lane: high lane and byte-boundary transpose
- LCD encoder: GRB ordering via Correction
- LCD encoder: RGBW row is 96 slot bytes
- LCD encoder: SWAR 16-lane transpose equals the naive gather
- LCD encoder: SWAR transpose equals the naive gather for all patterns
- LCD encoder: empty mask emits all-zero slots
- LCD encoder: inactive lanes stay LOW regardless of wire content
- LCD encoder: one lane, MSB-first, 3 slots per bit
- LCD encoder: transpose across two lanes
- shift encoder: an exhausted strand stays dark while its pin-mate keeps rendering
- shift encoder: inactive strands idle LOW on every cycle
- shift encoder: latch pulses on the FIRST cycle of each slot (not the last)
- shift encoder: prefill + data-only == the whole-slot encode, byte for byte
- shift encoder: prefill + data-only agree on an exhausted strand
- shift encoder: start slot is HIGH and tail slot LOW across every cycle
- shift encoder: strand N rides the correct shift cycle ('595 MSB-first)
- shift encoder: strands on different pins share a cycle, same pin serialize
- shift encoder: the STRAND receives a correct WS2812 waveform (595 pipeline modelled)
- shift encoder: the packed transpose matches the reference at every pin count
- shift encoder: the strand idles LOW through the latch pad (the frame reset)
LayoutBase¶
ModifierBase¶
MoonI80Peripheral¶
- MoonI80Peripheral direct mode: clockPin is unrouted, so it cannot collide
- MoonI80Peripheral drives a valid config like its sibling
- MoonI80Peripheral is LCD_CAM-only — it does not claim the classic ESP32's I2S i80
- MoonI80Peripheral keeps the i80 bus rules: power-of-two bus, full-width loopback
- MoonI80Peripheral rejects a latchPin on WR
- MoonI80Peripheral shift mode requires a clockPin; direct mode does not
- MoonI80Peripheral shift mode: a data pin on clockPin (WR) is caught
- MoonI80 ring v2: coalesced EOFs (batched refill) are byte-identical to one-per-EOF
- MoonI80 ring: a failed ring build tears the bus down (no leaked ring)
- MoonI80 ring: a ragged frame tiles byte-identically (a strand ending mid-slice)
- MoonI80 ring: a recycled buffer produces the same bytes as a fresh one
- MoonI80 ring: a short last slice in a reused buffer has a clean pad (no stale ghost rows)
- MoonI80 ring: an EMPTY lane does not break uniformity (prefill-skip stays valid)
- MoonI80 ring: an exhausted RAGGED strand clocks zeros, on a fresh AND a recycled buffer
- MoonI80 ring: no slice writes past its rows (buffers are rows-only)
- MoonI80 ring: no-reuse frame clocks a clean LOW tail and stops deterministically
- MoonI80 ring: ringSnapshot control is hidden unless the ring is active
- MoonI80 ring: sliced encode tiles into a byte-identical whole frame
- MoonI80 ring: snapshot keeps the whole window when outCh > srcCh (RGBW correction on RGB source)
- MoonI80 ring: the PARALLEL snapshot's range-split is byte-identical to the whole-range serial
- MoonI80 ring: the encode reads a per-frame snapshot, not the live source
- MoonI80 ring: the no-reuse stopgap clocks a clean tail and stops on the drain counter
- MoonI80 ring: the windowed snapshot bias reads this driver's slice, not from light 0
MoonLivePalette¶
- a broken palette script leaves the last good palette, rather than going dark
- a scripted palette fills all sixteen entries
- an out-of-range entry index writes nothing at all
- entries the script does not write keep their previous color
- setPalEntry does nothing outside a palette script
- setPalEntryHSV reaches the same table, in the space a palette is reasoned in
MoonLiveParticles¶
- a particle call from a script with no pool does nothing
- a script sizes its own particle pool and is told what it got
- a script that fails to compile holds none of the last script's pool and trail
- a script that never asks for particles allocates none
- a script's particles die and free their slots for new ones
- a spark thrown upward comes back down
- asking for a pool while the frame is running allocates nothing
- colliding balls spread sideways instead of falling through each other
- disabling a scripted effect frees its particles
- editing a script to a different pool size resizes it
- emitting into a full pool stops rather than overwriting
- emitting twice from the same point does not repeat the same trajectories
- switching to a script that asks for no pool or trail frees the last script's
- the fountain example keeps emitting once its pool has cycled
- two scripted effects each get their own particles
MpegTs¶
- A frame too large for the buffer reports overflow instead of overrunning
- A keyframe carries the clock reference, a delta frame does not
- A segment opens with the program tables, then video
- Continuity counters advance per PID
- Continuity survives across the separate writers a stream is muxed with
- Discarding an overflowed frame can restore the continuity counters
- Every muxed packet is 188 bytes and starts with the sync byte
- Muxing many frames stays inside its buffers
- Only a frame's first packet flags the payload start
- The PAT points at the PMT, and the PMT at the video stream
- The frame's bytes survive the split across packets
Palette¶
- LivePalettes: a departing publisher cannot unpublish its successor
- LivePalettes: an unconditional clear detaches whoever owns the seam
- Palette: a degenerate (empty) gradient is all black, never out-of-bounds
- Palette: a mid-gradient sample interpolates between stops
- Palette: colorFromPalette index 0 reads entry 0; brightness scales
- Palette: gradient endpoints land on the first/last stop colors
- Palette: the index wraps at 255→0 (no out-of-range read)
- Palettes::active swaps the global palette on setActive
- Palettes::nearestForHue folds an out-of-range hue instead of overflowing
- Palettes::nearestForHue maps a color to the closest palette
- the WLED palette list and its count cover the scripted palettes too
ParlioPeripheral¶
- ParlioPeripheral 16-lane frame doubles the byte size (16-bit bus)
- ParlioPeripheral accepts 9..16 pins, rejects more than 16
- ParlioPeripheral accepts any lane count from 1 to 8
- ParlioPeripheral allocates a real host bus the driver can encode into
- ParlioPeripheral bad pins → status error → recovery
- ParlioPeripheral counts the DMA buffers in its memory readout
- ParlioPeripheral even split over 8 lanes
- ParlioPeripheral frame at the Parlio single-transfer ceiling (byte limit, channel-relative)
- ParlioPeripheral frame grows on RGBW preset
- ParlioPeripheral gives the host bus two distinct buffers when asked
- ParlioPeripheral loopbackRxPin tracks the loopbackTest toggle
- ParlioPeripheral loopbackTxPin tracks the loopbackTest toggle
- ParlioPeripheral setup/release is repeatable
- ParlioPeripheral slices lanes and sizes the frame by the longest
- ParlioPeripheral tick is crash-safe for every pin configuration
- ParlioPeripheral tolerates a zero-light buffer
- ParlioPeripheral with the empty default pins idles cleanly
RtpH264¶
- RtpH264 abandons an access unit whose sink refuses
- RtpH264 advances the sequence per packet across access units
- RtpH264 fragments an oversized NAL and the pieces reassemble
- RtpH264 marks the last packet of an access unit and no other
- RtpH264 reads three-byte and four-byte start codes alike
- RtpH264 sends a small NAL as one packet, payload unchanged
RtspSession¶
- RtspSession agrees a UDP transport and remembers the client's port
- RtspSession answers DESCRIBE with the SDP and its length
- RtspSession answers a verb it does not implement with 501
- RtspSession builds an SDP naming H.264 and the stream's geometry
- RtspSession echoes the request's CSeq
- RtspSession judges each Transport alternative whole
- RtspSession lists the verbs it implements
- RtspSession reads CSeq whatever its casing
- RtspSession refuses PLAY before SETUP
- RtspSession refuses a number that would overflow rather than wrapping it
- RtspSession refuses a transport it does not speak
- RtspSession walks OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN
draw¶
- a bar colors each cell by its distance from the origin
- a bar growing right lights exactly its length from the origin
- a bar growing up rises toward row zero
- a bar longer than the grid stops at the edge
- a bar never spills into the next light on a 1-channel fixture
- a bar of zero or negative length draws nothing
- a bar works on a single-row strip
- a filled rectangle colors by row
- a filled rectangle covers its whole area
- a rectangle outline draws its border and leaves the middle dark
- bars grow down and left from their origin
- thin rectangles stay single bars rather than doubling back
- Canvas addresses x fastest, then y, then z
- Canvas and the buffer+dims form write the same bytes
- Canvas gives a 2D layer a depth of one, not zero
- Canvas leaves the white channel of an RGBW light untouched
- Canvas pixel writes land where get reads them, and clip outside
- Canvas reports out-of-grid coordinates as unwritable
- EffectBase canvas reflects the layer it is attached to
- a circle far larger than the grid costs the grid, not the radius
- a circle larger than the coordinate type terminates
- a circle larger than the grid clips instead of overflowing
- a circle outline is hollow and sits at the radius
- a circle outline is symmetric about its center
- a filled circle colors by its row offset from the center
- a filled circle covers more cells than its outline
- a filled circle is solid from center to rim
- a negative radius draws nothing
- a zero-radius circle is a single pixel
- an anti-aliased line handles steep and reversed directions
- an anti-aliased line of zero length is a single point
- an anti-aliased line reaches both endpoints
- an anti-aliased line running off the grid clips instead of overflowing
- an anti-aliased line splits light without inventing it
- an anti-aliased line spreads a shallow diagonal over neighboring cells
- a non-wrapping scroll past the end clears the grid
- a scroll of zero steps changes nothing
- a strided wrap conserves every light, whatever the rotation
- a wrapping column scroll moves every channel of a wide fixture
- a wrapping column scroll rotates rows
- a wrapping scroll conserves every light
- a wrapping scroll the length of the axis leaves the grid unchanged
- scrolling a 1-channel fixture moves whole lights
- scrolling a degenerate axis does nothing
- scrolling down moves each row one step and frees the top row
- scrolling left moves content toward decreasing x
- scrolling right moves content toward increasing x and darkens the vacated edge
- scrolling up moves rows toward the top
- scrolling x on a 2D grid keeps rows separate
- scrolling z moves whole slices in a volume
- a circle SDF describes the same shape a radius test would
- coverage ramps across the edge rather than switching
- coverage with a zero-width edge is a hard threshold
- one distance yields a fill, an outline and a falloff
- sdBox is negative inside and grows with distance outside
- sdBox respects different half-extents per axis
- sdCircle returns a true distance in sub-pixel units
- sdCircleSq is negative inside, zero on the rim, positive outside
- sdSegment measures distance to the nearest point on the line
- sdSegment treats a zero-length segment as a point
- smin digs exactly a quarter of the blend radius, at any radius
- smin leaves distant shapes alone
- smin pulls two nearby shapes together below either alone
- smin with zero blend is a plain minimum
- a per-channel write never spills into the next light
- splat between two pixels splits the light by coverage
- splat clips at the grid edge without writing outside it
- splat conserves total brightness across sub-pixel positions
- splat is additive, so two points on one pixel brighten it
- splat on an exact pixel lights only that pixel
- splat saturates rather than wrapping to black
- splat spreads over eight neighbors in a volume
- splat weights follow the distance to each neighbor
- splat works on a 1D strand
- toPixel floors so a negative fraction lands in the pixel that contains it
- a 16-bit plane carried by a whole-cell flow arrives intact, so light is transported rather than smeared away
- a 16-bit trail plane decays at the same rate whatever the framerate
- a disc lights its interior and softens its edge
- a dithered value never wraps past full, so a bright light cannot flash black
- a flat field stretches across a volume's depth without z work
- a flow off the edge circulates under Wrap and carries the light out of the grid under Clamp
- a half-cell flow splits a 16-bit sample between the two cells it straddles, and loses none of it
- a light reached by index is the light reached by its coordinate
- a pixel is scaled, added to and blended in place
- a radial flow points away from its center, and inward when reversed
- a ring is hollow: lit on its band, dark at its center
- a ring's inner edge is antialiased, not cut
- a sphere fills a volume, so a cube gets a ball rather than a stack of discs
- a spiral is a radial flow with a turn added, so it both circles and escapes
- a thick line covers pixels a one-pixel line would miss
- a thick line stops at its endpoints instead of running along the infinite line
- a uniform field survives being advected, so a flow does not dim what it carries
- a volumetric field interpolates along z as well, so a coarse cube fills a fine one
- a zero-length thick line draws a dot rather than nothing
- advect carries the picture along the flow, one whole pixel at a time
- advect moves every slice of a volume, so a cube flows like a panel
- an upscaled plane keeps a uniform value, so a flat field does not gain texture
- an upscaled ramp stays monotonic, so a gradient does not gain steps or reversals
- an upscaled saddle stays inside the values it was given, so a field that dips does not light up
- blit16 narrows a wide plane to the canvas, and dithering carries the error a truncation would drop
- decay dims a plane by half over one half-life at a realistic frame time
- decay leaves a plane alone when no time has passed, and clears it after a long stall
- draw: a line partly off the grid clips cleanly
- draw: blur matches the reference carryover-seep on a 1D row
- draw: blur spreads in 2D and 3D and is safe at degenerate sizes
- draw: glyph renders upright and un-mirrored (the 'L' probe)
- draw: line draws a 2D diagonal, endpoints inclusive
- draw: line fills a 1D row inclusive of both endpoints
- draw: line shorten pulls the far endpoint back toward the start
- draw: line spans a 3D cube diagonal
- draw: pixel writes in-bounds and clips out-of-bounds
- draw::sprite blits the requested frame with transparent holes
- draw::sprite clips at every edge and survives bad frame and palette indices
- draw::sprite mirrors horizontally without moving the sprite
- scrolling along y moves every column, and along z every cell of the volume
- temporal dithering resolves neighbors that a single frame cannot
- the edge rule decides whether a flow loops the grid or leaves it
- the ordered pattern differs per z, so a volume does not repeat one texture
- the wind blows every point the same way
- truncating a slow gradient bands it, and dithering restores its true mean
- two overlapping discs brighten where they meet, because light adds
- upscale16 declines rather than writing when the caller's tap table is too small
golden_frame¶
light_types¶
- Coord3D % and / guard a zero or degenerate axis
- Coord3D arithmetic is per-axis
- Coord3D equality
- Coord3D modulo and divide fold per axis
particles¶
- Each sprite follows its own frequency band
- Loud music moves the sprites faster than quiet music
- Silence stands the sprites still
- The spectrum is spread over the live sprites, not the pool capacity
- Without an audio source the sprites keep moving normally
- a bounce loses the same speed in either direction
- a bouncing particle loses energy rather than gaining it
- a burst sends particles in different directions
- a burst stops early rather than overflowing the pool
- a dying particle renders dimmer than a fresh one
- a force pushes along both axes
- a fresh pool is valid and entirely dead
- a particle bounces off a wall and reverses direction
- a particle can outlive the old 8-bit ceiling
- a particle lands in the same place at any framerate
- a particle leaving one edge re-enters the opposite one
- a particle spawned with no life still appears for one frame
- a particle with no velocity stays where it was put
- a pool with no storage reports itself invalid
- a pool without an accumulator ignores sub-unit forces
- a pool without size storage still renders every particle
- a rate of zero makes the pool immortal
- a sized particle draws a disc rather than a point
- a spray emits the requested number of particles in varied directions
- a sub-unit force does nothing on its first frame, then accumulates
- a sub-unit force eventually moves a particle
- a sub-unit force works in both directions
- aging kills a particle and frees its slot for reuse
- aging never wraps past zero
- an angled burst emits the requested number of particles
- an attractor does not fling a particle sitting on top of it
- an attractor pulls a particle toward it
- an empty pool renders nothing
- an exact multiple of the axis lands on the edge it approached from
- audio-reactive stepping moves sprites by their own band, and not at all in silence
- collisions with a zero radius do nothing
- dead particles are skipped by every pass
- drag slows a particle without reversing it
- drag slows left and right movers by the same amount
- gravity accelerates: each frame covers more ground than the last
- gravity still acts when frames are faster than the clock can resolve
- one second of motion is the same amount of motion however fast the device renders
- overlapping particles separate instead of jittering forever
- particles already moving apart are not kicked again
- particles outside the grid can be killed instead of bounced
- particles outside the grid draw nothing and do not crash
- particles too far apart do not interact
- rendering lights the grid where particles are, and nowhere else
- repeated drag brings a particle to rest
- restitution sets how much speed a bounce keeps
- spawning brings a particle to life and consumes a slot
- spawning into a full pool fails rather than overwriting
- spreadLane gives every slot its own lane, for any count
- spreadLane interleaves rather than striding in order
- step moves a particle by its velocity
- the same seed produces the same burst
- the same seed produces the same spray
- two particles approaching head-on bounce apart
- wrapping can be enabled per axis
- wrapping leaves a particle already inside the grid untouched
- wrapping puts a particle at an exact position, however far out it started
- zero drag leaves velocity untouched
polar¶
- a refused table leaves nothing allocated behind
- a single-pixel grid is its own center
- a wide panel fills to its edges instead of banding in a circle inside it
- an effect can bind the polar controls before it has a fixture
- an empty grid reports not ready instead of building a table of nothing
- angles point the way the geometry says
- at one light deep every projection is the flat address, so no panel changes
- cylindrical carries depth separately, so every slice reads the same address
- only spherical pays for the third table
- opposite sides of the center face opposite ways
- preparing the same geometry again costs nothing
- radial measures distance from the center of the volume, so the field reads as shells
- releasing the table gives the memory back and the next prepare rebuilds it
- resizing the grid rebuilds the table for the new geometry
- spherical adds an elevation, so a sphere maps evenly instead of pinching
- switching precision live does not hold both tables
- switching projection live rebuilds the table and frees what the new one does not need
- the 8-bit table is the same address at 256 steps, which is what it costs half the memory for
- the index form and the coordinate form address the same pixel
- the radius runs from nothing at the center to full scale at the furthest corner
- the table gives the same angle a radial effect would compute per pixel
- the table is refused rather than taking the last of a small heap
- the tables are the module's memory, freed with it
- the volumetric index matches the buffer's own ordering
- an effect still renders when the polar table cannot be built
- the 16-bit table renders exactly what computing the address per pixel renders
- the 8-bit table costs a quantized angle, and nothing else
- the computed fallback keeps the mapping the table would have used
shader¶
- a cosine palette produces varied colors around its ramp
- a full turn returns a point to where it started
- a longer lens magnifies
- a point at or behind the viewer does not project
- a point on the view axis projects to the centre
- a polygon is negative inside and positive outside
- a polygon with too few sides falls back to a circle
- clamp holds a value inside its range
- depth fade dims with distance and stops at the far plane
- fract keeps only the fractional part, and wraps
- length measures distance from the origin
- mirror folds space about the origin
- mix interpolates between its endpoints
- mixing colors interpolates each channel
- opRound grows a shape outward
- opShell hollows a shape into an outline
- projecting a point that lands outside the coordinate range is rejected
- projection shrinks a point in proportion to its depth
- repeat tiles space into cells
- repeat with a zero cell leaves the coordinate alone
- rotate turns a point about the origin
- rounding a box pulls its corner in
- smoothstep clamps outside its edges rather than running away
- smoothstep is flat at both ends, steep in the middle
- smoothstep ramps smoothly between its edges
- step is a hard threshold at the edge
- the SDF operators combine shapes
- the shader runner gives each pixel a different coordinate
- the shader runner visits every pixel
- uv centres the grid and scales by the short side
- uv keeps a circle circular on a wide panel
Light · Layouts¶
CarLightsLayout¶
GridBlacksLayout¶
- GridBlacks black columns emit gap pixels at their true position
- GridBlacks black columns hold on the true column under serpentine
- GridBlacks black run tolerates out-of-range bounds
- GridBlacks with no black run emits zero gaps
GridLayout¶
- scenario_GridLayout_resize
- scenario_Layouts_resize_reallocates_live
- GridLayout 1x1x1 edge case
- GridLayout 2x2x2 produces 8 coords with z
- GridLayout 4x4x1 produces 16 coords in row-major order
- GridLayout serpentine reverses x on odd rows
- Layouts with one layout
- Layouts with two layouts offsets physical indices
MoonLiveLayout¶
- a broken script fixed in place compiles, without being renamed
- a broken script leaves an empty fixture rather than taking the pipeline down
- a disabled scripted layout stops reporting the memory it freed
- a disabled scripted module publishes no controls bound to freed memory
- a layout answers count and coordinates every time it is asked
- a layout that changes size mid-build cannot overrun the mapping
- a layout that starts empty still compiles the first script it is given
- a layout whose script is missing reports it without retrying forever
- a loop counter survives the body that uses it
- a nested loop lays out a full grid, on every target's register budget
- a script can place a shape no rectangular layout can express
- a script name at the accepted length survives the control it is stored in
- a script name cannot escape the script folder
- a script places lights wherever it likes, which is the point of scripting one
- a script that disappears takes its lights with it
- a scripted control keeps its live value when the script is edited
- a scripted layout allocates nothing, like every other layout
- a scripted layout reports every heap byte it holds, compiled or not
- a serpentine layout places every light exactly once
- a stray character in a for header is rejected, not spun on
- a subtraction feeding a loop bound produces the whole value
- backlight.mll: a gap crossing the top-left corner skips its lights on both sides of it
- backlight.mll: a gap ending at the last light leaves the walk up to it
- backlight.mll: a gap removes a run of lights and leaves the rest where they were
- backlight.mll: a position past the strand's length wraps round it, reversed too
- backlight.mll: a strand coarser than the wall still spans its edges
- backlight.mll: each edge contributes its own count, each corner once
- backlight.mll: each wiring starts at its corner and runs its own way round
- backlight.mll: offset slides the strand along itself, whichever way it runs
- editing a script's text recompiles it, without renaming the file
- editing the script changes the fixture
- naming a different script through the control actually swaps the program
- preparing an unchanged script installs no new program
- the count and the coordinates always agree, because one script produces both
- the default script lays out a grid, one light per cell
- the light count is known before any coordinate is asked for
- the loader accepts any role extension, and nothing else
- the scripts the documentation shows all compile
- two threads can run scripts at once without stealing each other's sink
PanelLayout¶
PanelsLayout¶
RingLayout¶
- a GridBlacks without dark columns places what a Grid places
- a Panels tile is a Panel layout shifted to its place in the grid
- a Ring layout without a center places itself by its own size, and one with a center is shifted there
- a later part continues the numbering and stops at the light cap, the safe-mode boot's 1024
- a tube is a Single Column layout's column at the tube's x
- car lights are Ring layouts' rings and two strips, each with a quarter arc and an upright
- the 241 disc is nine Ring layout rings about one center
Rings241Layout¶
- Rings241 emits 241 consecutively indexed lights in either wiring order
- Rings241 wires from the center outward by default
- angleFirst moves the first light around the ring, and 0 leaves the disc where it was
- outside in puts light 0 on the outer ring and the center LED last, rings unchanged inside
SingleColumnLayout¶
- A 10-high column emits the same coordinates as a 1x10x1 grid
- SingleColumnLayout of height 10 emits ten consecutively indexed lights
- reversed order walks the column from the far end while keeping indices contiguous
- starting Y moves the column in space without renumbering its lights
SphereLayout¶
- SphereLayout default radius
- SphereLayout emits sequential physical indices
- SphereLayout is hollow — no centre or interior points
- SphereLayout lightCount matches the iterator
- SphereLayout radius 1 is the 18-point base shell
- SphereLayout shell is centre-symmetric
TubesLayout¶
WheelLayout¶
- WheelLayout coordinates are non-negative (centre-shifted into address space)
- WheelLayout different spoke counts give different layouts
- WheelLayout indices are dense [0, count)
- WheelLayout lightCount = spokes * ledsPerSpoke and matches the iterator
Light · Modifiers¶
BlockModifier¶
- BlockModifier folds to Chebyshev distance from the box center
- BlockModifier logical box is {1, maxDistance + 1, 1}
- BlockModifier survives degenerate grids
- BlockModifier uses max(|dx|,|dy|) so rings are axis-aligned squares
CheckerboardModifier¶
- CheckerboardModifier does not resize the logical box
- CheckerboardModifier invert flips the kept squares
- CheckerboardModifier size 1 keeps even-parity, drops odd
- CheckerboardModifier size 2 groups into squares
CircleModifier¶
- CircleModifier folds the centre to the origin and collapses to one column
- CircleModifier logical size is a radius-tall single column
- CircleModifier maps equidistant lights to the same ring
- CircleModifier survives degenerate grids
MirrorModifier¶
- MirrorModifier halves each mirrored axis, rounding up
- MirrorModifier handles degenerate grids
- MirrorModifier leaves a disabled axis untouched
- MirrorModifier leaves the centre column unpaired on an odd axis
- MirrorModifier maps a coord and its mirror to the same logical position
MoonLiveModifier¶
- a broken script leaves the pattern alone rather than taking the layer down
- a coordinate beyond 255 is scripted like any other, in and out
- a for loop runs its body once per step
- a loop in an effect script paints every light it walks
- a loop over an empty range runs its body no times
- a scaled mirror, the transform this binding exists to make possible
- a script can offset a coordinate, the scroll a modifier usually hard-codes
- a script can read the grid extent it is folding within
- a script can swap axes, which is a transform no control could express
- a script computes with the usual precedence, so a transform means what it reads like
- a script that computes a position outside the grid leaves lights mapped
- a scripted layout that changes its light count under a scripted modifier rebuilds the mapping to the new count
- a scripted modifier mirrors the pattern, the way a hand-written one would
- a subtraction produces the whole value, not just its low byte
- editing a script asks the layer to rebuild its mapping
- editing the script changes the transform without a rebuild of the firmware
- folding a wall's worth of lights compiles the script once, not once per light
- loops nest, which is what placing a grid of lights needs
- print reports a value without changing what the script computes
- re-preparing with an unchanged script does not ask for another rebuild
- the default script mirrors within the grid it is given, not a fixed 255
- the script reads the light's own position, not a fixed value
MultiplyModifier¶
- MultiplyModifier advertises D3 dimensions
- MultiplyModifier clamps a multiplier above the axis extent
- MultiplyModifier default logical size = mirror-XY fold
- MultiplyModifier drops the leftover strip on a non-divisible extent
- MultiplyModifier four corners fold to logical (0,0)
- MultiplyModifier identity when all multipliers are 1
- MultiplyModifier logical size on Z
- MultiplyModifier mirrorX folds reflected columns together
- MultiplyModifier multiplyZ on 2D does nothing
- MultiplyModifier tiles without mirror (repeat, not fold)
PinwheelModifier¶
- PinwheelModifier 1D maps the sweep across distinct petals on Y
- PinwheelModifier 1D reshape puts petals on Y (1 x petals x 1)
- PinwheelModifier 2D reshape puts petals on X, radius on Y
RandomMapModifier¶
- RandomMapModifier does not resize the logical box
- RandomMapModifier is a bijection (every pixel mapped once)
- RandomMapModifier is deterministic for a fixed generation
- RandomMapModifier rebuilds on a grid resize
- RandomMapModifier reshuffle changes the mapping, stays a bijection
- RandomMapModifier tick() reshuffles on a beat (bpm 60 ≈ 1/s)
- RandomMapModifier tick() with bpm 0 never reshuffles (frozen)
- RandomMapModifier tolerates an empty box
RegionModifier¶
- RegionModifier abutting regions tile exactly
- RegionModifier default region is the full box
- RegionModifier folds physical to region-local and rejects outside
- RegionModifier fully off-screen window renders nothing
- RegionModifier half region is exact (half-open end)
- RegionModifier handles degenerate axes
- RegionModifier never produces a zero-width region
- RegionModifier rounding on a small panel (floor start, ceil end)
- RegionModifier slides a window off-screen without rescaling
- RegionModifier window wider than the box
RippleXZModifier¶
- RippleXZModifier collapses Z, and both X and Z together
- RippleXZModifier default collapses the X axis to one plane
- RippleXZModifier handles a degenerate 0x0x0 box
- RippleXZModifier with shrink off is the identity
RotateModifier¶
- RotateModifier advertises a live (per-frame) modifier
- RotateModifier at angle 0 is identity
- RotateModifier leaves z untouched
- RotateModifier tolerates an empty box
TransposeModifier¶
- TransposeModifier default swaps x and y
- TransposeModifier handles degenerate boxes
- TransposeModifier inverse flips within the transposed box
- TransposeModifier swaps the selected axis pair
Light · Supporting¶
Drivers¶
- scenario_Drivers_output_and_brightness
- scenario_Palettes_a_script_drives_the_effects
- A fixture-profile list edit re-resolves each driver's correction without re-preparing it
- Drivers accepts only driver-role children in the add picker
- Drivers gives a scripted palette's color from its live entries
- Drivers is the light output core reads, from prepare until release
- Drivers: a localBrightness change re-scales the driver's correction LUT
- Drivers: relay pins and double buffering are expert settings, and the loopback test a developer one
- Drivers: three sibling NetworkSendDrivers each own their buffer, and removing the middle one leaves the others driving
- Drivers::on gates the correction LUT without clobbering brightness
- Drivers::tick() skips disabled child drivers
- FixtureProfiles library is a non-deletable singleton
- a brightness change does not rescan the palette scripts; a palette file written through the API does
- a brightness change re-bakes the correction without re-laying the driver's frame; a channel-count change does
- a built-in palette clears the palette script's status and keeps another's
- a probe Drivers reads the scripted-palette list without rescanning it
- a saved script palette's name resolves when the file is applied, before setup
- a typo in the relay list releases the relays it used to hold
- clearing the palette script leaves the Drivers card without an error
- safe mode darkens the lights, and holds the output drivers after four failed boots
- the lights rise to their brightness after a device boot, and a host process starts at it
- the relay opens at brightness 0 and closes again when brightness returns
- a factory palette keeps its number whether the device holds it or not
- a palette added while running becomes selectable without a reboot
- a palette downloaded to the factory directory is offered by the picker
- a quote in a live palette's name is escaped, so the options stay parseable
- a scripted palette chosen by name survives a script added before it
- editing a factory palette leaves one entry, not two
- the StadBeest fire palette is number 64, the one its autopilot picks
- the palette saves as its name, built in or scripted
- render-split: a detached ParallelLedDriver's peripheral swap does not disturb the live split
- render-split: a single-layer multicore config stays engaged across many ticks (no flap)
- render-split: a wedged worker degrades to single-core instead of hanging the render loop
- render-split: deleting a driver WHILE core 1 is inside its tick() is safe (no use-after-free)
- render-split: live disengage stops the worker when the last driver leaves
- render-split: multicore off → drivers tick inline on the render core (the proven path)
- render-split: multicore on → every driver ticks on the worker, never on a torn frame
- render-split: mutating a LAYOUT while core 1 runs is safe via the quiesce-render hook
- render-split: no driver → the split does not engage (nothing to move)
- render-split: reordering children (moveChildTo) while core 1 runs waits for the worker
- render-split: the stall warning clears once the worker is healthy again
- render-split: toggling multicore live engages and disengages the worker
Effects¶
- scenario_Effects_pipeline_builds_and_renders
- scenario_Effects_swap_while_running
- scenario_Layers_stack_and_blend_live
- Disabling a parent Layer cascades release to its effects (effectivelyEnabled)
- Drivers allocates the output buffer only when compositing or mapping is needed
- Drivers composites two enabled Layers into one output buffer
- Drivers composition drops to single layer when one is disabled
- Effects with one Layer produces the same output as a bare Layer
- Effects with two Effects: each child Layer's tick runs and writes its buffer
- Effects::activeLayer returns first enabled child, or nullptr when empty
- Effects::activeLayer returns nullptr when no child has role Layer
- Effects::firstEnabledLayer returns first enabled child, nullptr when all disabled
FixtureProfilesModule¶
- A built-in added by an update reaches a device with saved profiles
- A built-in never shadows a saved profile of the same name
- A driver allocates the 16-bit table only for a profile with fine roles
- A driver keeps its 16-bit table when its profile goes back to 8 bits
- A driver referencing a missing profile falls back to the default built-in
- A driver's fixture Select is populated from the library, and picking one resolves
- A driver's fixture is set by the profile's name
- A newly-added profile becomes selectable on a driver
- A profile cannot take a name another profile has
- A saved profile without an id gets its own, and its name is not seeded twice
- Built-in RGBCCT has white (WarmWhite counts) and resolves 5 channels
- Built-in WRGB resolves to W,R,G,B offsets
- Built-in moving head (BeeEyes-15) resolves at full width with the RGB block placed
- Color roles keep their indices after the vocabulary grew (append-only)
- Editing a profile flows to every driver referencing it (consistency)
- FixtureProfiles add / edit / resolve a custom profile
- FixtureProfiles seeds the curated built-ins as locked rows
- FixtureProfiles serializes the channel-role options ONCE, rows reference by optionsRef
- FixtureProfiles: a built-in is protected; a missing id does not resolve
- FixtureProfiles: a custom profile round-trips through persistence with its roles
- FixtureProfiles: a malformed ch
field name is rejected, not coerced to 0 - FixtureProfiles: a profile name with a quote or backslash survives persistence
- FixtureProfiles: an out-of-range persisted role clamps to a default on restore
- FixtureProfiles: delete + reorder keep ids stable
- FixtureProfiles: growing channels preserves existing role picks
- The WS2816's built-in sends green, red and blue, each as two bytes
- The built-in profiles read in the shipped order, whatever order they were saved in
- The fine roles come after every earlier role
- whiteMode is hidden for a no-white profile, shown for an RGBW one
Layer¶
- scenario_perf_full
- scenario_perf_light
- a feedback trail compounds at the same rate however fast the device renders
- every effect behaves the same at any framerate
- fireworks launch the same number of shells per second at any framerate
- D1 effect on 3D grid: columns and z-slices are identical (Layer::extrude)
- D2 effect on 3D grid: z-slices are identical (Layer::extrude)
- D2 effect on 3D layer: Fire extruded across z (heat sized to w*h)
- D2 effect on 3D layer: Particles extruded across z (trail sized to whcpl)
- D2 effect on 3D layer: Spiral extruded across z
- D3 effect on 1D layer: NoiseEffect produces a valid 1D strip
- D3 effect on 1D layer: PlasmaEffect produces a valid 1D strip
- D3 effect on 2D layer: NoiseEffect produces a valid 2D image
- D3 effect on 2D layer: PlasmaEffect produces a valid 2D image
- Layer coalesces rebuilds from two dynamic modifiers
- Layer live pass: Rotate remaps the buffer per frame
- Layer live pass: a disabled live modifier does not run
- Layer live pass: skipped when no modifier is live
- Layer chains Region then Multiply (both fold)
- Layer modifier order matters (A∘B ≠ B∘A)
- Layer skips a disabled modifier in the chain
- Layer swaps a box-resizing modifier for one that does not, and back, resizing the mapping each time
- LavaLampEffect animates over a 100ms gap
- MetaballsEffect animates over a 100ms gap (desktop dt ≈ 0..1ms)
- Replacing an effect at runtime: new effect still animates
- SpiralEffect animates over a 100ms gap
- Layer: GridBlacks black columns build a gap-dropping LUT
- Layer: RegionModifier carves the logical box to a sub-region
- Layer: an all-black GridBlacks builds an empty LUT without crashing
- Layer: an effect renders through a gapped GridBlacks layer
- Layer: dense grid stays on the identity path (no LUT)
- Layer: high fan-out Multiply builds a full, in-range LUT (no overflow)
- Layer: serpentine grid leaves the identity path and builds a LUT
- Layer: sparse sphere builds a box->driver LUT, no out-of-range index
- Layer: sphere + mirror maps into driver-index space
- FireEffect on 0,0,0 grid
- GEQ3DEffect on 0,0,0 grid
- GameOfLifeEffect on 0,0,0 grid
- LavaLampEffect on 0,0,0 grid
- MetaballsEffect on 0,0,0 grid
- NoiseEffect on 0,0,0 grid
- PaintBrushEffect on 0,0,0 grid
- ParticlesEffect on 0,0,0 grid
- PlasmaEffect on 0,0,0 grid
- RainbowEffect on 0,0,0 grid
- RingsEffect on 0,0,0 grid
- RipplesEffect on 0,0,0 grid
- SpiralEffect on 0,0,0 grid
- a layer refuses a zero channel count
- a live modifier is skipped on an empty grid without crashing
- modifiers keep ticking while the grid is empty
Layouts¶
- Disabling the Layouts container reports zero lights and an empty iteration
- Layouts skips disabled children and shifts indices
- safe mode places the first 1024 lights of a larger layout
- Layouts add multiple layouts of different types
- Layouts add one layout
- Layouts remove a layout
- Layouts replace a layout with another type
- Toggle a single layout off then on: pipeline survives and renders again
Other¶
OscillatorBank¶
PolarLut¶
Scripts · Checks¶
check_clang_query¶
- Clang query check
check_clang_tidy¶
- Clang tidy check
check_code¶
- Code check
- a blocking call on the render path is a row per site
- a build from before the launcher is rebuilt clean once
- a build running another launcher is told to reconfigure without a clean rebuild
- a clone counts under both of its files and names the other
- a commit account shows what it added and what it saved
- a constant written as a macro is a finding and a macro the preprocessor tests is a flag
- a function over both limits counts once per rule
- a hand buffer in light is a finding and a factory or a comment is not
- a header must guard with pragma once and never say using namespace
- a large file is a finding and a vendored one is not
- a large frame is a row per function and only ours count
- a measurement that reads nothing is a failure not a clean report
- a module type named in the ui is a finding and a container is not
- a mutable static in core or light is a finding and a constant or function is not
- a nothrow array new in light is a hand buffer too
- a type named beyond its own files spreads onto its header
- dynamic cast and typeid are findings in firmware and a comment saying them is not
- every rule the coding standards say check code checks is a rule it reports
- silence with the warning on is a measured zero and without it unmeasured
- the baseline is read from the last commit and a report never committed is a first run
- the counts the ratchet reads back are the counts the run recorded
- the launcher keeps what the compiler said beside the object
- the ratchet refuses a rise per rule and in total over the union
- the tree is read from every files saved findings after an incremental build
- the two boundaries are counted where the architecture draws them
- the type double is a finding in firmware and a word in a string or a comment is not
- two overloads with large frames stay two findings
check_codeql¶
- Codeql check
check_devices¶
- Devices check
- a child module without a type is refused
- a field the catalog does not know is refused
- a flash baud outside the standard rates is refused
- a name longer than the device keeps is refused
- a name that is no string is reported rather than crashing the gate
- a patch other than replace is refused
- a supported capability no module drives is refused
- a well formed entry meets the schema
- an entry missing its state is refused
- the gate fails on a firmware the registry lacks which no schema can know
- the gate fails on a schema violation
- the gate passes a well formed entry
- the gate refuses a baud written as a float which esptool would misread
- the schema is valid json schema and the repo catalog meets it
check_docgen¶
- Docgen check
- a catalog card leads with its picture and a moving one is a gif
- a class body nested type still reports
- a class comment past ten lines is flagged
- a code comment run past one line is flagged
- a colon or semicolon does not end a sentence
- a continuation that does not start lowercase is still a wrap
- a control guide repeating its line is reported and one adding to it is not
- a control is explained on its member never in the module guide
- a control line too wide for its card is reported tighter on a visual catalog
- a control stating its default or range is reported
- a cpp file is held to the comment rules
- a cpp is asked for no member docs
- a cpp is asked for no member docs of either kind
- a cpp may open with a plain comment but must open with one
- a declaration with one doc line a banner or a body note is clean
- a documented public member is accepted
- a fenced block does not count toward the section cap
- a fenced block is not read as wrapped prose
- a file lead written in paragraphs stays exempt
- a file level code comment is exempt
- a file with no leading comment exempts nothing
- a friend declaration is not a member
- a function carrying an attribute macro is still seen
- a function local type is exempt
- a hard wrap blocks in an implementation file too
- a header a build generates is not counted
- a header finding blocks and an implementation one warns
- a header of free functions leads with its own block
- a header opening with a slash slash block is flagged
- a header with no lead comment is flagged
- a heading inside a fenced example is no section
- a lead is measured as a block so chopping does not satisfy it
- a list is structure rather than an over long run
- a local struct in an allman function is not a member
- a long code comment line is flagged
- a long doc sentence is flagged
- a member after a function local type is still seen
- a member comment past one line is flagged
- a module off the catalogs has no card to measure
- a multiline struct inside a free function is exempt
- a namespace level struct is still public
- a note on a declaration is reported whatever its access
- a note on a file scope declaration is reported and an implementation file is not measured
- a one line code comment is accepted
- a one line member comment is accepted
- a paragraph over several lines is one finding at its start
- a paragraph too long for one line is cut not split
- a private member needs no doc
- a private nested type stays exempt
- a process no runner verifies is listed as information and is no finding
- a process over its word caps is a warning on its stage file
- a process within its word caps is silent
- a provenance marker above the lead is not the lead
- a public member after a nested type is still seen
- a real member beside an inline body is still seen
- a run flush against a declaration is its comment not the file lead
- a single class header leads with its class comment
- a stage file the generator refuses is an error rather than a traceback
- a statement inside a function body is not a member
- a struct inside a free function is exempt
- a tag emoji groups at least two modules or scripts
- a technical does not exempt a member comment
- a technical on a member is flagged
- a technical on a struct is accepted
- a technical section past ten lines is flagged
- a template line and a provenance marker add no second or false finding
- a templated member is not asked for a second doc
- a ten line class comment is accepted
- a test hook needs no doc
- a wrapped sentence in a class comment is a finding
- an appendix section nothing refers to is a finding
- an indented block keeps its layout
- an undivided technical appendix is flagged
- an undocumented public member is flagged
- an xref in the file reaches a section
- an xref naming no heading is flagged
- an xref that cannot render is reported
- an xref to a real heading resolves
- every module and every control has the line the app shows
- one comment cap holds in both kinds of file
- one rule set applies to both kinds of file
- prose after a closed fence is still checked
- several short sentences on one line are not flagged
- structure is not read as a wrapped sentence
- the app help fits a glance per control per section and in total
- the appendix is measured from technical to the end of its run
- the class introducing a catalog is no card and needs no picture
- the file comment is exempt but nothing below it is
- the headers are actually scanned
- the line length cap warns even in a header
- the ratchet catches a rule the baseline never had
- the ratchet refuses a rule that rose against the committed counts
- the ratchet refuses an appendix that grew
- the scan covers cpp as well as headers
- the scan includes tests
- the scan reaches beyond src
- two sentences that fit one line are a split paragraph
- two short technical sections are accepted
- without llvm the help freshness is skipped and the report is left as it is
check_esp32_built¶
- Esp32 built check
- a build reading the ui header depends on the ui files
- a build without the ui header gains nothing
- a recipe without the ui list stops the check
- the sources a firmware reads include the ui files its header embeds
- the ui inputs are the files the header is generated from
check_firmwares¶
- Firmwares check
check_footprint¶
- Footprint check
check_module¶
- Module check
check_nonblocking¶
- Nonblocking check
check_prose¶
- Prose check
- a vague closer is a list ending and not a path or a comparison
- the link text tone and vague closer rules fire on their words and not on a named link
- the whole tree count ratchets per rule and in total against the committed counts
check_sanitizers¶
- Sanitizers check
check_specs¶
- Specs check
- a registry link into a generated catalog page resolves its anchors
- the registry page check reads the registry and names a missing anchor
check_taglines¶
- Taglines check
Scripts · Python tests¶
build_esp32_cache¶
- a plain build after a versioned one reconfigures
build_esp32_s31¶
- ci target is the firmwares declared chip
- every firmware chip has a family label
- s31 firmware entry is well formed
- s31 gets ethernet not MM NO ETH
- s31 is a preview target
build_esp32_venv_select¶
- falls back to newest when no version match
- newest python minor wins within a version
- none when no venvs
- picks matching version even when other is newer
- picks matching version for the older idf
- version prefix does not collide across minor
check_findings_tools¶
- a critical alert is an error and a note a warning
- a firmware check replaces its own verdict and keeps the other firmwares
- clang query rows become findings and a clean run writes an empty record
- every check writes its findings except those that find nothing of their own
clip_posters¶
- a clip takes the poster beside its source and one that names a poster keeps it
- every tracked clip has its poster beside it
- the deck gif is cropped to the deck in whole pixels and scaled to readme width
compute_version¶
- a build after a release carries the next minor
- channel for tag centralizes the mapping
- core strips prerelease and build
- latest is core dev n when library json is ahead
- no tag fallback counts from root
- stable is core
- stable rc tag keeps prerelease
findings_sarif¶
- a findings record becomes one sarif run with its rules and located results
- no findings record is a failed export not a clean one
flash_baud¶
- default is fast
- exact model with optdown is slow
- exact model without flashbaud gets fast default not sibling
- firmware only falls back to lowest sibling
- unknown firmware is fast default
- unknown model falls through to firmware
flash_last_port¶
- mac match is case insensitive
- no mac is a noop
- s31 eui64 mac alias still links
- sets last port on matching mac
- strips stale port from swapped out board
- unknown mac is a noop
flash_split¶
- every run inside one commit compares against the last commits measurement
- nm counts what the image carries and keeps names whole
- the commit line names each firmware in kilobytes
- the parts sum to the net and a rename reads as removed plus new
fseq_play¶
- a file that is not fseq is refused
- a frame becomes ddp packets with the push on the last
- a written file reads back as uncompressed v2
- frames follow the header at the data offset
- serpentine wiring reverses every other row
- the sweep marks the logical corner in wiring order
gen_help¶
- a base classs template method and its helpers publish the controls a driver shows
- a block tag is the help field of its name and runs to the blank line
- a card tag names the picture then its alt text
- a catalog intro shows its picture above the line and its clips side by side below the summary
- a catalog row naming a commons file shows it linked to its file page
- a catalog whose page is gone is generated from the help
- a class without a comment has no line
- a control with no comment anywhere has an empty line
- a control without a member takes the comment above its add line
- a control without a member takes the comment behind its add line
- a controls comment is its line and its guide line its more
- a controls line is the comment on the member it publishes
- a lead may name several clips each with its title
- a link outside the docs points at the file on github
- a link reads the type as the app names it without its role noun
- a link to the same page becomes its anchor
- a modifiers kind is dynamic animated or static as its overrides say
- a module reads as plain markdown under its deck
- a modules tests link opens its section in the test inventory
- a picture is named by its file and found under the assets
- a ref anywhere in the help becomes the link its class name resolves to
- a ref to a module links its card and to a class its header under its own name
- a source file outside the modules credits by its attribution line
- a written catalog page is the intro its deck follows and its intro class is no card
- an absolute link or picture stays as written
- an attribution names catalog entries and keeps an unlisted name as written
- card names the image and stays out of the text
- controls defined in the cpp beside the header and through a local add lambda count
- each guide section is help with its heading as title
- every attribution catalog row is read
- every attribution entry has a kind the deck filters by
- technical is for developers and stays out of the help
- the attribution catalog shows avatars and the modules that name each entry
- the attribution table is wrapped in a deck the page never writes
- the docs strip the same role nouns the app does
- the first paragraph is the line and the rest of the lead the summary
- the module guide stays general even when a section names a control
- the role nouns match the app and the firmware
gen_moonwalk¶
- a call derives nothing since its result is not in its action
- a call encodes its values keeps a placeholder and refuses a missing path parameter
- a call gets its method its path with the parameters filled in and its entry on the rest api page
- a call names an operation of the rest api or it is refused
- a choice naming an option the input lacks is refused
- a choice naming an undeclared input is refused
- a choice runs a step for one option of a declared input
- a choice shows once per section and names its option in the plain text
- a control lists the steps that set it or expect its value
- a control named on must be quoted or it is refused
- a dependency on a missing process is refused
- a detail over its limit is a warning rather than a refusal
- a file that does not parse is reported alone
- a file that is not yaml names itself
- a key written twice is refused rather than the last kept
- a link in a process prose to a missing page is refused
- a link leaving docs names a repo file that exists
- a link to a process or step on the moonwalk page names one that exists
- a link to an anchor its page lacks is refused and a heading is one
- a misspelled expectation key is refused rather than never checked
- a pages anchors are its headings explicit ids and html ids but not code in a fence
- a process entry that is not a mapping is reported not crashed on
- a process id is unique across stages
- a process no runner performs is reported and one with a script a set or a condition is not
- a process with a choice is measured per reader rather than as the sum of its options
- a process with more steps than a reader follows is a warning to fold them
- a read step names an anchor its page holds
- a read step naming a section the page lacks is refused
- a repo path in a run step exists unless a build makes it
- a repo process repeats no run of another
- a run step passes only flags its script declares
- a scenario run names a scenario whose file exists
- a script declaring options in several spellings is read
- a set step derives that its control equals the value
- a stage with more processes than a column holds is a warning to split it
- a step adds the systems its action implies
- a step carries the line a reader copies written once by the generator
- a step for several options names them all in the plain text
- a step instruction of two sentences is refused since the result goes in its note
- a step instruction over eight words is refused
- a step is done by the process actor unless it names its own
- a step names a module type and control the help knows or it is refused
- a step reading a missing page is refused
- a step running a missing process is refused
- a step runs a script that exists or it is refused
- a step runs in the runners its action allows
- a step that opens an address or reads a page links there
- a system is one the architecture names
- a title past its cap or a goal of two sentences is refused
- a web address a placeholder and a generated catalog anchor are not checked
- a written then is kept
- an add step derives that its module exists
- an expectation is a value or a range never both
- an image is seen rather than read and a link counts its text only
- every page a process names exists
- the commit checks fold into one subprocess per trigger group each within its caps
- the map carries its links absolute for a device page
- the map leaves a process prose to its own file fetched when the view opens
- the module index is keyed by type and names the type a step adds
- the prose is plain text under the map with the process id as its address
- the repo stage files build into one map
- two steps in a row that two processes share are a warning to call one process
- writing the map replaces the prose of a process that is gone
gen_tests¶
- a checks findings are kept in one shape and read back as rules with counts and every file with its errors
- a doctest junit file becomes runs by the derived id with failures and skips told apart
- a live only scenario is a live test of the large size
- a module is grouped by the help page it is on and anything else by the domain of its tests
- a python test reads as a sentence and its file says what it is
- a ratcheted check that never ran shows the counts committed with the tree
- a recorded run is current only on the tree it saw
- a scenario is named by its file stem and carries its own steps in order
- a scenario names its controls by type through the instances it declares
- a step is tested by the scenarios that set or expect the control it sets or expects
- a step naming an instance the scenario does not declare is refused rather than guessed
- a step tag above a case names the process steps it proves without joining its description
- a steps instance has the type its last state document gave it and a declared one a document creates is refused
- a subcase run counts for the longest registered case it belongs to
- a test about two subjects is in the shard of each
- a test naming a step no process has is reported by the test and the step
- a test that names a step comes before the ones that only share its control
- a unit test is named by its file and its own sentence with its line and subjects
- doctests one output file is told into its console text and its junit document
- each subject of the index says how many tests passed failed or have no result and what its check found
- every subject of the repository has a group and the groups are the help pages and the kinds of test
- pytest and node junit files become runs by id and a title two files share lands on both
- scenario results and a checks last run become runs by id
- the gaps are the device changing steps no test covers the modules no test is about and the controls the help lacks
- the python js and check tests of this repository are registered in groups of their own
- the registry of this repository is valid against its schema
- two tests with one id are refused since an id names exactly one test
github_annotations¶
- a checks errors are annotated only in the files a pull request changes
- a failing test is annotated where its failure points and counted
improv_frame¶
- a busy device gets the same frame again until it acks
- a document carries its container object unchanged
- a document is one frame when it fits and chunks in order when it does not
- a lost ack resends the whole document and then succeeds
- a state frame arriving before the ack is chatter not a refusal
- an entry sends one document per top level container in key order
- an ethernet only entry is recognized the same way the browser does it
- any other error is a refusal and the op fails once
- checksum covers header through payload
- checksum is sum mod 256
- drivers goes last whatever the entrys key order
- empty or malformed entry yields no documents
- frame layout
- golden vector g1
- no ack at all fails instead of claiming success
- the device model name rides in the system document
- the script no longer sends a vendor rpc the firmware does not have
improv_smoke_test¶
- output without a url reports none
- the probe reports a provisioned boards url
- the provision step reports the url
installer_manifests¶
- a teensy manifest is its flat image at offset zero
- app and bootloader carry the version
- every shipping firmware has a manifest with parts
- manifest parts match staged file globs
live_scenario_runner¶
- a board is named after its firmware wherever the runner is
- a control a scenario wrote is put back to what it held
- a control whose module is gone does not stop the rest
- a desktop build on another machine is named after its own platform
- a desktop build on this machine is named after this host
- a device that kept running is not mistaken for a restart
- a fixture naming a container the run does not restore is refused
- a module a scenario switched off is switched back on
- a password is not remembered because the device returns it obfuscated
- a restart long after boot is seen
- a restart seconds after the previous one is seen
- a restore that fails unexpectedly is reported and a vanished module is not
- a restore the device declines is not counted
- an error response that is not json still reads as a failure
- an http post step sends an empty body as a button does
- an http step on a path goes to the device under test
- only a host on this machine counts as local
- only the value before the first write is remembered
- the fixture becomes one replacing document per container
- the modules a state document can create are the objects with a type
- the snapshot is each containers document and skips one the device lacks
- whole second rounding does not invent a restart
mkdocs_repo_links¶
- a folder link goes to its tree and a file link to its blob
- an embedded root file links from the site root whatever folder embeds it
- an embedded site link becomes the climb to the site root
- every site link in a readme names a page and an anchor the site has
mkdocs_slug¶
- catalog card images resolve on disk
- slug matches markdown toc slugify
- slug resolves to the real rendered heading id
- the generators heading id is the one mkdocs renders
moondeck_mac_match¶
- derivation is directional efuse to device
- empty never matches
- exact match case insensitive
- malformed only matches itself never derives
- s31 eui64 truncated derivation matches
- unrelated macs do not match
moondeck_port_identity¶
- already labeled esp port stays probeable
- apply probe caches serial and chip
- apply probe corrects stale serial on swap
- apply probe ignores unmatched and failed
- chip from usb external adapter reveals nothing
- chip from usb pid table then generic
- firmware to chip
- normalize chip
- parse esptool probe
- parse esptool probe alt wording and failure
- port serial extracts stable key
- resolve ch343 p4 by port serial
- resolve external adapter matches by port serial and derives chip
- resolve known non esp is labeled and not probeable
- resolve native usb matches by mac and reveals chip
- resolve native usb p4 not mislabeled as s3
- resolve no usb descriptor still matches by port serial
- resolve non esp without product falls back
- resolve prefers probed chip over firmware
- resolve unknown vendor stays probeable
moonlive_catalog_scripts¶
- a dimensions the generator cannot read fails the build
- a script in a subfolder is cataloged with it for the download url
- a script that declares nothing reads as unsaid
- a subfolder named like a role folder is still a subfolder
- a tags the generator cannot read fails the build
- each declaration is read independently
- every shipped script is readable
- more subfolders than an index holds fail the build
- reads a declared dimension and tags
- two scripts of one name in different subfolders fail the build
moontube_voices¶
- a script is spoken in the voice it names
- a script naming no voice is spoken by luna
- an unknown voice stops the run and lists the choices
- every clip names a voice that exists
- no clip shares a voice with its neighbor in the series
- speakers of one model share one download
- the command line overrides the script
- the intros overview names a project that exists
- the overview lists the clips after this one from the project
- the slides are lunas
mtrun_chapter¶
- a chapter card is held once by the screencast
- a chapter card lasts its seconds once the clip is sped up
observed¶
- a measurement goes into the local window and the next run builds on it
- a renegotiated contract starts the window over
- a window of each scenario step and target is kept apart
- the summary a scenario file publishes has the statistics and no samples
openapi_routes¶
- every path in the spec is one the server dispatches
- every route the server dispatches is in the spec
- the api names the firmware version it describes
- the spec is openapi 3 1 and every operation has a response
release_version¶
- a major release is declared before it is tagged
- after a release development moves to the next minor
- only the version field changes
- the api version moves with the firmware version
repo_health_baseline¶
- a good file on disk loads
- a section of the wrong shape is rejected
- a snapshot that is not an object is rejected
- a snapshot with no sections is still valid
- a valid snapshot passes through unchanged
- a wrong shape on disk reads as empty
- carry forward drops a renamed firmware
- carry forward keeps previous values for unmeasured sections
- carry forward survives a rejected baseline
- malformed json on disk reads as empty
- the baseline comes from the commit not the working tree
- the baseline falls back to disk when git cannot answer
- the gains leave out a metric the release lacked and mark a new firmware
- the gains section shows each change with its share
repo_health_host_keys¶
- the key names the host and never lumps two together
- the measurement itself is written under the host key
repo_health_measured_state¶
- built labels say measured carried or stale
- second snapshot does not inherit the first runs measurement
run_scenario_freshness¶
- an edit to a platform the desktop never links does not make the runner stale
- the desktop platform and the shared platform headers still count
scenario_nav_order¶
- a card the interface does not name sorts last
- scenarios are ordered by the card they prove
- the order comes from the interface
scenario_runner¶
- a live op is skipped and an op neither runner knows fails
- a mutate scenario with nothing to build fails unless it says live only
- an unknown mode fails even on a live only scenario
scenario_runs_somewhere¶
- a scenario with a fixture declares mutate
- no scenario is skipped by both runners
- there are scenarios to check
scripts_are_cpp¶
- script compiles as cpp
- the check can fail
teensy_flash_map¶
- the build reports the program room the platform leaves
- the installer erases the settings where the platform keeps them
- the installer writes exactly the flash below the reserved top
verify_version¶
- latest skips
- stable tag matches clean library
- stable tag matches dev library
- wrong core fails
walk_status¶
- a calls paths guard the steps it calls so a docs change runs no code check
- a card counts its own calls but not another cards
- a failed run or one with other arguments proves nothing
- a glob crosses folders with two stars and stays in one with one
- a merge reads the change against where the branch left main and a release every step
- a pass holds until a file it covers changes
- a pass on a tree git has pruned proves nothing
- a pass that writes a file it covers still holds
- a record that cannot be read is no runs
- a step is triggered only when the change touches its paths
- a step names its call and only the process file holds it
- a step running a subprocess reports the subprocess steps under their own ids
- a step takes as long as its longest recent run and is slow past the line
- a step with a placeholder in its call is the readers to run
- a step without paths runs on every change and any change makes its pass stale
- a subprocess reached twice or through a cycle is read once
- an untracked file triggers the steps whose paths it falls under
- the last matching pattern decides so an exclusion carves out a folder
- the snapshot leaves the real index as it was
- two runs ending together both land in the record
wled_json_shape¶
- all python wled required fields present
- golden parses as json
- python wled accepts device
UI · JS tests¶
backup-bundle¶
- a child the live tree does not hold yet has its type checked and its controls left alone
- a control gone from a matched child is reported on that child
- a file that fails to read is skipped and named; the rest still archive
- a fully modern config yields an empty report; preset payloads are skipped
- a mid-walk rewrite re-lists once and archives the fresh bytes
- a non-text file is skipped and named, not archived mangled
- a truncated read fails the backup loudly instead of archiving it
- an unknown child type is reported once, not per key
- restore creates parent directories before children, each once
- siblings of one type are matched by name and checked against their own controls
- the HLS segment dir is transient encoder output, never backed up
- the report names a control this firmware lacks, defaults noted
- the report names a module this firmware lacks
- the walk collects every file at every depth, byte-verified
backup-snippet¶
- a truncated read fails the backup loudly instead of archiving an incomplete file
- the bookmarklet downloads a complete bundle the Restore button accepts
- without /api/state the bundle still builds, named by hostname
file-manager-sd-upload¶
- a dropped connection on a long /sd upload reports plainly, not as a generic network error
- a file over the flash cap bound for flash is skipped client-side, never opening a connection
- a genuine server failure on an SD upload still reports, carrying the server's own message
- fmIsSdPath matches /sd and anything under it, nothing else
- the same file bound for /sd is sent, not skipped by the flash cap, and reports progress
improv-frame¶
- APPLY_OP always emits at least one frame (so
lastalways sends) - Drivers goes last whatever the entry's key order
- a document carries its container object unchanged
- a frame whose checksum fails is skipped and the next one still read
- a reply is found between lines of log text
- a reply split across two reads is kept until it is whole
- an APPLY_OP reply is an ack, a refusal to resend, or an operation the firmware lacks
- an already-serialized document is framed as given
- an empty or malformed entry sends no documents
- an entry sends one document per top-level container, in key order
- checksum is sum-mod-256 of the first 9+length bytes
- frame layout: magic, version, type, length, payload, checksum
- golden vector G1: buildImprovFrame(RPC, [0x01])
- golden vector G2: a small state document is a single APPLY_OP frame
- golden vector G3: a >125-byte document chunks into ordered frames
- no catalog entry has a container document the device cannot reassemble (512 bytes)
installer-desktop-download¶
- a desktop archive is offered for download even though it has no manifest
- a desktop is never offered an ESP32 firmware as its update
- a desktop is offered exactly one archive: the one built for its own OS
- a platform shipping both an installer and a tarball offers the installer
- a release with only an archive still offers it, for every platform it has
- a version with dots does not break the platform match
- a windows installer whose name carries a suffix after the version is still matched
- an ESP32 device is never offered a desktop archive to flash
- an ESP32 firmware and its ethernet variant remain mutually flashable
- each desktop platform is offered once, however many archives it ships
installer-eth-only¶
- at least one eth-only and one WiFi-capable firmware ship (the rule distinguishes them)
- the installer's /-eth$/ rule matches firmwares.json eth_only for every ESP32 firmware
installer-firmware-merge¶
- mergeFirmwares: a local-only firmware (not in the release) is added
- mergeFirmwares: a same-named local entry overrides the published one
- mergeFirmwares: an incomplete extra (missing manifest or binary) is ignored
- mergeFirmwares: no extras returns the published list unchanged (production path)
- parseFirmwaresFromAssets: MoonBase assets are never offered as an OTA image
- parseFirmwaresFromAssets: a firmware needs BOTH a manifest and an app .bin
- parseFirmwaresFromAssets: no/empty assets yields no firmwares
installer-flash-baud¶
- catalogFlashBaud falls back to 460800 on any lookup failure
- every board's flashBaud (when set) is a standard esptool rate
- the orchestrator drives esptool-js from the catalog flashBaud, not a hardcoded rate
installer-flash-progress¶
- a file's completion lands at its share of the total, not at 100
- a single-image flash still sweeps the full bar
- a tick with no total does not move the bar past what is already written
- malformed input reports 0 rather than NaN
- progress never goes backwards across a multi-image flash
- progress reaches 100 exactly once, at the end of the last image
installer-release-label¶
- a release reads as its tag, the moving prerelease as latest plus its version
installer-s31-webflash¶
- every flagged chip matches a real deviceModels.json board chip
- the S31 board points at the esp32s31 firmware (so the CLI hint names the right build)
- the S31 is browser-flashable: esptool-js 0.7.0 knows the chip
- the guard only fires on the connect-flash stage
installer-state-modules¶
- a child of a child module is reached depth first
- a root member is a top-level module whose type is not written
- an entry without a state document has no modules
- an object member with a type is a child module, its scalars and arrays are controls
installer-teensy-writer¶
- a Teensy in its bootloader keeps the erase choice, and its checkbox is what the writer reads
- a Teensy is recognized by PJRC's vendor id and the USB-serial product id
- a report is a 64-byte header carrying the block's offset little-endian, then 1024 image bytes
- after the reboot the installer finds the installed board, never another granted Teensy
- an erase zeroes the settings' superblock pair, 8 KB at the bottom of the 1 MB settings area, so the firmware formats it at boot
- an image is refused when it is a HEX file, lacks the FCFB block, or exceeds the program flash
- block 0 is always written, since it erases the chip, and a later all-0xFF block is skipped
- the boot report is 0xFF in its three offset bytes and zero elsewhere
- writing the reports onto erased flash reproduces the image byte for byte
migrate¶
- Network's Ethernet controls move onto its Ethernet child, which starts with the shared IP settings
- Network's one WiFi network becomes the first known network of its WiFi child
- a bundle carrying both old and new names reports the collision, never silent
- a config that is already a state document passes through byte-exact, unreported
- a corrupt config file is restored as-is and flagged for review
- a deterministic peripheral move is set: MoonLedDriver becomes ParallelLedDriver on LCD-MM
- a driver's preset control migrates to fixture with its value
- a flat config becomes the module's document, its code-wired child named as the live tree names it
- a flat preset becomes a state document, each module named as the device names it
- a legacy network joins a WiFi child's saved known list rather than replacing it
- a preset that is already a document passes through
- a saved $name names its child, and unnamed children of one type become X and X-2
- a saved soundReactive value comes back under audioReactive
- a user preset that shares a renamed filename is the user's, not migrated
- an E1.31 multicast output becomes E1.31, flagged to set its addressing
- chip-dependent peripherals get a review entry, never a guessed value
- control VALUES migrate too: peripheral MoonI80 maps to LCD-MM, i80 flags review
- control renames apply, and semantics changes flag review instead of guessing
- every control rename declares the key renameKeys reads, and carries its scope
- light presets are fixture profiles: type, list key and the driver's Select all map
- preset payload values rename (the captured container), other content byte-exact
- renamed config files land under their new names, reported
- renamed module types map in place, including nested children
- renames are scoped by module type: NetworkSendDriver keeps its own fps
- soundReactive is left alone on a module outside its scope
- the MIDI desk's usb switch becomes its source, browser or USB
- the receiver lists become hosts, and discovery's WLED switch becomes its addressing
- unknown content passes through untouched with an empty report
mooncloud-page¶
- other holds the rows it folds, and the legend unfolds them as links
- the worker loads as a module, its page included
mooncloud-report¶
- a message from a device is accepted, and a malformed sender is not
- a non-string sneaking into the module list is dropped
- a numeric field that is not a number stores zero rather than text
- a numeric suffix inside a script path is left alone, being a folder the user named
- a report cannot smuggle a field the firmware never sends
- an instance suffix is folded into its type, so one driver counts once
- an unknown country is recorded rather than left empty
- memory and light counts are stored as numbers, not dropped
- oversized values are dropped rather than truncated into storage
- the country comes from the edge, never from the report
- the event is one of the three words the card can show
- the module list is bounded and flattened
- the received date is a day, not a moment
- the stats endpoint selects only aggregates, never a row
moonwalk-device¶
- a condition reads as the words a reader checks the device against
- a contributor's process is done when every step the change needs has a pass that still holds
- a duration rounds before it splits into minutes, so it never reads 60 seconds
- a followed step points at the module type its action names, and the control it sets
- a host vouching for a process keeps it with the reader's own marks
- a list control counts as its number of rows
- a module that exists is an expectation the state answers
- a process is done when every expectation the device's state answers holds
- a process the state cannot answer is never done by the device, only by the reader's mark
- a range holds within its inclusive bounds, either of which may be open
- a select's index also reads as its option's name, so a step that names the option holds
- a step a change triggers still shows on the site, which knows no change: only a reader's choice hides a step
- a step can name several options, and shows for any of them
- a step names a module by type, so any module of that type can meet it, whatever its name
- a step running a subprocess stands for its steps: needed when any is, passed when every needed one is
- a step shows for the chosen option, and a step for every option always shows
- a step the state cannot answer is the reader's to mark, so it is neither done nor not done
- a step's call copies as the line the map carries, and a step that runs nothing copies nothing
- next can be narrowed to the reader's actor
- next is a process not done whose dependencies all are, in the journey's order
- one expectation that does not hold leaves the process to do
- the bar moves past every step the state shows done and stops at the first it does not
- the choices are what is here and what comes next, without what is done or listed twice
preview-adapt¶
- a clean link never loses detail: any run of drop-free windows holds full resolution
- a dead link holds its stride: nothing delivered and nothing dropped is not a verdict
- a held refine is forgiven: later congestion coarsens without extra backoff punishment
- a refine that brings the drops back is taken back, and the next try waits twice as long
- a renderer-limited device keeps full detail: slow frames without drops are not congestion
- clean windows refine one rung at a time back to full detail
- heavy starvation coarsens immediately: more frames dropped than delivered
- one dirty window is a hiccup and changes nothing; a second in a row coarsens
- repeatedly failing refines back off exponentially and cap, so a borderline link cannot oscillate
- the stride caps at 64 however bad it gets, and never goes below 1
- trace drops are pacing jitter, not congestion: they hold refining but never coarsen
semver¶
- a build after a release ranks above it and below the next release
- compare: a release out-ranks a prerelease of the same core (§11)
- compare: monotonic -dev.
ordering (the latest-channel case) - compare: numeric core ordering
- compare: prerelease identifier precedence (§11)
- compare: unparseable side sorts lowest (never falsely claims newer)
- isNewer: the update-available predicate
- parse: core, prerelease, leading v, build metadata
ui-address-move¶
- a control marks its own reset button as it writes, on a card and in a row alike
- an address edit names where the device goes: the static address, or a lease from the router
- an address field is sent when it is committed, not as it is typed
- the Ethernet card asks before moving the device only while Ethernet carries the page
- the page knows which interface carries it, and the access point is none
ui-docs-links¶
- every docs link in the UI points at an existing page and heading
ui-emoji-labels¶
- every emoji a module declares in tags() has a tooltip
- every emoji the interface generates has a tooltip
ui-gallery¶
- a document is shown indented with its keys in the order written
- a gallery file lands under its own name, and a preset name the device accepts
- a gallery link is kept only when it is http or https
- a hundred entries show twelve to a page, and a page past the end lands on the last
- a look, a palette or a look with its palette applies unasked, and pins or geometry ask first
- a pad saves the container its editor shows, through the one save path
- a preset brings the scripts the device lacks: the gallery's copy first, else the one the firmware ships
- a preset names the scripts it needs as control values
- a refused name stops the save, so it never lands under the name before it
- a shipped script's repo path carries its subfolder, and a script the catalog lacks sits at its kind's root
- a thumbnail path from the index stays inside the gallery repository
- browsing loads thumbnails only, and the full-size preview waits until an entry opens
- every gallery download refuses a failed response before anything is written or applied
- search covers the name, the description and the author, and a kind chip narrows to one kind
- the gallery index and files are fetched past GitHub's cache, and the index at most once per 30 seconds
- the gallery sorts by most liked or by newest
ui-gamepad-bridge¶
- a pad at rest encodes no buttons and every axis at 128
- a value past the stick's range is clamped rather than wrapped
- buttons become bits in the standard layout's order, and the axes span 0 to 255
ui-gif¶
- a frame long enough to fill the code table restarts it and still decodes
- a gradient of more than 256 colors keeps every pixel close to its color
- a saved preview loops, keeps its frame timing, and every pixel decodes to its color
ui-help¶
- a MoonWalk link in the help opens the map in the app at its step, not a popup
- a NUL in the text cannot pose as a code block
- a blocked popup leaves the link to open its own tab
- a clip on its own line plays with its poster, a picture shows, and anything not https stays text
- a clip that ends the text still plays
- a control's help names the step that sets it
- a fenced block keeps its text as written, escaped and unformatted
- a fenced block with a blank line inside stays one block
- a heading renders one level under the view's own
- a help link opens in one named popup with its opener cut, so the app stays open
- a link or picture from the file must be https and stay inside its attribute
- a link outside the help, or a modified click, is left to the browser
- a link that is not http or https stays text
- a list renders as one, its items formatted
- a module matches when it carries every active chip
- a module's help lists each MoonWalk process it takes part in once, opening at its first step
- a star inside code or a link's address stays literal, and a link's text may be emphasized
- a table renders with its first row as the head, its cells formatted
- a table without a separator row keeps every body row
- a type's display name drops its role noun
- a walk link's address is the host's page with the ids encoded, so the file cannot choose where it leads
- a yes is kept, so help downloads and the question is not asked again
- code, bold and absolute links render
- markup in the text stays text
- paragraphs stay apart
- the compiled chip matches a module with no scripted marker
- turning help on asks first, and a no downloads and stores nothing
- without MoonWalk's index, or for a process the index lacks, the help shows no link
ui-help-cache¶
- a cached copy of another release does not show, and one of the running release does
- before the state names the firmware nothing is fetched
- the release's copy is asked first, then the site's, and the answer is cached with its address
- the site's cached copy serves any release, and an old plain-text cache is dropped
ui-hex-field¶
- a hex field reads 0x18, 0X18 and 18 alike, and holds no value until it is a whole hex number
- a hex field shows 24 as 0x18, and a decimal field as 24
- an edit that leaves no number cancels the queued write, and blur sends nothing for a malformed field
- the live patch formats a number field the way its renderer does
ui-live-patch-text¶
- ${fn} removes nodes only when something actually changed
- ${fn} writes text only through setText, so a selection survives the patch
- every function on the patch path is covered by this file
- setText writes only when the text actually differs
ui-midi-bridge¶
- SysEx is asked for only when the greeted desk's greeting carries one, so a Mackie desk works with SysEx refused
- a batch is each message as two-digit hex, separated by spaces
- a desk's greeting goes out as whole SysEx and channel messages, and nothing malformed
- a slot that is not a channel message never reaches the desk
- the desk is sent every slot once, then only the slots that changed
ui-mooncloud¶
- Enter in a text field presses the card's send button
- MoonTalk adds no special case to the shared control path
- a card that cannot reach MoonCloud says so
- a clicked slice filters every chart, and says so
- both MoonCloud fetches use one compiled-in address
- consent has no reset-to-default button
- the card re-reads only on a write that changed the server, once
- the consent explanation is a module status, not a bespoke UI block
- without consent nothing is fetched, and the card still shows its shape
- without consent the board says it was not read, not that it is empty
ui-mooncloud-other¶
- a picked row inside other filters a bucketed chart by its bounds
- other holds the rows it folds, in order, and counts them
- the legend unfolds other into rows that filter
ui-name-case¶
- no lookup keyed on a module name goes through a bare querySelector
- queryByName compares the attribute exactly, and the app routes lookups through it
- the case-sensitivity flag is never used: Chrome throws SyntaxError on it
ui-one-renderer¶
- ${name} draws its values through createControl rather than its own inputs
- every field createControl draws carries the key a list rebuild restores it by
- the card's save-as-preset name is the surface's own name control
ui-password¶
- a list's password field shows the decoded password, as the control does
- the page decodes what the device's writeObfuscatedPassword sends
ui-picker-legacy-keys¶
- only the new key is ever written, so the old one fades out
- the URL parameter has exactly one spelling
- the old storage key is still read
- the resolution order prefers the explicit link over the saved habit
ui-picker-scripts¶
- a replace always renames the card, whichever kind of row was picked
- a replaced card is found by POSITION, so a name collision still reaches the right module
- a script becomes a picker row named after the file, without its extension
- a script the device does not hold is marked as a download before it is chosen
- every script row carries the scripted marker, whatever its own tags say
- only the roles a parent accepts are offered
- pointing a card at a script re-renders it, so a failing script still shows its source
- saving an unmodified factory script does not create a shadowing copy
- scripts and compiled modules sort as one alphabetical list, markers ignored
- the picker's chip groups lead with the two kinds
- the two kind chips partition the list, the compiled one matching by absence
ui-progress-label¶
- a KiB gauge scales first, so a 32 GB card reads as gigabytes
- a byte gauge picks KB, MB or GB per number
- a count gauge shows plain numbers, never a byte suffix
- a missing value reads as zero rather than NaN
ui-render-guard¶
- a full state deferred while editing renders as soon as the edit ends
- a truncated preview frame feeds no adaptation counters: validate first, then count
- the /api/types arrival keeps its guard too
- the WebSocket full state does not rebuild the DOM while the user is interacting
- the editing guard lives in exactly one place
ui-script-picker-download¶
- one helper owns making a picked script local
- the script dropdown goes through that helper rather than its own copy
ui-segmented-choice¶
- both render paths build a select through one builder, so a list crossing three changes form in place
- up to three short choices are segments; a fourth, or labels too long to fit, make a dropdown
ui-selected-root¶
- a choice made this session is not overwritten by what was saved earlier
- a link naming a module opens it with its tab selected, over what was saved
- a saved root that is no longer in the tree is ignored, leaving the fallback to choose
- an empty or absent tree leaves the selection alone rather than guessing
- the saved root is restored, so a refresh returns to where the user was
- the state arrival restores the selection before the render that reads it
ui-subscribe¶
- a config backup names its device from the state the page holds, without fetching the tree
- a full state releases the callers waiting for it, and not those whose request came later
- a page load draws from the subscribed state and fetches no whole tree
- a reconnect subscribes again, since the device keeps no subscription across sockets
- a subscription names the root and the modules the page reads outside it
- after an add, delete or move the page asks the socket for its view, not /api/state for the whole tree
- every module the page always subscribes to is a registered type
- every module type the page names is subscribed or read where it is whole
- the cards subscribe to the root they render, and an outline keeps the previous cards until its controls arrive
- the full-state handler answers the waiting callers with the id the document echoes
- the same root is subscribed once, and another root subscribes again
- the target picker offers controls from the whole tree, not only the root on screen
ui-visibility¶
- a preview reconnect armed before hiding is cancelled, and never fires on a hidden tab
- closing the preview socket stops the adaptation loop with it
- hiding cancels a pending reconnect, so no control socket opens on a hidden tab
- hiding the tab closes the preview socket immediately, keeping the pane's intent
- returning reopens control first, then the preview via the pane's own intent
- the control socket closes only after a grace, so alt-tab costs no resync
ui-ws-scheme¶
- a plain HTTP page is unchanged, so the direct case pays nothing
- a port survives, since location.host carries it
- an HTTPS page opens a secure WebSocket
- no channel hardcodes its own scheme