Skip to content

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
  • 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 last always 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
  • 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
  • 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