Skip to content

Somewhat of a rewrite - #35

Draft
domiko96 wants to merge 39 commits into
mainfrom
mqttsecure
Draft

Somewhat of a rewrite#35
domiko96 wants to merge 39 commits into
mainfrom
mqttsecure

Conversation

@domiko96

@domiko96 domiko96 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

No description provided.

domiko96 added 30 commits August 6, 2026 23:27
   2
   3 Drop the WiFiManager captive-portal flow, its SPIFFS-persisted
   4 config.json, and ArduinoOTA support to simplify the connectivity
   5 path. WiFi/MQTT/strip settings are now compile-time values in a
   6 gitignored include/config.h (template in config.example.h) instead
   7 of being set at runtime and persisted to flash.
   8
   9 Also fixes a buffer-size mismatch on mqtt_ssl (was sized for "no"
  10 only) now that its default comes from config.h.
   2
   3 Move wipe()/noise() out of the monolithic AnimationHandlerBus.cpp into
   4 their own files under AnimationHandlerBus/Modes/, matching the existing
   5 convention in AnimationHandler/Modes/. No behavior change.
   6
   7 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Fill in the 4 reserved-but-unimplemented modes (FIRE, VU_METER,
   4 STROBE_PARTS, MOVING_PARTS) and add 4 more (RAINBOW_CYCLE, THEATER_CHASE,
   5 COMET, NIGHT_SKY) to AnimationHandlerBus, each as its own file under
   6 Modes/. FIRE is a directional single-end flame with a tunable side
   7 (silo->position), occasional blue base tint and dim smoke near the tip.
   8 VU_METER needed AnimationHandlerBus::handle() to start sampling audio
   9 every tick (it never did), so it's wired up the same way
  10 AnimationHandlerPWM::handle() already does.
  11
  12 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Lets an MQTT client push an explicit R,G,B target for every LED in one
   4 shot (e.g. lighting only selected rack-unit sections), decoupled from
   5 the JSON control channel so it scales to 500 LEDs: a raw 3-bytes/LED
   6 binary payload on a new <mainTopic>/pixels sub-topic, with PubSubClient's
   7 buffer grown to fit and the callback dispatching to
   8 AnimationHandlerBus::setPixelData() before the existing per-byte debug
   9 print/JSON parsing.
  10
  11 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Four modes that need no color input from the user, only speed, working
   4 on both PWM and Bus strips: three fixed-palette gradient loops (Ocean,
   5 Forest, Dusk) sharing a new cycleColors() interpolation helper on
   6 AnimationHandler, and a whole-strip fire-flicker mode that lerps between
   7 random picks from a fixed fire-hued palette. Wired into both
   8 AnimationHandlerPWM::handle() and AnimationHandlerBus::getNewColor().
   9
  10 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Move PinDefinitions.h and config.h out of lib/Definitions and include/
   4 into a new top-level config/, split into board_config.h (fixed ESPLED
   5 board GPIO wiring, git-tracked) and device_config.h (WiFi/MQTT secrets
   6 and strip settings, git-ignored, template in device_config.h.example).
   7 lib/Definitions now holds only TypeDefinitions.h - shared type/enum code,
   8 not settings.
   9
  10 Note the settings file is named device_config.h rather than
  11 user_config.h: the ESP8266 NONOS SDK's osapi.h does
  12 `#include "user_config.h"` to pull in its own framework-level config
  13 header, so a project file with that exact name shadows it and breaks
  14 the build - discovered by actually building after the rename attempt.
  15
  16 main.cpp now fails the build with an actionable #error (via
  17 __has_include) if device_config.h hasn't been created from the example
  18 yet, instead of a bare "file not found". platformio.ini gains
  19 `[platformio] include_dir = config` plus an explicit `-Iconfig` build
  20 flag, since PlatformIO's per-library dependency finder builds each
  21 lib/* file with its own scoped include path and doesn't automatically
  22 add include_dir to those - only to src/-level compiles.
  23
  24 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 The Modes table and Example payloads section had drifted far from the
   4 real implementation (swapped mode numbers, a fabricated duration field
   5 and BLINK_COLOR state machine, modes that don't exist in the enum at
   6 all). Rewrite it to describe exactly what's implemented today,
   7 including all of Parts A-D's new modes (OCEAN/FOREST/DUSK/FIRE_FLICKER,
   8 FIRE/VU_METER/STROBE_PARTS/MOVING_PARTS/RAINBOW_CYCLE/THEATER_CHASE/
   9 COMET/NIGHT_SKY/PIXEL_ARRAY), and fix the parameter-usage table to match.
  10
  11 Switch the Modes table to raw HTML so each row can hold its full
  12 multi-line JSON example directly, rather than splitting a compact
  13 one-liner in the table from full examples in a separate section.
  14 GitHub's copy-button only attaches to fenced code blocks and can't
  15 appear inside a table cell (plain or HTML), so this trades that button
  16 away in exchange for one scrollable reference instead of two. PIXEL_ARRAY
  17 keeps a real fenced/copyable snippet right after the table since its
  18 actual pixel data is a separate binary payload that doesn't fit a
  19 single JSON example.
  20
  21 Also document the Modes/ one-file-per-mode convention and the new
  22 config/ vs lib/ layout in DEVNOTES.md.
  23
  24 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Pull the deterministic math several modes lean on (rate/length guards,
   4 directional index mapping, traveling-head position, keyframe-color
   5 interpolation) into plain functions under lib/ModeMath - zero Arduino/
   6 FastLED dependency, so they build and test on the host with no hardware.
   7 fire/vuMeter/strobeParts/movingParts/comet/nightSky/noise/strobe/
   8 fireFlicker/cycleColors now call these instead of inlining the same
   9 logic six times over; behavior is unchanged.
  10
  11 Add a [env:native] PlatformIO environment and Unity tests in
  12 software/test/test_modemath covering guard boundaries, both directions
  13 of the index mapping across a few strip lengths (including 500),
  14 wrap-at-exact-period behavior, and the ocean() keyframes' interpolated
  15 colors at each segment midpoint - all hand-computed and verified against
  16 the implementation (`pio test -e native`, 22/22 passing).
  17
  18 This intentionally doesn't cover FastLED rendering, MQTT, or the
  19 inherently-randomized effects' actual appearance - those still need a
  20 real strip once hardware is available.
  21
  22 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Confirmed via the schematic (hardware/ESPLED.net) that the MIC header
   4 (J4) wires directly to the ESP module's ADC pin (U2 pin 2 = A0) - the
   5 existing analogRead(A0) path was already reading the right pin, it just
   6 assumed the mic's silence point sits at exactly 0.5 on the normalized
   7 0-1 scale. A MAX4466 centers its output around VCC/2, but the exact
   8 point drifts with its onboard trimmer and component tolerance, so track
   9 it instead: a slow (0.1%/sample) exponential moving average in
  10 recordAudioSample(), used by vuMeter() in place of the hardcoded 0.5f.
  11
  12 No separate HAS_MAX4466 compile flag: there's nothing to gate - A0 is a
  13 fixed ADC pin either way, and the bias tracker degrades harmlessly
  14 (settles on whatever floating/noise A0 reads) if no mic is populated.
  15
  16 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 BASS_REACT (shared, PWM+Bus, mode 9): scales colorValue's brightness by
   4 a bass envelope extracted from the audio ring buffer via a one-pole
   5 low-pass filter (rejects mid/treble wiggle), same FFT-avoidance
   6 rationale as vuMeter(). Tunable via sensitivity (gain) and frequency
   7 (update rate).
   8
   9 VU_METER_PEAK (addressable-only, mode 111): vuMeter()'s bar-graph body
  10 plus a white peak-hold LED that jumps to the highest recent level and
  11 decays back down at a fixed rate - classic VU-meter behavior, sibling
  12 to the plain VU_METER rather than a replacement.
  13
  14 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Confirmed via the schematic (hardware/ESPLED.net) that the DS18B20 (U4)
   4 data pin wires to the ESP module's GPIO4 (U2 pin 13), with a 4.7K
   5 pull-up (R8) to 3.3V already on the PCB and VDD tied to 3.3V rather than
   6 DQ - normal (non-parasitic) power mode, no special timing needed.
   7
   8 Adds OneWire/DallasTemperature, a non-blocking request-then-poll-later
   9 read cycle (12-bit conversion takes up to ~750ms and must not block
  10 FastLED.show()/the animation loop, same throttle idiom as audioTimer/
  11 fpsTimer), and publishes Celsius to one decimal place on a new
  12 <mainTopic>/temperature MQTT topic, analogous to how debugTopic/
  13 pixelTopic are built.
  14
  15 No compile-time enable flag: the pull-up is passive and always present
  16 regardless of whether U4 itself is populated, and an absent sensor just
  17 reads back DallasTemperature's -127.0 disconnected sentinel - same
  18 "degrades gracefully, no flag needed" call made for the MAX4466 mic.
  19
  20 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 AnimationHandler, AnimationHandlerBus, AnimationHandlerPWM, StripControle,
   4 and CRGBWW each get a short block comment above their definition
   5 explaining their responsibility and how they relate to each other,
   6 matching the plain // prose style already used throughout this codebase
   7 (no Doxygen tags). Comment-only, no code changes.
   8
   9 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Thin wrapper around PlatformIO (software/) plus a doc-gifs target for
   4 the per-mode animated-GIF documentation pipeline.
   5
   6 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 A full class diagram covering AnimationHandler/AnimationHandlerBus/
   4 AnimationHandlerPWM/StripControle/CRGBWW/PixelBytes/varSilo and how
   5 they relate, plus two sequence diagrams walking an MQTT message through
   6 to the strip: an addressable-strip WIPE (mode 100) example through
   7 AnimationHandlerBus/FastLED, and a non-addressable-strip FADE_2_COLOR
   8 (mode 0) example through AnimationHandlerPWM/StripControle.
   9
  10 Also fixes a stale Modes/pixelArray.cpp reference left over from the
  11 mode-file rename (now 109_pixelArray.cpp), and documents the
  12 <modeNumber>_<name>.cpp file-naming convention.
  13
  14 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 tools/gifgen/ compiles the ACTUAL AnimationHandler/AnimationHandlerBus
   4 mode source (unmodified) into a host shared library against a small
   5 Arduino.h/FastLED.h shim (millis()/analogRead() driven by controllable
   6 globals; real reimplementations of HeatColor/blend/fadeToBlackBy/
   7 fill_rainbow/qsub8/qadd8/random8/random16/cos16), then drives it via
   8 ctypes + Pillow to render a 15s/20fps GIF per mode (100 LEDs, one 8x8 box
   9 per LED stacked into an 8x800 image) - not a random-color stand-in.
  10
  11 19 of 21 modes render from the real production code as-is. BLINK_COLOR
  12 (1) and SOUND_2_LIGHT (3) are skipped - both are genuine stubs that
  13 always return black, so a GIF would show nothing. SOUND_2_LIGHT's real
  14 source needs arduinoFFT, which isn't worth vendoring in just to render
  15 permanent black; the harness supplies its own black-returning
  16 sound2Light() purely to satisfy the linker. PIXEL_ARRAY (109) is driven
  17 by a one-off setPixelData() call instead of silo fields, since that's
  18 how it actually receives data.
  19
  20 `make doc-gifs` / `python3 tools/gifgen/generate.py` rebuilds the shared
  21 library automatically if missing or stale and regenerates every GIF;
  22 verified end to end (clean rebuild, full regeneration, frame count and
  23 total playback duration checked per file). Adds a Preview column to
  24 the README's modes table wired up to the generated GIFs.
  25
  26 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Part H (modes 9 and 111) landed after the README's mode table was
   4 written, so it silently jumped 0-8 then 100-110 with no rows for either.
   5 Add both, wired to the GIF-pipeline previews already generated for them,
   6 and fix the parameter table's "used by modes" lists and the "uniform
   7 modes (0-8)" range to include mode 9.
   8
   9 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… row

   2
   3 Regenerate every GIF as 800px wide x 8px tall (LED boxes laid out left
   4 to right, matching an actual strip's shape) instead of the original
   5 8x800 vertical column, and move the preview out of a narrow per-row
   6 "Preview" column into its own <tr><td colspan="6"> row directly under
   7 each mode - one continuous full-width strip per mode instead of a small
   8 80px thumbnail off to the side.
   9
  10 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…KY color

   2
   3 COMET (108): silo->sensitivity (0-255, native range) is now the trail's
   4 fade-per-tick amount instead of a hardcoded 40 - 0 means no spacing at
   5 all (a solid trail fills in behind the head), 255 a very short, tight
   6 trail.
   7
   8 COMET_BACK_AND_FORTH (113): comet()'s head+fading-trail, reflecting at
   9 each end instead of wrapping - same relationship as
  10 MOVING_BACK_AND_FORTH has to MOVING_PARTS, using the same
  11 bouncePositionFor() helper and the same tunable trail spacing.
  12
  13 NIGHT_SKY (110): stars now render in silo->colorValue (scaled by each
  14 star's current brightness) instead of a hardcoded near-white/blue tint.
  15 Verified by rendering its example/GIF in cyan-blue and confirming the
  16 actual rendered pixel bytes carry that tint, not white.
  17
  18 Regenerates the affected GIFs and updates README rows/params/preview
  19 cells accordingly.
  20
  21 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sition anchor

   2
   3 FIRE (101): silo->timeVariance (ms, 0=disabled) now controls how often
   4 the cooling value switches between a "tall/persistent flame" regime and
   5 a "short flame" one - smaller values switch more often, larger values
   6 commit to one regime longer. silo->maxBrightnes (0-1023, 0 treated as
   7 unset/full brightness) scales the whole flame's rendered brightness
   8 down for an overall dimmer fire. Verified via the regenerated GIF:
   9 peak channel value across all frames caps at 149, matching the 600/1023
  10 scale in its updated example.
  11
  12 VU_METER (103): silo->position now picks the bar's anchor - 0 grows
  13 from the start (left, the old always-on behavior), 1 grows from the
  14 end (right), anything else grows symmetrically outward from the
  15 center. Verified via the regenerated GIF: lit-pixel ranges stay
  16 centered around the strip's midpoint at every amplitude, not just at
  17 full-scale.
  18
  19 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 NOISE_RANDOM_COLOR: same 50/50-per-LED sparkle as NOISE, but no color
   4 input - every lit LED independently rolls its own random RGB each
   5 refresh, rather than all lit LEDs sharing colorValue. Verified via the
   6 regenerated GIF: 15,051 distinct lit colors across the animation, not a
   7 fixed palette.
   8
   9 Both NOISE and NOISE_RANDOM_COLOR now respect silo->maxBrightnes as an
  10 overall brightness cap (0 treated as unset/full brightness, matching
  11 fire()'s existing convention). Extracted the shared "0=unset, clamp to
  12 1023, scale to 0.0-1.0" logic into a new brightnessScaleFor() ModeMath
  13 helper (with unit tests) and refactored fire() to use it too instead of
  14 inlining the same three lines.
  15
  16 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 The old time=1500 example finished wiping almost instantly relative to
   4 the GIF's 15s length, so the preview looked static (just a held solid
   5 color) for most of its playback. 12000 spends 12s of the 15s actually
   6 wiping, leaving 3s held at the end - verified against the regenerated
   7 GIF by cumulative playback time (reaches fully lit right at t=12000ms).
   8
   9 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Fades up to color over time, holds for duration, fades back down over
   4 time, holds off for duration, repeating length times (length <= 1 skips
   5 the fade-down/hold-off legs, giving a single pulse). Once finished, hands
   6 control straight back to whatever mode/color was running before -
   7 instantly, no crossfade back in - picking up exactly where it left off
   8 rather than restarting or jumping ahead by however long the blink ran.
   9
  10 The "resume without jumping" trick is pure timestamp math, not pixels:
  11 AnimationHandler::snapshotSilo() copies *silo and lastChange into new
  12 savedSilo/savedLastChange members before main.cpp's MQTT callback
  13 overwrites Silo's fields with the blink's own parameters (the only point
  14 where the previous mode's live parameters still exist - by the time any
  15 mode method runs, *silo already holds whatever's newly selected). On
  16 resume, blinkColor() restores *silo and shifts lastChange by exactly how
  17 long the blink ran, so `now - lastChange` - which is what nearly every
  18 mode derives its position/progress from - comes out the same as if the
  19 blink had never happened. AnimationHandlerPWM/AnimationHandlerBus both
  20 privately inherit AnimationHandler, so each gets a one-line public
  21 snapshotSilo() forwarder for main.cpp to actually call.
  22
  23 Also: a second mode:1 message while one is already blinking is ignored
  24 rather than restarting the sequence, and a genuinely new varSilo.duration
  25 field carries the hold time (there was previously nowhere to put it -
  26 `time` already means "fade duration" everywhere else).
  27
  28 Fixes a pre-existing bug found along the way: AnimationHandlerPWM::handle()
  29 called blinkColor() but discarded its return value, never actually
  30 pushing it to the strip - harmless while it was a stub, a real bug now
  31 that it computes real colors.
  32
  33 Verified beyond just compiling: two standalone host-side programs (using
  34 the gifgen shim, not committed - one-off checks) directly exercise
  35 blinkColor()'s snapshot/restore math for both the single-pulse and
  36 multi-cycle paths and confirm zero elapsed-time drift on resume, i.e. the
  37 actual behavior this feature exists to provide. Also wired into the GIF
  38 pipeline and regenerated its preview - confirmed the fade/hold/repeat
  39 cadence and cycle timing show up correctly frame-by-frame.
  40
  41 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
   2
   3 Samples the mic, runs the existing DCRemoval->Hamming->FFT->magnitude
   4 pipeline sound2Light() already stubs out, and splits the result across
   5 a tunable number of gapped bins spread over the strip. Bin brightness
   6 reflects that frequency range's intensity (auto peak-normalized, then
   7 scaled by sensitivity as a gain); color sets the hue, time controls
   8 the FFT windowing duration, and frequency throttles the redraw rate.
   9
  10 ModeMath::binWidthFor() computes per-bin pixel width for N gapped bins
  11 and is unit tested alongside the rest of ModeMath. The GIF pipeline's
  12 build.sh now compiles the real vendored arduinoFFT source (it turns
  13 out to be host-portable already, no shim needed) so 115_spectrum.gif
  14 is a genuine preview, not a stub.
   2
   3 length now sets how many consecutive pixels each lit segment spans
   4 (default 1, the original single-pixel look); position sets the
   5 spacing between repeats (default 3, the previous hardcoded constant).
   6 Both default when left at 0, so existing configs render unchanged.
   2
   3 Shared uniform-color mode (works on both PWM and addressable strips).
   4 S1 ("lub" - louder/longer) rises to maxBrightnes and decays, a brief
   5 systole gap, then S2 ("dub" - sharper/shorter, peaks at ~60% of S1)
   6 rises and decays, followed by a diastole pause (duration ms) at
   7 minBrightnes before the next beat. frequency sets how fast the
   8 S1/gap/S2 sequence itself runs (1000/frequency ms), independent of
   9 duration, which only controls the quiet gap between beats.
   2
   3 Replaces the old FFT-based stub (always returned black) with the same
   4 rectified-amplitude envelope follower VU_METER uses for its bar graph,
   5 but scaling the whole strip/color's brightness between minBrightnes
   6 (silence) and maxBrightnes (loudest) instead. Shared uniform mode, so
   7 it works on both PWM and addressable strips with no separate variant.
   8 sensitivity is the gain, frequency the update rate.
   9
  10 No longer needs arduinoFFT, so the GIF pipeline now compiles the real
  11 source instead of linking a hand-written black-returning stand-in.
…them

   2
   3 Reorganizes the flat 0-115 mode numbering into bands: 0-49 core shared
   4 modes, 50-99 sound-reactive shared modes, 100-499 normal addressable-
   5 only modes, 500-599 sound-reactive addressable-only modes, 1000+
   6 special modes that bypass the normal parameter model (PIXEL_ARRAY,
   7 BLINK_COLOR). FADE_2_COLOR stays 0, BREATHE moves to 2, and every
   8 Modes/*.cpp file is renamed to match its mode's new number.
   9
  10 Along the way, found and fixed a real bug: varSilo.mode was declared
  11 unsigned char (max 255), so every mode number above 255 silently
  12 truncated on assignment - both in the real MQTT callback and in the
  13 GIF-preview harness. Widened it to unsigned int.
  14
  15 Also updates every doc reference to the new numbers: software/README.md's
  16 full modes table and parameter cross-reference table, software/DEVNOTES.md,
  17 and tools/gifgen's build/generate scripts (all GIFs regenerated under
  18 their new filenames).
   2
   3 Divides the strip into alternating red/blue length-pixel sections
   4 separated by 4px gaps; every instant either all red sections are lit
   5 and all blue ones dark, or the reverse, swapping at frequency Hz. No
   6 color input - the red/blue palette is fixed.
   7
   8 ModeMath::policeLightsLayoutFor() computes the section/gap layout:
   9 leftover pixels that don't divide evenly into sections+base-gaps are
  10 spread across the internal gaps first, then any remainder is split
  11 across start/end margins, with a final odd pixel going to the start.
  12 Covered by 6 new native unit tests.
   2
   3 Directory listings previously sorted lexically (0, 1, 10, 100, 1000,
   4 101, ...) instead of numerically, since filenames weren't a fixed
   5 width. Pads every mode number to 4 digits (enough for the 1000+
   6 special band) so filename order matches mode-number order.
   7
   8 Pure rename - no logic or content changes. generate.py's filename
   9 field, software/README.md's GIF image paths, and software/DEVNOTES.md's
  10 file references are updated to match; all GIFs regenerated under their
  11 padded names with identical byte sizes to before.
   2
   3 Previously always grew from the start regardless of silo->position.
   4 Now shares VU_METER's 0=left/1=right/else=center anchor semantics; the
   5 peak-hold marker tracks the bar's leading edge for left/right, and
   6 splits into two symmetric markers (one per side) for the center anchor
   7 since the bar itself grows both ways there.
   2
   3 cycleColors() now scales its interpolated result by
   4 ModeMath::brightnessScaleFor(silo->maxBrightnes) (0=unset/full),
   5 covering all three gradient-loop modes at once since they share that
   6 helper - same overall-dim convention as FIRE/NOISE/NOISE_RANDOM_COLOR.
   2
   3 recordAudioSample() used to nudge audioBias toward every single
   4 incoming sample (a plain per-sample EMA). Every audio-reactive mode
   5 (SOUND_2_LIGHT, VU_METER, VU_METER_PEAK, BASS_REACT) measures loudness
   6 as deviation from audioBias, so a loud or sustained sound could drag
   7 the bias toward itself with no protection - once corrupted, ordinary
   8 quiet audio afterward read as "loud" forever (frozen at peak) until
   9 the same tiny per-sample nudge slowly drifted back.
  10
  11 Replaced with periodic, gated recalibration: once a second, a
  12 candidate bias (the ring buffer's mean) is only committed if it's both
  13 internally quiet (low peak-to-peak spread) AND close to the current
  14 bias - the second check matters because a hard-clipped/rail-pinned
  15 loud signal is perfectly flat (zero spread) despite being nowhere near
  16 silence. A window that fails either check is skipped and retried next
  17 second, so the bias just holds at its last known-good value for as
  18 long as things stay loud.
  19
  20 ModeMath::recalibrateBias() is the new pure/testable helper (5 new
  21 native unit tests). Verified end-to-end with a standalone harness
  22 simulating quiet -> sustained hard-clipped loud -> quiet again:
  23 audioBias now stays anchored throughout instead of drifting to the
  24 clipped value.
   2
   3 lerpAmount (0.15 -> 0.04) was too fast relative to how often a new
   4 flicker target gets picked, especially at higher frequency where only
   5 a handful of ticks separate one pick from the next - current never
   6 had time to reach each target before the next one was picked, so it
   7 visibly stepped between palette entries instead of blending. Slowing
   8 the lerp fixes that at any frequency: max frame-to-frame delta in the
   9 GIF preview dropped from ~10 to 3, mean from ~4-5 to ~1.1.
  10
  11 Also regenerates 4 audio-reactive mode GIFs (sound2Light, bassReact,
  12 vuMeter, vuMeterPeak) that should have been refreshed in the previous
  13 audio-bias-recalibration commit but weren't - no further code changes,
  14 just catching the doc GIFs up to that already-committed fix.
…spacing

   2
   3 The trail used to be rendered by fadeToBlackBy() decaying whatever was
   4 already lit, plus setting only the single current head pixel each
   5 tick. That left gaps whenever the head moved more than one pixel
   6 between ticks (routine, depending on time/stripLength) - skipped
   7 pixels only ever got darker, never relit.
   8
   9 Fixed by rendering the whole trail explicitly every tick: walk
  10 backward from the head in length+1-pixel steps (a complete pack
  11 regardless of the head's actual per-tick movement), with brightness
  12 decaying per-step via sensitivity instead of per-tick. length (0-9,
  13 new) is the gap between trail steps - 0 gives a fully continuous pack
  14 with degrading brightness and no gaps, higher values space it into a
  15 sparser dotted look.
  16
  17 COMET_BACK_AND_FORTH additionally needs the new
  18 ModeMath::isBounceMovingForward() to know which side of the head to
  19 extend the trail on, since unlike COMET (always one direction) its
  20 trail has to flip sides whenever the head reverses. 4 new native unit
  21 tests for isBounceMovingForward(); verified the resulting trails via
  22 the GIF harness (continuous at length=0, correctly gapped otherwise,
  23 and flipping sides on each bounce).
   2
   3 length (0-100) sets what fraction of the strip is lit in color,
   4 position picks the growth anchor (0=left, else=right - reuses
   5 mapLogicalToPhysical(), the same reversal helper fire() uses for its
   6 base anchor). The leading pixel blinks white at frequency Hz to draw
   7 the eye to it; frequency=0 disables the blink instead of being
   8 rejected, so it needs the new ModeMath::isValidOptionalRateHz() guard
   9 (0-60) rather than the usual isValidRateHz() (1-60) every other
  10 rate-based mode uses.
  11
  12 Verified via the GIF harness: correct percentage/lit-count, growth
  13 direction on both anchors, leading-pixel blink timing, and frequency=0
  14 genuinely disabling the blink.
…at-lined

   2
   3 Measuring loudness as deviation from the tracked silence bias
   4 (audioBias) - even after the earlier recalibration fix - still ended
   5 up flat-lining the reading at/near peak most of the time on real
   6 hardware. Rather than keep tuning the bias-tracking approach, dropped
   7 it entirely for these two modes: ModeMath::peakToPeakAmplitude() (half
   8 the ring buffer's own max-min spread) measures loudness without
   9 needing to know where the signal's DC center sits at all, so there's
  10 no bias to get wrong. sensitivity is now the only amplification knob,
  11 applied as a plain gain on top - no other automatic leveling.
  12
  13 5 new native unit tests for peakToPeakAmplitude(). Verified via the
  14 GIF harness that the bar now visibly rises and falls with the
  15 synthetic envelope at a reasonable gain, instead of saturating
  16 immediately and staying there.
  17
  18 SOUND_2_LIGHT/BASS_REACT (50/51) still use the older bias-relative
  19 approach - revisit them the same way if they show the same symptom.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant