Skip to content

LED indicator: led-bridge pulls lifecycle state from voice-command#14

Merged
Julie Krasnick (jckras) merged 7 commits into
mainfrom
voice-command-led-indicator
Jun 4, 2026
Merged

LED indicator: led-bridge pulls lifecycle state from voice-command#14
Julie Krasnick (jckras) merged 7 commits into
mainfrom
voice-command-led-indicator

Conversation

@jckras

@jckras Julie Krasnick (jckras) commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires up an LED indicator that mirrors voice-command's lifecycle, using an inverted (pull) dependency: the LED component depends on voice-command and reads its state via getStatus, rather than voice-command knowing about LEDs.

  • voice-command is now a pure status source. It records its current lifecycle state and exposes it via Status() (the getStatus RPC) under a "state" key. It has no knowledge of LEDs and never drives them. The four states:

    • idle — wake-mode, waiting for the wake word
    • listening — STT actively capturing (post wake-word, or in conversation follow-up)
    • thinking — LLM interpret in flight
    • responding — speaking via TTS
    • Footprint is intentionally minimal: one activityState field + a setActivity helper, a "state" entry in Status(), and four one-line transition markers (in interpret, speak, and listenForCommand).
  • viam:conversation-bundle:led-bridge is the puller. It depends on a configurable status_source, polls that resource's Status() on an interval (default 200ms), reads "state", and writes the state to the USB-serial LED firmware only when it changes. All LED opinion lives in led-bridge (actionForState); the firmware owns the visuals via a stable state-word contract over serial.

  • led-bridge reports serial-write health via its own Status(): serial_port, baud_rate, messages_sent, bytes_sent, last_sent_at, and last_error/last_error_at. Lets an operator confirm from the dashboard whether the LED is actually wired up and receiving data ("0 sent, last_error: port not found" vs "412 sent, 2s ago"). Counters use a dedicated lock so a status query never blocks behind a slow port.Write.

Config

{
  "serial_port": "/dev/ttyUSB0",
  "status_source": "voice_command"
}

Optional: state_key (default "state"), baud_rate (default 115200), poll_interval_ms (default 200).

state_key is the field led-bridge reads from status_source's Status(). It defaults to "state" (what voice-command emits), but can point at any other key — e.g. a source exposing "current_step" or "mode" — so one led-bridge can drive lights for a completely different flow without code changes.

Why pull instead of push

Decouples the two components. Any resource that exposes a "state" in Status() can drive the LEDs — point a second led-bridge at a different status_source for a different flow, with no changes to either component's internals. voice-command stays free of feature-specific LED logic, and a stuck serial port can never stall the voice loop (led-bridge polls on its own goroutine).

Wire-serialization note

RDK's GetStatus serializes with raw structpb.NewStruct, which rejects []string (and other typed slices) — it would error the entire call over the wire. voice-command's Status() now returns commands as []interface{} accordingly. Rule of thumb for any Status()/DoCommand return: scalars, map[string]interface{}, and []interface{} only; numbers come back as float64.

Companion firmware

The ESP32 sketch (WS2812B strip) lives outside this repo and is flashed to the indicator hardware. It substring-matches the state word on each serial line to pick an animation. The current sketch handles idle/listening/thinking; a responding mode will be added firmware-side (led-bridge already forwards it). Any other indicator can be driven by a generic resource that consumes the same state-word protocol.

Test plan

  • go build ./... and go vet pass
  • voice-command Status() and led-bridge Status() maps verified structpb-serializable (wire-safe)
  • led-bridge opens the configured serial port on Reconfigure and logs port + baud + status_source
  • led-bridge polls voice_command.getStatus() and writes to serial on state change only
  • led-bridge Status() shows messages_sent/last_sent_at advancing, last_error on a bad port
  • End-to-end: wake word → listening (blue pulse) → thinking (amber pulse) → back to listening on continued conversation → idle (dark) on conversation end
  • responding forwarded over serial (lights once firmware adds the mode)

Comment thread resources/voice-command/voice-command.go Outdated
Comment thread resources/voice-command/voice-command.go Outdated
Replace the push model (voice-command driving an led_indicator) with a
pull model:

- voice-command becomes a pure status source. It records its lifecycle
  state (idle/listening/thinking/responding) and exposes it via Status();
  it has no knowledge of LEDs. Minimal footprint: one field + a setActivity
  helper, a "state" entry in Status(), and four one-line transition markers.
- led-bridge becomes the puller. It depends on a configurable status_source,
  polls that resource's Status() on an interval, and writes the state to the
  serial LED firmware whenever it changes. All LED opinion lives here.

This decouples the two: any resource exposing a "state" in Status() can
drive the LEDs, and the firmware owns the visuals via a stable state-word
contract over serial.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jckras
Julie Krasnick (jckras) force-pushed the voice-command-led-indicator branch from aee99bb to 299a7d9 Compare June 4, 2026 15:15
@jckras Julie Krasnick (jckras) changed the title voice-command: optional LED indicator with listening/thinking states LED indicator: led-bridge pulls lifecycle state from voice-command Jun 4, 2026
Julie Krasnick (jckras) and others added 4 commits June 4, 2026 11:39
RDK's GetStatus serializes with raw structpb.NewStruct, which rejects
[]string list values. Status() returned commands as []string, so any
wire-level getStatus call (led-bridge polling, web-app, remote) would
error instead of returning the state. Convert commands to []interface{},
the same workaround beanjamin documents in its Status() methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Surface operational diagnostics so an operator can confirm from the
dashboard whether the LED is actually wired up and receiving data:
serial_port, baud_rate, messages_sent, bytes_sent, last_sent_at, and
last_error/last_error_at. Counters are recorded on every write and guarded
by a dedicated statsMu so a Status query never blocks behind a slow
port.Write.

Adapted from the diagnostics in the backup branch (PR #15), but exposed
directly via Status()/getStatus rather than a DoCommand shim, since
Status() is on the resource.Resource interface in rdk v0.122.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- tick() committed lastState before the serial write, so a failed write
  was never retried — the LED stayed wrong until the next state change.
  Now commit lastState only after a successful write (or a deliberate
  ignore), so a transient serial hiccup self-heals on the next tick.
- write() dedupes its failure log (warn only when the error changes) so a
  persistently broken port retried every tick doesn't flood the logs;
  Status() still surfaces the sticky last_error.
- Reframe actionForState: forwarding the state word is the intended design
  (firmware owns the visuals), not a placeholder; collapse the four
  identical cases into one allow-list arm.
- Remove references to a firmware/led-indicator/ dir and "firmware sketches
  in this repo" — the sketch is external, not in this repo.
- Fix meta.json short_description: described the old DoCommand-forward
  model; led-bridge now polls status_source.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
No behavior change — tighten the comments added in this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Comment thread resources/led-bridge/led-bridge.go Outdated
logger: logger,
port: port,
source: source,
poll: poll,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe pollingPeriod ?

Comment thread resources/led-bridge/led-bridge.go Outdated
b.logger.Warnw("status_source Status() failed", "err", err)
return
}
state, _ := status["state"].(string)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"state", the key to look at in the status response, should probably be a config value.

- Add a `state_key` config field (default "state") so led-bridge can read
  the lifecycle field from any status_source, not just sources that name it
  "state" — e.g. point it at a resource exposing "current_step" or "mode".
  Restores the "watch any source" decoupling the hardcoded key undercut.
- Rename the `poll` struct field to `pollInterval` for clarity, matching the
  existing PollIntervalMs config field and defaultPollInterval const.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jckras
Julie Krasnick (jckras) force-pushed the voice-command-led-indicator branch from e4df6c9 to b28f25a Compare June 4, 2026 20:14
@jckras
Julie Krasnick (jckras) merged commit c6a5547 into main Jun 4, 2026
6 checks passed
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.

3 participants