#pragma once #include "led_transport.h" #include #include #include #include #include #include #include #include #include // LedController — writer-thread owner for the backglow subsystem. // // Owns a single ILedTransport (injected at construction) and a dedicated // std::thread driven by a condition_variable with a 33 ms timeout (D-11). // Callers (RunFrame / HandlePipeCommand / future VRChat OSC bridge) stage // data via QueueFrame / QueueLedSet / QueueBrightness / QueuePower and never // perform serial I/O themselves. // // Safety (LHWD-02): brightness ceiling is applied inside WriterThreadFunc, // on a LOCAL COPY of the staged bytes, BEFORE transport->SendRgbFrame. This // is the defense-in-depth safety control per PITFALLS §5 — 10× WS2812B at // full white is ~3 W against the face. // // Shutdown (LHWD-03): ShutdownAllOff sends one all-black Adalight frame + // {"on":false} JSON synchronously from the calling thread, then joins the // writer. Idempotent. // // Connection state machine: 0 = disabled (no port / open failed), 1 = open, // 2 = reconnecting. Consumers read via GetConnectionState() for status. class LedController { public: // ceiling is the brightness cap in [1..255]; values outside the range are // clamped into it. transport is injected (normally WledSerialTransport). LedController(std::unique_ptr transport, uint8_t ceiling); ~LedController(); LedController(const LedController&) = delete; LedController& operator=(const LedController&) = delete; // Attempt to open the underlying transport with the given port string. // Always safe to call; if the port is empty or Open() fails, the controller // enters "disabled" state (connection = 0) and the writer thread idles. // Starts the writer thread if not already running. Idempotent w.r.t. the // writer thread — re-calling with a different port is not supported in // Phase 14 (Plan 14-03 hot-swap is a separate story). void Start(const std::string& portName); // Synchronously send one black Adalight frame + {"on":false}, then signal // the writer thread to stop and join it. Idempotent. void ShutdownAllOff(); // Update the runtime brightness ceiling (VRSettings-driven, D-12). // Applied to subsequent frames; does NOT re-send last frame. void SetCeiling(uint8_t ceiling); uint8_t GetCeiling() const; // Stage a full kBackglowMaxLeds-sized frame. Copies numLeds*3 bytes into // the staged buffer under mutex and signals the writer. Returns false if // numLeds != kBackglowMaxLeds or rgb is null. bool QueueFrame(const uint8_t* rgb, int numLeds); // Stage a single-LED change. index in [0..kBackglowMaxLeds-1]. Modifies // the staged buffer's 3 bytes at 3*index and signals the writer. Returns // false on out-of-range index. bool QueueLedSet(int index, uint8_t r, uint8_t g, uint8_t b); // Queue a JSON brightness change (ceiling applied in writer). Writer // enforces a minimum 20 ms gap to any adjacent write. bool QueueBrightness(uint8_t bri); // Queue an on/off. When off is queued, writer sends {"on":false}. // When on is queued and LEDs were off, writer sends {"on":true} before // the next Adalight frame. bool QueuePower(bool on); // Connection state. 0 = disabled (no port / open failed), 1 = open, // 2 = reconnecting. Reads are cheap (atomic load). int GetConnectionState() const; private: void WriterThreadFunc(); void ApplyCeilingInPlace(uint8_t* rgb, int numChannels, uint8_t ceiling); bool TryReopen(); std::unique_ptr m_transport; std::string m_portName; // written once in Start; read by writer (const-after-Start) // Staged frame + pending JSON ops, protected by m_bufMutex. std::mutex m_bufMutex; std::condition_variable m_cv; uint8_t m_staged[kBackglowMaxLeds * 3]{}; bool m_bDirty{false}; bool m_pendingBriSet{false}; uint8_t m_pendingBri{0}; bool m_pendingPowerSet{false}; bool m_pendingPower{false}; // Atomics. std::atomic m_ceiling{50}; std::atomic m_connState{0}; // 0=disabled, 1=open, 2=reconnecting std::atomic m_bPoweredOn{false}; // tracks last sent {"on":?}; D-16 starts OFF std::atomic m_bStop{false}; // Touched only by the writer thread. std::chrono::steady_clock::time_point m_lastWrite{}; std::chrono::steady_clock::time_point m_lastReopenAttempt{}; std::thread m_writer; };