# VoiceHelper 0.9.0

Visual-polish build based on the stable 0.8.14 crash-containment build.

New bundled PNG assets:
- `resources/media/mic-ready.png`
- `resources/media/mic-listening.png`
- `resources/media/mic-processing.png`
- `resources/media/mic-error.png`
- `icon.png` for the addon browser

VoiceOverlay now renders real transparent microphone artwork instead of the
temporary `●` placeholder. Each voice state gets its own matching icon.

No Bluetooth, STT, authentication, remote-routing, or crash-containment logic
was changed from 0.8.14.


## 0.9.1 - Warm persistent STT

- Keeps the authenticated STT WebSocket open across voice commands.
- Sends an RFC6455 ping every 5 seconds while STT is idle.
- Waits for the server's `ready` event before sending Xiaomi `MIC_OPEN`.
- Reconnects only after an actual STT transport/heartbeat failure.
- Allows up to 30 seconds for initial authorization because STT now pre-warms Whisper before declaring the connection authorized.


## v0.9.2

- Sends the first WebSocket ping immediately after authentication.
- Reduces idle STT heartbeat interval from 25 seconds to 5 seconds.
- Keeps the authenticated socket persistent across voice commands.


## 0.9.3 persistent WebSocket RX fix

0.9.2 sent heartbeat PING frames but did not continuously read the WebSocket
while VoiceHelper was idle. A standards-compliant WebSocket peer such as
Uvicorn/websockets can independently send PING frames. The client must read
those frames and answer with PONG; merely transmitting client-side PINGs is not
sufficient.

0.9.3 adds a dedicated STT receive thread which:
- continuously reads the authenticated WebSocket, including while idle
- immediately answers server PING with PONG
- consumes PONG replies to VoiceHelper's 5-second heartbeat
- queues application text/binary messages for the existing STT logic
- serializes WebSocket writes so audio/PING/PONG frames cannot interleave

The authenticated socket remains persistent across voice commands and is only
reconnected after a real transport failure.


## 0.9.4 — STT fail-open degraded mode

STT is no longer allowed to block or tear down remote-control functionality.

- Bluetooth/HID comes up independently of STT.
- STT connect/auth/model warm runs in a background recovery thread.
- STT is retried every 10 seconds while unavailable.
- Voice presses are skipped while STT is unavailable; normal remote buttons
  continue to work.
- A successful background reconnect automatically restores voice service.
- The existing 5-second authenticated WebSocket heartbeat resumes after recovery.
- Heartbeat/start/audio/final transport failures retire only the STT socket.
- No STT network operation is allowed to block the Xiaomi ATT/HID event loop.


## 0.9.5 — boot-time Bluetooth takeover guard

Fixes the startup/cold-boot failure where VoiceHelper could start before the
CoreELEC/Amlogic Bluetooth controller had finished initializing.

Changes:
- 8 second boot/startup grace before raw Bluetooth takeover.
- Polls btmgmt for a fully instantiated hci0 controller before stopping BlueZ.
- Raw STT behavior from 0.9.4 remains unchanged.
- If raw LE/SMP takeover fails at any stage, VoiceHelper now fails open:
  it starts bluetooth.service again, explicitly powers hci0 on, trusts the
  Xiaomi RC, and makes a best-effort normal reconnect.
- A failed voice initialization can no longer leave bluetoothd stopped with
  the physical remote unusable.

This addresses the pattern where disabling VoiceHelper restored the remote and
re-enabling it later succeeded simply because Bluetooth initialization had
finished by then.


## 0.9.6 — state-driven Bluetooth startup

Removes the fixed 8-second startup sleep.

VoiceHelper now waits on actual CoreELEC Bluetooth state:

1. `bluetooth.service` must report `active`.
2. `btmgmt info` must expose `hci0` as a Primary controller.
3. The controller state must pass 3 consecutive checks, 200 ms apart.

As soon as those conditions are satisfied, VoiceHelper begins raw Bluetooth
takeover immediately. This avoids both starting too early and wasting a fixed
delay when the controller is already ready.

If the controller never becomes stable within 30 seconds, VoiceHelper leaves
BlueZ untouched and disables only voice for that run. The normal remote remains
available.

The existing v0.9.5 fail-open BlueZ restoration remains in place for failures
that occur after takeover begins.


## 0.9.7 — exclusive Bluetooth ownership

0.9.6 boot logs proved the readiness gate worked, but systemd restarted
bluetooth.service about four seconds after VoiceHelper stopped it. BlueZ
reclaimed hci0 during raw ATT setup and the raw connection failed with EBUSY.

0.9.7:
- runtime-masks bluetooth.service before raw takeover
- stops bluetooth.service only after the mask is active
- prevents systemd/dependencies from silently restarting BlueZ
- unmask + restart BlueZ on initialization failure
- unmask + restart BlueZ on normal add-on shutdown/disable
- uses a runtime-only mask, so nothing persistent is changed on CoreELEC

The state-driven controller readiness gate from 0.9.6 remains intact.


## 0.9.8 — Kodi restart / stale runtime-mask recovery

0.9.7 correctly runtime-masks bluetooth.service while VoiceHelper owns hci0.
A Kodi process restart, however, can terminate the service add-on before its
Python finally-block runs. Because /run is not cleared by a Kodi restart, the
systemd mask survives:

    systemctl is-enabled bluetooth.service
    -> masked-runtime

while bluetooth.service remains inactive. The next VoiceHelper instance then
waits forever for a service that systemd cannot start.

0.9.8 adds startup preflight recovery:

- detect bluetooth.service == masked-runtime
- treat it as stale ownership from the prior VoiceHelper process
- unmask --runtime
- start bluetooth.service
- best-effort power the controller on
- wait for the normal stable hci0 readiness gate
- perform the standard raw takeover
- runtime-mask bluetooth.service again while VoiceHelper owns hci0

A real cold boot is unaffected because /run (and therefore the runtime mask) is
cleared by reboot.


## 0.9.9 - deterministic STT shutdown

- Handles SIGTERM/SIGINT from Kodi/service shutdown.
- Explicitly sends WebSocket CLOSE, then socket shutdown/close through the
  existing SimpleWebSocket.close() implementation before daemon exit.
- Keeps a process-wide reference to the currently authenticated warm STT
  transport solely so shutdown can retire it deterministically.
- Normal STT failures and normal daemon exit unregister/close the same socket.


## 0.9.10 - non-blocking Kodi shutdown

- Fixes a Kodi restart/reboot hang introduced by the 0.9.9 SIGTERM cleanup.
- SIGTERM no longer performs the graceful WebSocket CLOSE handshake.
- Process shutdown atomically detaches STT, then immediately calls
  socket.shutdown(SHUT_RDWR) and socket.close().
- The shutdown path does not acquire the WebSocket send lock and does not join
  the STT RX thread.
- Normal runtime STT disconnects still use the graceful close path.


## 0.9.11 - fail-open voice transaction recovery

- Explicitly terminates a pending Xiaomi ATVV voice transaction with MIC_CLOSE
  when STT is unavailable.
- Adds a centralized `abort_voice_session()` reset path.
- Clears mic/audio/codec/deadline state without tearing down Bluetooth/HID.
- Immediately re-arms ATT keepalive after an aborted voice request.
- Applies the same recovery if STT fails to reach READY or PCM transport dies
  during an active voice request.
- Background STT retry interval reduced from 10 seconds to 5 seconds.
- Normal remote buttons remain available while STT reconnects.


## 0.9.12 - automatic daemon recovery

- Automatically restarts the raw Bluetooth/ATT daemon after an unexpected exit.
- Reproduces the successful manual disable/re-enable recovery path internally.
- Waits for the prior daemon's BlueZ restore before retrying takeover.
- Retry backoff starts at 3 seconds and caps at 15 seconds.
- Healthy 30-second runs reset the backoff.
- Does not restart during Kodi shutdown.
- Retains 0.9.11 STT/ATVV fail-open recovery.


## 0.9.13 - local ATT readiness recovery

- Fixes the startup race where the Xiaomi LE ACL handle exists but the kernel
  fixed-CID ATT socket is not ready yet.
- Adds `attach_att_retry()` with a 3-second local retry window and 150 ms
  interval for transient errno 16 / 38 / 110 failures observed on CoreELEC.
- Verifies the Xiaomi LE connection is still present before each retry.
- If the LE link disappears, recovery fails immediately so the normal full
  Bluetooth reconnect path can take over.
- Adds a 100 ms settle after the HCI connection handle appears.
- Avoids repeated BlueZ stop/start and full SMP pairing cycles for a transient
  ATT readiness race.
- Retains 0.9.11 STT fail-open recovery and 0.9.12 daemon-level fallback.


## 0.9.14 - HID first, voice last

- Reorders startup so the ordinary Xiaomi remote is established before any
  ATVV voice setup.
- After raw LE + SMP/encryption, HID CCCDs are enabled immediately and the
  daemon logs `REMOTE READY`.
- ATVV service discovery now happens only after HID has been declared ready.
- STT/WebSocket startup remains asynchronous and is now explicitly the final
  startup stage after HID and ATVV.
- ATVV discovery/setup failures after `REMOTE READY` no longer cause Bluetooth
  teardown or re-pairing.
- Adds a persistent HID-only fallback loop on the same encrypted ATT link.
- In HID-only mode, normal buttons continue to work; Voice simply reports
  `Voice temporarily unavailable`.
- HID-only keepalive uses a generic GATT read and does not depend on ATVV.
- Retains 0.9.13 ATT readiness instrumentation/recovery as a physical-link
  fallback.


## 0.9.15 - hard STT / HID isolation

- Dedicated `VoiceHelper-STT-Worker` is now the sole owner of DNS, TCP/TLS,
  WebSocket authentication, READY/final waits, PCM writes and heartbeats.
- The Bluetooth/ATT/HID loop performs no WebSocket I/O.
- START_SEARCH queues an STT start request and immediately returns to ATT.
- MIC_OPEN is sent only after an asynchronous READY event arrives.
- PCM notifications are decoded and queued; network backpressure cannot block HID.
- END queues finalization; final transcription/command returns asynchronously.
- STT failure changes voice state only and never restarts Bluetooth.
- MIC_CLOSE_ABORT is never sent unless MIC_OPEN was actually sent first.
- Retains the HID-first startup architecture introduced in 0.9.14.


## 0.9.16 - BlueZ-free raw runtime + voice-ready admission
- Runtime uses kernel hci0 + raw AF_BLUETOOTH only.
- bluetooth.service remains masked/stopped; VoiceHelper never restores BlueZ.
- Startup waits for hci0, not bluetoothd.
- STT adds application probe/voice_ready readiness.
- Xiaomi START_SEARCH is accepted only with a recent voice_ready proof.


## 0.9.17 - preserve HID while asynchronous STT finalizes

- Removes the stale two-second global Bluetooth deadline after MIC_CLOSE.
- The raw ATT/HID loop now remains persistent while Whisper transcription
  finishes asynchronously.
- STT worker final-result allowance increased from 8 to 12 seconds.
- Final/error events fully reset only voice-session state and immediately
  re-arm HID keepalive.
- Fixes the observed sequence where the client closed WSS ~2 seconds after
  END while the STT server was still transcribing.
- Retains the BlueZ-free raw-kernel runtime and 0.9.16 voice_ready admission.


## 0.9.18 POC - deterministic command dispatch

This build intentionally removes media lookup/dispatch from the voice FINAL
path and replaces it with a single deterministic Kodi notification.

Expected proof:

    "Play ballerina"
      -> STT FINAL
      -> command='play', query='ballerina'
      -> Kodi notification: VOICE POC / PLAY: ballerina
      -> voice transaction resets
      -> HID remains active
      -> STT probes and returns voice_ready again

The POC dispatcher is exception-isolated so a Kodi action failure cannot kill
the Bluetooth/HID loop or STT worker. Once this is repeatable, the same hook
will be redirected to KodiFlix title lookup -> custom info page -> autoplay.


## 0.9.19 - voice -> KodiFlix info page

- Preserves the known-good 0.9.18 FINAL -> Kodi command seam.
- Replaces the POC notification with `script.voicehelper.bridge`.
- `play <title>` dispatches `action=info_autoplay`.
- `search <title>` dispatches `action=info`.
- No Bluetooth, HID, STT, WebSocket, or voice-ready lifecycle changes.


## 1.0.0-rc1

Release-candidate baseline derived directly from the known-good 0.9.19 build.

Functional change:
- Voice capture hard window reduced from 6.00 seconds to 4.75 seconds.

Intentionally unchanged:
- raw HCI / Bluetooth connection path
- HID routing and button mappings
- SMP / encryption handling
- ATVV MIC_OPEN / MIC_CLOSE lifecycle
- STT worker isolation and WebSocket behavior
- Bridge dispatch and command handling
- daemon recovery behavior

The hold-to-release experiment and timing-debug instrumentation are not
included in RC1.


## 1.0.5 development — portable hardware identity

Built directly from frozen 1.0.0-rc1.

Physical MAC hardcoding has been removed completely.

Remote identity:
- selected by the standalone KodiFlix VoiceHelper Setup add-on;
- stored only in addon_data/service.voicehelper/remote.json;
- a fresh install stays idle until remote.json exists.

Local adapter identity:
- discovered dynamically from hci0 via hciconfig on every daemon start;
- no Xiaomi box Bluetooth adapter address is packaged.

Remote profile:
- remote-map.json now contains only reusable Xiaomi RC protocol/button data;
- legacy `address` and `adapter` metadata are stripped from existing addon_data.

Supervisor:
- honors setup.lock so the setup program can own hci0 while scanning;
- immediately starts after setup writes remote.json;
- stops cleanly if the selected remote is forgotten;
- genuine runtime failures still use RC1's bounded automatic recovery.

Unchanged:
- raw HCI/SMP/ATT pairing path;
- Xiaomi HID/button profile;
- ATVV/STT/Bridge behavior;
- 4.75-second RC1 voice capture window.


## 1.0.7 development — simple settings + embedded web editor

Remote MAC is now a normal add-on setting.

Local Kodi:
Add-ons -> My add-ons -> Services -> KodiFlix VoiceHelper -> Configure

Browser:
http://<box-ip>:8877/
Health:
http://<box-ip>:8877/health

The web server is embedded directly in service.py. Both local Kodi settings and
the browser edit the same `remote_mac` value. The service polls the setting and
restarts only the VoiceHelper daemon when the MAC changes.

Legacy 1.0.5 remote.json is retained only as a one-time migration source.


## 1.0.8 development — initialization order fix

Fixes the service startup NameError from 1.0.7. The embedded web configuration
functions/classes are now fully defined before `start_web_config()` is invoked.

No change to Bluetooth, voice capture, STT, remote MAC normalization, local
settings, or the web configuration interface.


## 1.0.9 development — standalone daemon settings fix

Fixes daemon rc=1 recovery loop introduced by 1.0.7/1.0.8.

The raw Bluetooth daemon is launched with system `python3`, so it must not
import Kodi-only modules such as `xbmcaddon`.

New configuration flow:
- `service.py` reads and normalizes the Kodi `remote_mac` setting;
- service passes the MAC as `VOICEHELPER_REMOTE_MAC` in the daemon environment;
- `daemon.py` reads only that environment variable;
- daemon remains independent of Kodi's embedded Python module path.

Web config and local Kodi settings still edit the same `remote_mac` setting.


## 1.0.10 UI polish responsibility split

- VoiceHelper owns only the transient top microphone UI.
- LISTENING and PROCESSING are pre-rendered assets sliding from behind the mic circle.
- VoiceHelper closes the overlay after successful command handoff.
- VoiceHelper Bridge owns successful lower-right command confirmation.
- Bluetooth, raw ATT/SMP microphone, STT, and command-routing behavior are unchanged.


## 1.0.13 UI + Bridge polish
- Lower-left 1080p voice overlay.
- Rebuilt neutral-grey production mic/status assets with embedded soft shadows.
- Canonical Bridge 0.4.x command/category/title dispatch.
- INFO accepted as a first-class voice command.


## 1.0.14 approved voice UI

Production UI now matches the approved VoiceHelper mockup without alternate styling:
- compact lower-left 132 px mic control
- dark metallic mic with soft shadow
- blue glow/ring + blue waveform for LISTENING
- amber glow/ring + amber processing dots for PROCESSING
- dark glass status pill slides from behind the mic
- state-specific mic texture swaps without changing the voice/STT/bridge pipeline


## 1.0.15 — category passthrough regression fix

- Restores STT `category` passthrough to Bridge (`movie` / `tv`).
- Removes the accidental hard-coded `category=movie` introduced during UI/Bridge polish.
- Prefers STT `title`, with legacy `query` fallback.
- Adds parsed command/category/title logging at FINAL for diagnostics.
- UI assets and Bluetooth/STT transport behavior are unchanged from 1.0.14.

## 1.0.17
- Reverts the rejected 1.0.16 Kodi keymap override approach.
- Xiaomi TV+ is already a raw VoiceHelper button (`xiaomi_tv`, HID report `96 01 00 00 00 00`).
- That button now dispatches `RunScript(script.kodiflix.info)` directly.
- Kodi's semantic `Info` action, `DialogVideoInfo.xml`, and TMDb Helper Info are never invoked by this button.
- Existing `addon_data/service.voicehelper/button-actions.json` files are migrated only when `xiaomi_tv` still equals the historical `Info` value.
- No Kodi user keymap is created or managed by VoiceHelper.


## 1.0.19 - daemon residency / warm-STT diagnostics

- 1.0.18 volume experiment removed; volume behavior is restored to 1.0.17.
- Preserves the existing persistent STT worker architecture.
- Logs explicit persistent-STT worker startup/alive state.
- Relays daemon stdout/stderr into kodi.log so rc=1 failures expose their cause.
- Flags unexpected clean daemon returns (rc=0) and recovers immediately.
- Recovery UI now reports "Voice connection lost — reconnecting…".
- No arbitrary listening delay was added.


## 1.0.20
- Mic-start latency fix: ATVV `MIC_OPEN` is transmitted immediately when the Voice request arrives instead of waiting for the STT start/READY round trip.
- STT start now runs concurrently with remote microphone activation.
- Added a bounded pre-STT PCM buffer so the first syllable is retained even if ATVV audio begins before STT READY.
- `Listening…` now begins on actual `AUDIO_START`; `Starting voice…` covers only the pre-audio interval.
- Added millisecond transaction markers for Voice request, MIC_OPEN completion, STT start/READY, AUDIO_START, first audio packet, and first PCM queued to STT.
- Persistent warm STT lifecycle, HID routing, Bridge contract, resolver behavior, and remote mappings otherwise unchanged.
- Compose/YAML/secrets/STT server/CDN unchanged.

## 1.0.22 - Locked spoken operator dispatch
The first spoken words now own the operator contract: FIND ME -> search, SHOW ME -> info, PLAY -> play. STT command classification cannot override that contract.
