Skip to content

Leds and buttons events#266

Merged
omaramin-2000 merged 164 commits into
OHF-Voice:mainfrom
omaramin-2000:leds-and-buttons-events
Jul 3, 2026
Merged

Leds and buttons events#266
omaramin-2000 merged 164 commits into
OHF-Voice:mainfrom
omaramin-2000:leds-and-buttons-events

Conversation

@omaramin-2000

@omaramin-2000 omaramin-2000 commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

Use events and commands to control custom onboard leds and buttons. Can be used to communicate with an external Python script via a WebSocket API running in a container alongside LVA.

@TheBest-Name

Copy link
Copy Markdown

I'm not sure if this is the right place to ask, but how would the external Python script work?

How would it also work if you have to repull down the container, for updating and the like?

Comment on lines +287 to +290
# When TTS is playing, keep _pipeline_active = True to block
# false wake word detections from speaker audio feedback.
# _tts_finished() callback will clear it when playback ends.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You say:
When TTS is playing, keep _pipeline_active = True to block false wake word detections from speaker audio feedback.
Would that block the stop word too?

Sorry, I was just looking and got confused by the wording.

@omaramin-2000

Copy link
Copy Markdown
Collaborator Author

I'm not sure if this is the right place to ask, but how would the external Python script work?

How would it also work if you have to repull down the container, for updating and the like?

It should communicate to any script/container using its api.

@TheBest-Name

Copy link
Copy Markdown

Where would be the documentation for the API?

Also, thank you for this great work!

@TheBest-Name

TheBest-Name commented Jun 15, 2026

Copy link
Copy Markdown

I see, does that need this pull request in order to work or does that work just with LVA on its own?
It looks very cool.

@danielholm

Copy link
Copy Markdown

I see, does that need this pull request in order to work or does that work just with LVA on its own? It looks very cool.

I cloned this repo and build my own docker image

git clone https://github.com/omaramin-2000/linux-voice-assistant.git -b leds-and-buttons-events /tmp/lva-dev
cd /tmp/lva-dev
docker build -t lva-dev:leds-and-buttons .

Then (me using picomposer) modify /compose/.env and add:

LVA_IMAGE="lva-dev"
LVA_IMAGE_TAG="leds-and-buttons"

Then save the led script ( I used the respeaker 2MIC HAT) and run it.

There is a readme here: https://github.com/omaramin-2000/linux-voice-assistant/blob/leds-and-buttons-events/docs/peripheral_api.md

I added a systemd service to work on boot. I also had to uncomment gpio in the python script since I hadn’t enabled it elsewhere.

@TheBest-Name

Copy link
Copy Markdown

How would you go about updating the container though? Do you have to rebuild it every time?

Maybe that would be nice to have this also send a command to your script just to tell you that it has a update available. That way you could display an icon or something.

omaramin-2000 and others added 17 commits June 18, 2026 03:41
When the hangup button is pressed it sends the `STOP_MEDIA_PLAYER` command (if the satellite is idle)
* Emit volume_muted so peripherals can track mute state

LVA never broadcast the muted bool to peripheral_api clients. The
``muted`` event signalled "we just entered the muted display state"
but no event ever signalled "we just left it" — only the generic
``idle`` did, and peripherals can't infer mute state from an idle
event since idle fires in many other situations (post-TTS, post wake
word, after errors).

As a result the ReSpeaker 2-Mic peripheral script's cached ``muted``
flag flipped to True on the first mute and stayed True forever. Its
button handler keyed off that flag (``muted → unmute_mic``,
``unmuted → mute_mic``), so after the very first mute the button
could only ever emit ``unmute_mic``: the user could unmute but never
re-mute.

The ``volume_muted`` event was already declared in the protocol and
already handled by the peripheral script — it just had no emitter.
Hook one into ``_set_muted`` so every mute transition (both
directions) ships the boolean. The peripheral's existing handler
clears its cached ``muted`` and switches the animator state back to
IDLE automatically.

* Change event emitted for mute state

* Track unmute by reading the boolean on the muted event

c523617 switched LVA to emit `muted` carrying {"muted": bool} in place
of the dedicated `volume_muted` event. The ReSpeaker `muted` handler
still hardcoded `muted=True`, so it set the flag on mute but never
cleared it on unmute. The cached state stuck on, the muted indicator
stayed latched, and the button toggle fell out of sync after the first
mute (every later press read as already muted).

Read `data["muted"]` instead, mirroring the existing `volume_muted`
handler, and default to True so a bare `muted` event still reads as
muted. Update the peripheral_api docstring, the event and command
tables, and the reference handler in peripheral_api.md to document the
new payload.

* Drop the duplicate muted event on the mute path

---------

Co-authored-by: Omar Amin <65174210+omaramin-2000@users.noreply.github.com>
@omaramin-2000

Copy link
Copy Markdown
Collaborator Author

OK, let's merge this for now and see if it will have some further updates. Thanks to everyone who helps and shares their thoughts.

@TheBest-Name

Copy link
Copy Markdown

Great job!

@florian-asche florian-asche left a comment

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.

@omaramin-2000 can you please have a look at this?

Comment thread linux_voice_assistant/satellite.py
parser.add_argument(
"--preferences-file",
default=_REPO_DIR / "preferences.json",
help="Directory and file name for the file where the preferences are stored in JSON format",

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.

Why does this got reverted?

)
parser.add_argument(
"--host",
help="Optional host IP address to bind to (default: Autodetected by network interface)", # 0.0.0.0 is IPv4, None is all interfaces

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.

Why does this got reverted?

)
parser.add_argument(
"--network-interface",
help="Network interface the application will be listening on (default: will be automatically detected by gateway)",

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.

Why does this got reverted?

Comment thread linux_voice_assistant/__main__.py
parser.add_argument(
"--enable-thinking-sound",
action="store_true",
help="Enable thinking finish sound, when the assistant is done thinking and needed more time to process",

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.

Why does this got reverted?


while attempt <= max_attempts:
try:
server = await loop.create_server(lambda: VoiceSatelliteProtocol(state), host=host_ip_address, port=args.port)

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.

Why does this got reverted?

if err.errno == errno.EADDRINUSE:
message = "address already in use"
if attempt < max_attempts:
_LOGGER.warning("Attempt %d/%d failed to bind on address (%s, %s): %s. Retrying in 1 second...", attempt, max_attempts, host_ip_address, args.port, message)

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.

Why does this got reverted?

await asyncio.sleep(1)
attempt += 1
else:
_LOGGER.exception("All %d attempts failed to bind on address (%s, %s): %s", max_attempts, host_ip_address, args.port, message)

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.

Why does this got reverted?

process_audio_thread.start()

# Auto discovery (zeroconf, mDNS)
discovery = HomeAssistantZeroconf(port=args.port, name=state.name, mac_address=state.mac_address, host_ip_address=host_ip_address)

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.

Why does this got reverted?

@TheBest-Name

Copy link
Copy Markdown

Speaking of, is it possible that more features could be added on later other pull requests?

By other features, I of course mean features that add on to this feature or pull request.

@omaramin-2000

Copy link
Copy Markdown
Collaborator Author

Speaking of, is it possible that more features could be added on later other pull requests?

By other features, I of course mean features that add on to this feature or pull request.

Of course

@TheBest-Name

Copy link
Copy Markdown

Great!

There are some great features that might be nice to have but I'm glad this is getting released now so I can go ahead and start trying to script things.

@TheBest-Name

TheBest-Name commented Jul 5, 2026

Copy link
Copy Markdown

When do you think a new version of this will be released so I can use this?

Or is this already in the new version? That's been released.

@omaramin-2000

Copy link
Copy Markdown
Collaborator Author

When do you think a new version of this will be released so I can use this?

Or is this already in the new version? That's been released.

It will be available starting from the next release.

@tobuh

tobuh commented Jul 5, 2026

Copy link
Copy Markdown

Hi @omaramin-2000
Thanks for your great work. I wanted to test with my ReSpeaker USB Mic Array with XVF3000.

Unfortunately, I receive these errors:

(venv) pi@voice-1:~/linux-voice-assistant/examples/ReSpeaker Mic Array v2.0 (USB) $ python respeaker_usb_mic_array.py --host localhost --port 6055
2026-07-05 20:56:44,991  INFO      respeaker-usb  ReSpeaker Mic Array v2.0 found (USB 2886:0018, bus 1, addr 2)
2026-07-05 20:56:44,993  INFO      respeaker-usb  ReSpeaker Mic Array v2.0 controller started – connecting to ws://localhost:6055
2026-07-05 20:56:44,994  INFO      respeaker-usb  Connecting to LVA at ws://localhost:6055 …
2026-07-05 20:56:44,994  WARNING   respeaker-usb  USB write error: [Errno 32] Pipe error – attempting reconnect
2026-07-05 20:56:45,341  INFO      respeaker-usb  Connected to LVA peripheral API
2026-07-05 20:56:46,996  INFO      respeaker-usb  Attempting to reconnect to ReSpeaker USB device …
2026-07-05 20:56:46,999  INFO      respeaker-usb  ReSpeaker Mic Array v2.0 found (USB 2886:0018, bus 1, addr 2)
2026-07-05 20:56:47,034  WARNING   respeaker-usb  USB write error: [Errno 32] Pipe error – attempting reconnect
2026-07-05 20:56:49,036  INFO      respeaker-usb  Attempting to reconnect to ReSpeaker USB device …
2026-07-05 20:56:49,037  INFO      respeaker-usb  ReSpeaker Mic Array v2.0 found (USB 2886:0018, bus 1, addr 2)

Same with root. Any idea? Debug flag did not show any useful information here.

Using the demo project https://github.com/respeaker/pixel_ring.git mentioned in https://wiki.seeedstudio.com/ReSpeaker_Mic_Array_v2.0/ works.

Happy to help. Please let me know how.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A more integrated assistant Expose button entities to trigger/abort listening external function call feature request