Commit Graph
163 Commits
Author SHA1 Message Date
imonlinux fed867936a Update xvf3800_button_controller.py
Fix physical mute button not illuminating the led ring with the Mute State.
2026-01-10 11:50:02 -08:00
imonlinux 05817ceb58 Update config.json.example
Added volume_sync and max_volume_percent options.
2026-01-06 13:46:32 -08:00
imonlinux 010e5447df Update __main__.py
Add volume max percentage and config for volume sync.
2026-01-06 13:37:00 -08:00
imonlinux 6866358dfe Update config.py
Add volume max percentage and config for volume sync.
2026-01-06 13:36:28 -08:00
imonlinux 7891997392 Update audio_volume.py
Add volume max percentage and config for volume sync.
2026-01-06 13:35:52 -08:00
imonlinux cfc24f6b73 Update config.py
Fix self shot foot.
2026-01-06 12:03:44 -08:00
imonlinux 4da78c7435 Update mpv_player.py
What this fixes:

On boot, if PipeWire/Pulse starts with the sink at ~40%, LVA now pushes the sink volume to match the persisted LVA volume (0.0–1.0).

When you move the HA media_player volume slider, LVA now updates:

wpctl (PipeWire) if available

pactl (PulseAudio / PipeWire-Pulse) if available

amixer (ALSA) as a fallback

No reboot / alsactl store required — LVA simply re-applies the persisted volume at start.
2026-01-05 21:52:41 -08:00
imonlinux 8a9939cf62 Update __main__.py
What this fixes:

On boot, if PipeWire/Pulse starts with the sink at ~40%, LVA now pushes the sink volume to match the persisted LVA volume (0.0–1.0).

When you move the HA media_player volume slider, LVA now updates:

wpctl (PipeWire) if available

pactl (PulseAudio / PipeWire-Pulse) if available

amixer (ALSA) as a fallback

No reboot / alsactl store required — LVA simply re-applies the persisted volume at start.
2026-01-05 21:52:02 -08:00
imonlinux eb4c4c26c8 Update config.py
What this fixes:

On boot, if PipeWire/Pulse starts with the sink at ~40%, LVA now pushes the sink volume to match the persisted LVA volume (0.0–1.0).

When you move the HA media_player volume slider, LVA now updates:

wpctl (PipeWire) if available

pactl (PulseAudio / PipeWire-Pulse) if available

amixer (ALSA) as a fallback

No reboot / alsactl store required — LVA simply re-applies the persisted volume at start.
2026-01-05 21:51:21 -08:00
imonlinux a1b460391d Add audio_volume.py
This helper script keeps the LVA media player volume and the OS device volume synchronized.
2026-01-05 21:46:15 -08:00
imonlinux b8a3ce2c39 Update xvf3800_led_backend.py
Extends the existing XVF3800 USB control backend beyond LEDs to support startup preflight commands needed for audio reliability.

Implements command execution internally (no reliance on xvf_host.py) using the same USB control-transfer approach already used for LED features.
2026-01-05 18:57:52 -08:00
imonlinux 3ba4e748ee Update __main__.py
Adds XVF3800-specific initialization flow during controller startup.

Ensures the XVF3800 preflight runs at the right time so audio initialization sees the correct device state.
2026-01-05 18:56:37 -08:00
imonlinux 5315c56556 Add per LED support
Added support for the XVF3800 per-LED command LED_RING_COLOR (12-LED ring, 48-byte payload).

Implemented/readied the device control logic so the backend can:

write per-LED frames (set_ring_rgb / set_ring_solid / clear_ring),

detect availability (supports_per_led) and fall back if unsupported.

Improved robustness/diagnostics for USB control transfers (more consistent request composition + debug logging around writes).
2026-01-01 15:18:54 -08:00
imonlinux a7a4f47f52 Add per LED control
Prevented MQTT command/state feedback loops while keeping HA-friendly behavior:

accepts retained /state topics during a short startup bootstrap window to restore last-known settings,

then stops processing /state messages and unsubscribes from .../+/state after bootstrap.

Ensures runtime control is driven only by /set topics, while /state remains publish-only for HA UI synchronization.

Adds explicit logging around bootstrap completion to make startup behavior clear.
2026-01-01 15:17:41 -08:00
imonlinux 4f05312e16 Per LED Control Added
Integrated per-LED rendering into the existing effect/state pipeline:

solid/off uses ring frames directly,

blink toggles between frames (color ↔ off),

pulse now rewrites LED_RING_COLOR with scaled RGB per step (instead of relying on global brightness),

spin uses a single “dot” moving around the 12 LEDs.

Ensured the controller uses the XVF3800’s fixed ring size (12) where appropriate.

Fixed the “stuck in Responding” issue by subscribing the voice_idle handler so Idle effects reapply correctly when returning to IDLE.
2026-01-01 15:16:13 -08:00
imonlinux 77c2299453 Update audio_engine.py
reverting the revert
2025-12-22 11:46:09 -08:00
imonlinux ae9b834ee3 Update audio_engine.py
Revert refractory bug fix
2025-12-22 09:54:01 -08:00
imonlinux 670cd52725 Merge pull request #50 from imonlinux/refractory-rejection-loop_#48
Update audio_engine.py issue: 48
2025-12-20 13:35:34 -08:00
imonlinux 46c39c6ae5 Update config.json.example
cleanup and restore of missing options
2025-12-12 15:18:39 -08:00
imonlinux ab99019d42 Update entity.py
Fix play status sync error between mpv and HA when skipping tracks.
2025-12-11 18:47:01 -08:00
imonlinux 7abce1060d Update mpv_player.py
Fix play status sync error between mpv and HA when skipping tracks.
2025-12-11 18:46:31 -08:00
imonlinux 8187760b39 Update mqtt_controller.py 2025-12-11 11:25:59 -08:00
imonlinux 70079d6a61 Update config.py
Bug fix.
2025-12-09 07:57:02 -08:00
imonlinux 957d3f2839 Update to include XVF3800 Support 2025-12-08 15:21:49 -08:00
imonlinux ae8f1c5c3a Add legacy XVF3800 LED Controls
Minimal pyusb-based wrapper focused on the legacy LED controls only
2025-12-08 14:08:01 -08:00
imonlinux 3d7cc49466 Add support for XVF380 LEDs
Keeps all existing DotStar/NeoPixel behavior intact.

Adds a USB-based XVF3800 backend using the legacy LED controls (LED_EFFECT, LED_BRIGHTNESS, LED_SPEED, LED_COLOR).

Plugs it into LedController by treating "led_type": "xvf3800", "interface": "usb" as a new backend.
2025-12-08 14:06:45 -08:00
imonlinux 59d8071384 Delete linux_voice_assistant/xvf3800_controller.py
Duplicate file...
2025-12-08 11:34:22 -08:00
imonlinux e845c5c811 XVF3800 Mute Button Support
This is the same design as earlier, but now explicitly expects button_config.mode/poll_interval_seconds to exist (with defaults from ButtonConfig).
2025-12-08 11:33:21 -08:00
imonlinux a52b503cdb XVF3800 Mute Button Support
linux_voice_assistant/config.py – adds button.mode + poll_interval_seconds
2025-12-08 11:31:18 -08:00
imonlinux 13aad43c29 XVF3800 Mute Button Support
Import XVF3800ButtonController.

In _init_controllers, choose between ButtonController and XVF3800ButtonController based on config.button.mode.
2025-12-08 11:29:01 -08:00
imonlinux 8d135dc51b Update audio_engine.py issue: 48
Behavioral changes:

Each wake word has its own timer.

Rapid repeats of the same phrase within the refractory window still get rejected (which is desirable).

A misfire of “Hey Jarvis” no longer locks out “Okay Nabu” immediately afterward.

If a user really wants to disable the gating, they can set:

"wake_word": {
  "refractory_seconds": 0
}


and this code will treat it as “no refractory” (max(..., 0.0) above).
2025-12-06 19:32:29 -08:00
imonlinux 7305a289a0 Add files via upload
What’s in it:

Xvf3800RuntimeConfig – simple dataclass for VID/PID, timeouts, etc.

Xvf3800Controller(EventHandler) – structured very similar to your other controllers:

On init:

Checks enabled and hid availability.

Uses hid.enumerate() to find the XVF3800 by VID/PID.

Opens the first HID interface (non-blocking).

Starts a background thread _poll_loop().

_poll_loop():

Reads HID reports (read(64, timeout_ms=...)).

For each non-empty report:

Calls _parse_hw_mute_from_report(report) → Optional[bool].

If it returns True/False and the value changed, it publishes:

self.loop.call_soon_threadsafe(
    self.event_bus.publish,
    "set_mic_mute",
    {"state": new_state},
)


Exits cleanly when self._running is false or state.shutdown is true.

@subscribe def set_mic_mute(self, data: dict):

Listens to the same set_mic_mute event MQTT and ButtonController use.

Updates _last_hw_mute_state.

Calls _send_hw_mute_state(desired) to push mute+LED down to hardware.

Placeholders:

def _parse_hw_mute_from_report(self, report: bytes) -> Optional[bool]:
    # TODO: implement once we know the report layout
    _LOGGER.debug("XVF3800 HID report (len=%d): %s", len(report), report.hex())
    return None

def _send_hw_mute_state(self, muted: bool) -> None:
    # TODO: implement once we know output report format
    _LOGGER.debug("(stub) Would send HID command to set XVF3800 mute=%s", muted)
2025-12-06 18:34:54 -08:00
imonlinux 186f33f1a1 Update config.json.example
Add clarity for using NeoPixel LEDs with Grove Port interface.
2025-11-30 12:31:34 -08:00
imonlinux 57b20a1ef1 Update button_controller.py
Fixed bug when running on a non-RPi board.
2025-11-30 00:49:59 -08:00
imonlinux a9cf822136 Update satellite.py
Extended the satellite state machine with a manual wake entrypoint:

Added manual_wakeup(self, phrase: str = "button"):

Mirrors the semantics of wakeup(), but does not require a wake-word model instance.

Respects the current state: only triggers from STARTING or IDLE.

If a timer alarm is ringing, calls the existing _stop_timer_alarm(...) instead of starting a new conversation (consistent with wake-word behavior).

Otherwise calls a shared helper _start_conversation(phrase).

Introduced _start_conversation(wake_word_phrase: str) to encapsulate:

Sending VoiceAssistantRequest(start=True, wake_word_phrase=...),

Transitioning to LISTENING,

Setting _is_streaming_audio = True,

Playing the configured wakeup_sound via tts_player.

Kept prior timer/alarm logic intact (including auto stop and Stop wake word behavior), so the button integrates cleanly with existing timer and TTS flows.
2025-11-29 23:43:08 -08:00
imonlinux 0073dc6a23 Update __main__.py
Imported the new ButtonController.

In _init_controllers():

Instantiated ButtonController when config.button.enabled is True.

Passed loop, event_bus, state, and config.button.

Stored the instance on state.button_controller to keep it alive for the lifetime of the process.

Left the rest of startup orchestration unchanged (audio engine, MQTT, LEDs, etc.), so the button is just an additional input surface plugged into the existing architecture.
2025-11-29 23:42:24 -08:00
imonlinux dc9049e899 Add button_controller.py
Introduced a new ButtonController responsible for handling the ReSpeaker (or generic GPIO) button:

Uses RPi.GPIO with BCM numbering and internal pull-up (PUD_UP), assuming an active-low button.

Starts a dedicated polling thread that:

Reads the GPIO level at a configurable interval (default 10 ms / 100 Hz),

Detects press and release transitions,

Computes press duration to classify short vs long press.

Short press behavior:

If state.tts_player.is_playing or state.music_player.is_playing:

Schedules state.satellite.stop() on the asyncio loop,

Also stops music via state.music_player.stop().

Otherwise:

Schedules state.satellite.manual_wakeup("button") to start a new voice interaction (wake-like behavior without needing a model instance).

Long press behavior:

Toggles mic mute using the existing event path:

Publishes set_mic_mute on the EventBus with {"state": not state.mic_muted}.

This ensures MicMuteHandler remains the single writer for mic state, updates mic_muted_event, and syncs MQTT mute state/LEDs.

Designed to fail gracefully:

If ButtonConfig.enabled is False or RPi.GPIO is unavailable, logs a message and disables button support without affecting the rest of the system.
2025-11-29 23:41:24 -08:00
imonlinux edc66183c3 Update config.py
Added a new ButtonConfig dataclass:

enabled: bool = False

pin: int = 17 (BCM pin for the button)

long_press_seconds: float = 1.0

Extended the main Config dataclass with a button: ButtonConfig field.

Updated load_config_from_json() to construct button_config from the "button" section in config.json.

This allows enabling/disabling the button, changing the GPIO pin, and tuning the long-press threshold (and optional poll interval) without code changes.
2025-11-29 23:40:16 -08:00
imonlinux 0e928bd7b3 Update satellite.py
Added internal tracking for an alarm auto-stop:

self._timer_auto_stop_handle: Optional[asyncio.TimerHandle]

On VOICE_ASSISTANT_TIMER_FINISHED:

enables the Stop wake word,

starts the repeating timer_finished_sound alarm as before,

reads state.preferences.alarm_duration_seconds and, if > 0, schedules an auto-stop callback after that duration.

Centralized alarm stopping in helper methods:

_clear_timer_auto_stop() – cancels any pending auto-stop timer.

_stop_timer_alarm(reason: str) – stops the alarm sound, clears flags, unducks music, and removes the Stop wake word from active_wake_words.

_auto_stop_timer_alarm() – invoked by the scheduled timer, calls _stop_timer_alarm("auto_timeout").

Updated behavior paths:

wakeup() now stops a ringing alarm instead of starting a new conversation if a timer is active.

stop() (Stop wake word) stops a ringing alarm first; only falls back to stopping TTS if no alarm is active.

_play_timer_finished() continues to loop the sound until _timer_finished is cleared, then unducks and exits cleanly.
2025-11-29 14:39:46 -08:00
imonlinux 0025fc781f Update mqtt_controller.py
Added MQTT topics for the new alarm duration control:

lva/<device_id>/alarm_duration/set

lva/<device_id>/alarm_duration/state

Published a new Home Assistant number discovery config:

number.<device_id>_alarm_duration (0–3600 seconds, step 5, unit s).

On /alarm_duration/set:

validates and parses the payload as a non-negative integer,

publishes a set_alarm_duration event on the EventBus,

echoes the value back to /alarm_duration/state.

On connect, publishes the current alarm_duration_seconds from preferences to the state topic for retained startup behavior.
2025-11-29 14:38:52 -08:00
imonlinux 5da1dd4bc4 Update models.py
Updated Preferences dataclass with a new field:

alarm_duration_seconds: int = 0

Kept ServerState.save_preferences() unchanged so alarm_duration_seconds is automatically serialized into preferences.json.
2025-11-29 14:38:13 -08:00
imonlinux b43b036198 Update __main__.py
Extended Preferences loading to initialize and backfill a new alarm_duration_seconds field (default 0).

Added a set_alarm_duration event handler to MicMuteHandler which:

validates and normalizes the duration value,

updates state.preferences.alarm_duration_seconds,

persists the updated preferences to disk.

Kept the existing “single writer” pattern (MicMuteHandler) for preferences/state updates.
2025-11-29 14:37:18 -08:00
imonlinux e13cc31e5b Update satellite.py
Fix bug with no stt after wake word detection.
2025-11-24 15:14:15 -08:00
imonlinux baa6f9080f Update satellite.py
Fix wake word change bug
2025-11-22 16:11:16 -08:00
imonlinux f3ae02da0d Update util.py
Revert MAC address detection logic.
2025-11-21 17:17:56 -08:00
imonlinux d2a133191b Update __main__.py
File: linux_voice_assistant/__main__.py

In _init_basics(), the --list-output-devices block is updated so that:

It exits with 0 on success.

It exits with 1 only if an exception occurs.
2025-11-21 17:00:25 -08:00
imonlinux 8d474acdab Update led_controller.py
Wrap the board import in a try/except and introduce a _BOARD_AVAILABLE flag.

If board (or Blinka/platformdetect) is not available or fails, log a clear warning and skip hardware LED initialization.

Keep the event subscriptions intact, but ensure all LED actions become no-ops when _is_ready is False.
2025-11-21 16:59:04 -08:00
imonlinux 93ceffebae Update mpv_player.py
Removed the home-grown backend probing / priority logic (pipewire → pulse → alsa) when no device is explicitly configured.

Kept support for explicitly specifying an audio_output_device in the config/CLI; if provided, it is passed through to mpv.

Added debug logging after mpv initialization to show:

current ao

current audio-device

the full audio-device-list
2025-11-21 11:00:03 -08:00
imonlinux adbb37e731 Update __main__.py
New CLI helpers

    Added support for:

    script/run --list-input-devices
    script/run --list-output-devices

    These code paths:

        Initialize just enough of the audio stack to enumerate devices.

        Print a clean, human-readable list of input/output devices (including PipeWire / PulseAudio virtual devices like Echo-Cancel Source and pipewire/echo-cancel-sink).

        Exit cleanly with status 0 instead of trying to start the full LVA process afterward (fixing the previous CalledProcessError when --list-output-devices was used).

Behavior change

    __main__.py now treats the “list devices” modes as early-exit utility modes, not “run LVA and then also print devices”.

    This makes it practical to use these commands in setup docs and shell scripts.
2025-11-20 20:02:23 -08:00
imonlinux 0151b64810 Update audio_engine.py
Behavioral & logging changes

Improved the wake word processing loop to make the following clearly visible in DEBUG logs:

The current set of active wake word IDs (MWW/OWW).

When the Stop wake word:

is activated by the model,

is accepted (i.e. meets refractory timing + is currently allowed), and

is ignored because it is no longer in active_wake_words.

Ensured the special stop model:

Is only treated as “active” when state.stop_word.is_active is True.

Is flipped back to inactive immediately after satellite.stop() is called, so:

The first “Stop” that passes all checks cancels playback and alarm tones.

Subsequent detections in the same moment appear as “TRIGGERED but IGNORED” (expected and now clearly logged).

Why this matters

When the LVA is playing audio (TTS or timer alarm), you can now:

See exactly when the Stop model fires.

Confirm that playback is being canceled at the right time.

Understand why you might see multiple “Stop wake word ACTIVATED” messages even though the alarm already stopped (model still “hearing” the tail, but logically ignored).

This matches the behavior seen in your logs:

At lower volumes / with AEC, stop is reliably accepted and playback stops immediately.

At high volumes without AEC, the model sees far less “clean” speech, so activations are rare or absent.
2025-11-20 20:01:13 -08:00