From 5cdef0d72eab85e8012bc67e9f1984d35003c6df Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Sun, 13 Apr 2025 22:02:06 -0400 Subject: [PATCH] wip: firehose mode --- colmi_r02_client/cli.py | 10 +++++ colmi_r02_client/client.py | 35 +++++++++++++++++- colmi_r02_client/firehose.py | 72 ++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 colmi_r02_client/firehose.py diff --git a/colmi_r02_client/cli.py b/colmi_r02_client/cli.py index be49cec..a8efa6a 100644 --- a/colmi_r02_client/cli.py +++ b/colmi_r02_client/cli.py @@ -198,6 +198,16 @@ async def reboot(client: Client) -> None: click.echo("Ring rebooted") +@cli_client.command() +@click.pass_obj +async def firehose(client: Client) -> None: + """Start firehose data dump for 5 seconds""" + + async with client: + await client.get_firehose() + click.echo("Firehose completed") + + @cli_client.command() @click.pass_obj @click.option( diff --git a/colmi_r02_client/client.py b/colmi_r02_client/client.py index 3f74e2e..3f14700 100644 --- a/colmi_r02_client/client.py +++ b/colmi_r02_client/client.py @@ -10,7 +10,19 @@ from bleak import BleakClient from bleak.backends.characteristic import BleakGATTCharacteristic -from colmi_r02_client import battery, date_utils, steps, set_time, blink_twice, hr, hr_settings, packet, reboot, real_time +from colmi_r02_client import ( + battery, + date_utils, + steps, + set_time, + blink_twice, + hr, + hr_settings, + packet, + reboot, + real_time, + firehose, +) UART_SERVICE_UUID = "6E40FFF0-B5A3-F393-E0A9-E50E24DCCA9E" UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" @@ -48,6 +60,7 @@ class FullData: hr.CMD_READ_HEART_RATE: hr.HeartRateLogParser().parse, set_time.CMD_SET_TIME: empty_parse, hr_settings.CMD_HEART_RATE_LOG_SETTINGS: hr_settings.parse_heart_rate_log_settings, + firehose.CMD_FIREHOSE: firehose.parse_firehose, } """ TODO put these somewhere nice @@ -105,7 +118,7 @@ def _handle_tx(self, _: BleakGATTCharacteristic, packet: bytearray) -> None: assert len(packet) == 16, f"Packet is the wrong length {packet}" packet_type = packet[0] - assert packet_type < 127, f"Packet has error bit set {packet}" + # assert packet_type < 127, f"Packet has error bit set {packet}" if packet_type in COMMAND_HANDLERS: result = COMMAND_HANDLERS[packet_type](packet) @@ -154,6 +167,7 @@ async def _poll_real_time_reading(self, reading_type: real_time.RealTimeReading) except TimeoutError: tries += 1 + # TODO, probably make this a try / finally so we do our best to stop await self.send_packet(stop_packet) if error: return None @@ -257,3 +271,20 @@ async def get_full_data(self, start: datetime, end: datetime) -> FullData: sport_detail_logs.append(await self.get_steps(d)) return FullData(self.address, heart_rates=heart_rate_logs, sport_details=sport_detail_logs) + + async def get_firehose(self): + try: + await self.send_packet(firehose.START_FIREHOSE_PACKET) + tries = 0 + while tries < 20: + tries += 1 + try: + data = await asyncio.wait_for( + self.queues[firehose.CMD_FIREHOSE].get(), + timeout=2, + ) + logger.error(data) + except TimeoutError: + pass + finally: + await self.send_packet(firehose.STOP_FIREHOSE_PACKET) diff --git a/colmi_r02_client/firehose.py b/colmi_r02_client/firehose.py new file mode 100644 index 0000000..c2baf2e --- /dev/null +++ b/colmi_r02_client/firehose.py @@ -0,0 +1,72 @@ +from enum import IntEnum +from dataclasses import dataclass +import logging + +from colmi_r02_client.packet import make_packet + +logger = logging.getLogger(__name__) + +CMD_FIREHOSE = 161 # 0xa1 + +START_FIREHOSE_PACKET = make_packet(CMD_FIREHOSE, bytearray([4])) +STOP_FIREHOSE_PACKET = make_packet(CMD_FIREHOSE, bytearray([2])) + + +class Kind(IntEnum): + SPO2 = 1 + PPG = 2 + ACCELEROMETER = 3 + + +@dataclass +class SpO2: + current: int + max: int + min: int + diff: int + + +@dataclass +class PPG: + current: int + max: int + min: int + diff: int + + +@dataclass +class Accelerometer: + """I think this is the x, y, z axis""" + + x: int + y: int + z: int + + +def parse_firehose(packet: bytearray) -> SpO2 | PPG | Accelerometer | None: + r""" + bytearray(b'\xa1\x03\x1d\t\x07\x08\xfa\x00\x00\x00\x00\x00\x00\x00\x00\xd3') + """ + kind = packet[1] + if kind == Kind.SPO2: + return SpO2( + current=(packet[2] << 8) | packet[3], + max=packet[5], + min=packet[7], + diff=packet[9], + ) + elif kind == Kind.PPG: + return PPG( + current=(packet[2] << 8) | packet[3], + max=(packet[4] << 8) | packet[5], + min=(packet[6] << 8) | packet[7], + diff=(packet[8] << 8) | packet[9], + ) + elif kind == Kind.ACCELEROMETER: + x = ((packet[6] << 4) | (packet[7] & 0xF)) - (1 << 11) if packet[6] & 0x8 else ((packet[6] << 4) | (packet[7] & 0xF)) + y = ((packet[3] << 4) | (packet[3] & 0xF)) - (1 << 11) if packet[2] & 0x8 else ((packet[2] << 4) | (packet[3] & 0xF)) + z = ((packet[4] << 4) | (packet[5] & 0xF)) - (1 << 11) if packet[4] & 0x8 else ((packet[4] << 4) | (packet[5] & 0xF)) + return Accelerometer(x=x, y=y, z=z) + else: + logging.error(f"Unexpected kind of firehose packet {packet}") + return None