-
Notifications
You must be signed in to change notification settings - Fork 25
Refactor connect-omi.py for improved device selection and user intera… #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
44b3b7f
3a5b242
55da001
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -303,6 +304,17 @@ async def open_conversation_job( | |||||||||||||||||||||||||||||||||||||||||||
| conversation_id = conversation.conversation_id | ||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"✅ Created streaming conversation {conversation_id} for session {session_id}") | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Attach markers from Redis session (e.g., button events captured during streaming) | ||||||||||||||||||||||||||||||||||||||||||||
| markers_json = await redis_client.hget(session_key, "markers") | ||||||||||||||||||||||||||||||||||||||||||||
| if markers_json: | ||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||
| markers_data = markers_json if isinstance(markers_json, str) else markers_json.decode() | ||||||||||||||||||||||||||||||||||||||||||||
| conversation.markers = json.loads(markers_data) | ||||||||||||||||||||||||||||||||||||||||||||
| await conversation.save() | ||||||||||||||||||||||||||||||||||||||||||||
| logger.info(f"📌 Attached {len(conversation.markers)} markers to conversation {conversation_id}") | ||||||||||||||||||||||||||||||||||||||||||||
| except Exception as marker_err: | ||||||||||||||||||||||||||||||||||||||||||||
| logger.warning(f"⚠️ Failed to parse markers from Redis: {marker_err}") | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+307
to
+316
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 Fix: define session_key before use # Attach markers from Redis session (e.g., button events captured during streaming)
+ session_key = f"audio:session:{session_id}"
markers_json = await redis_client.hget(session_key, "markers")
if markers_json:
try:
markers_data = markers_json if isinstance(markers_json, str) else markers_json.decode()
conversation.markers = json.loads(markers_data)
await conversation.save()
logger.info(f"📌 Attached {len(conversation.markers)} markers to conversation {conversation_id}")
- except Exception as marker_err:
+ except (json.JSONDecodeError, UnicodeDecodeError) as marker_err:
logger.warning(f"⚠️ Failed to parse markers from Redis: {marker_err}")Note: The 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.14.14)[error] 308-308: Undefined name (F821) [warning] 315-315: Do not catch blind exception: (BLE001) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Link job metadata to conversation (cascading updates) | ||||||||||||||||||||||||||||||||||||||||||||
| current_job.meta["conversation_id"] = conversation_id | ||||||||||||||||||||||||||||||||||||||||||||
| current_job.save_meta() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2024 Chronicle AI Contributors | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| This package is derived from the OMI Python SDK: | ||
| https://github.com/BasedHardware/omi/tree/main/sdks/python | ||
|
|
||
| Original work: Copyright (c) 2024 Based Hardware Contributors | ||
| Licensed under the MIT License. | ||
|
|
||
| Fork: https://github.com/AnkushMalaker/Friend |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # friend-lite-sdk | ||
|
|
||
| Python SDK for OMI / Friend Lite BLE devices — audio streaming, button events, and transcription. | ||
|
|
||
| Derived from the [OMI Python SDK](https://github.com/BasedHardware/omi/tree/main/sdks/python) (MIT license, Based Hardware Contributors). See `NOTICE` for attribution. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| pip install -e extras/friend-lite-sdk | ||
| ``` | ||
|
|
||
| With optional transcription support: | ||
|
|
||
| ```bash | ||
| pip install -e "extras/friend-lite-sdk[deepgram,wyoming]" | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```python | ||
| import asyncio | ||
| from friend_lite import OmiConnection, ButtonState, parse_button_event | ||
|
|
||
| async def main(): | ||
| async with OmiConnection("AA:BB:CC:DD:EE:FF") as conn: | ||
| await conn.subscribe_audio(lambda _handle, data: print(len(data), "bytes")) | ||
| await conn.wait_until_disconnected() | ||
|
|
||
| asyncio.run(main()) | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| from .bluetooth import OmiConnection, listen_to_omi, print_devices | ||
| from .button import ButtonState, parse_button_event | ||
| from .uuids import ( | ||
| OMI_AUDIO_CHAR_UUID, | ||
| OMI_BUTTON_CHAR_UUID, | ||
| OMI_BUTTON_SERVICE_UUID, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "ButtonState", | ||
| "OMI_AUDIO_CHAR_UUID", | ||
| "OMI_BUTTON_CHAR_UUID", | ||
| "OMI_BUTTON_SERVICE_UUID", | ||
| "OmiConnection", | ||
| "listen_to_omi", | ||
| "parse_button_event", | ||
| "print_devices", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import asyncio | ||
| from typing import Callable, Optional | ||
|
|
||
| from bleak import BleakClient, BleakScanner | ||
|
|
||
| from .uuids import OMI_AUDIO_CHAR_UUID, OMI_BUTTON_CHAR_UUID | ||
|
|
||
|
|
||
| def print_devices() -> None: | ||
| devices = asyncio.run(BleakScanner.discover()) | ||
| for i, d in enumerate(devices): | ||
| print(f"{i}. {d.name} [{d.address}]") | ||
|
|
||
|
|
||
| class OmiConnection: | ||
| def __init__(self, mac_address: str) -> None: | ||
| self._mac_address = mac_address | ||
| self._client: Optional[BleakClient] = None | ||
| self._disconnected = asyncio.Event() | ||
|
|
||
| async def __aenter__(self) -> "OmiConnection": | ||
| await self.connect() | ||
| return self | ||
|
|
||
| async def __aexit__(self, exc_type, exc, tb) -> None: | ||
| await self.disconnect() | ||
|
|
||
| async def connect(self) -> None: | ||
| if self._client is not None: | ||
| return | ||
|
|
||
| def _on_disconnect(_client: BleakClient) -> None: | ||
| self._disconnected.set() | ||
|
|
||
| self._client = BleakClient( | ||
| self._mac_address, | ||
| disconnected_callback=_on_disconnect, | ||
| ) | ||
| await self._client.connect() | ||
|
Comment on lines
+28
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
🐛 Proposed fix — reset client on connection failure async def connect(self) -> None:
if self._client is not None:
return
def _on_disconnect(_client: BleakClient) -> None:
self._disconnected.set()
self._client = BleakClient(
self._mac_address,
disconnected_callback=_on_disconnect,
)
- await self._client.connect()
+ try:
+ await self._client.connect()
+ except Exception:
+ self._client = None
+ raise🤖 Prompt for AI Agents |
||
|
|
||
| async def disconnect(self) -> None: | ||
| if self._client is None: | ||
| return | ||
| await self._client.disconnect() | ||
| self._client = None | ||
| self._disconnected.set() | ||
|
|
||
| async def subscribe_audio(self, callback: Callable[[int, bytearray], None]) -> None: | ||
| await self.subscribe(OMI_AUDIO_CHAR_UUID, callback) | ||
|
|
||
| async def subscribe_button(self, callback: Callable[[int, bytearray], None]) -> None: | ||
| await self.subscribe(OMI_BUTTON_CHAR_UUID, callback) | ||
|
|
||
| async def subscribe(self, uuid: str, callback: Callable[[int, bytearray], None]) -> None: | ||
| if self._client is None: | ||
| raise RuntimeError("Not connected to OMI device") | ||
| await self._client.start_notify(uuid, callback) | ||
|
|
||
| async def wait_until_disconnected(self, timeout: float | None = None) -> None: | ||
| if timeout is None: | ||
| await self._disconnected.wait() | ||
| else: | ||
| await asyncio.wait_for(self._disconnected.wait(), timeout=timeout) | ||
|
|
||
|
|
||
| async def listen_to_omi(mac_address: str, char_uuid: str, data_handler) -> None: | ||
| """Backward-compatible wrapper for older consumers.""" | ||
| async with OmiConnection(mac_address) as conn: | ||
| await conn.subscribe(char_uuid, data_handler) | ||
| await conn.wait_until_disconnected() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """Button event parsing for Omi BLE button characteristic.""" | ||
|
|
||
| import struct | ||
| from enum import IntEnum | ||
|
|
||
|
|
||
| class ButtonState(IntEnum): | ||
| IDLE = 0 | ||
| SINGLE_TAP = 1 | ||
| DOUBLE_TAP = 2 | ||
| LONG_PRESS = 3 | ||
| PRESS = 4 | ||
| RELEASE = 5 | ||
|
|
||
|
|
||
| def parse_button_event(data: bytes) -> ButtonState: | ||
| """Parse the button event payload into a ButtonState. | ||
|
|
||
| Payload is two little-endian uint32 values: [state, 0]. | ||
| """ | ||
| if len(data) < 8: | ||
| raise ValueError(f"Expected 8 bytes for button event, got {len(data)}") | ||
| state, _unused = struct.unpack("<II", data[:8]) | ||
| return ButtonState(state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: SimpleOpenSoftware/chronicle
Length of output: 4063
🏁 Script executed:
Repository: SimpleOpenSoftware/chronicle
Length of output: 4243
🏁 Script executed:
Repository: SimpleOpenSoftware/chronicle
Length of output: 7187
Markers clearing in websocket_controller confirmed, but timeout path introduces potential race condition.
The websocket_controller does clear
client_state.markers(at lines 537 and 1158) after persisting them to Redis, confirming external coordination. However, the timeout path at line 72 callsstart_new_conversation()viaasyncio.create_task()without blocking, meaning the websocket_controller's finalization may not have run yet when a new conversation begins, allowing markers from the previous conversation to accumulate.Since
close_current_conversation()is documented as "legacy V1 code" (lines 139–140) and doesn't participate in the websocket_controller's finalization flow, markers must be cleared explicitly instart_new_conversation()before resetting conversation state:🤖 Prompt for AI Agents