So I sent the "fetch today's step" command this morning after letting the ring sit overnight. I got the "empty" response back (second byte is 0xff), which makes sense. After taking a few steps I got a different response back ```python bytearray(b'C\xf0\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x005') bytearray(b'C#\x08\x08\x18\x00\x01Q\x00\x15\x00\x0b\x00\x00\x00\x00') ``` So possibly there are more packets as the day wears on? Or the other packets are other types of data (like SPO2 and Heart Rate) This also indicates the smart ring has a clock of some kind so it can tell when there's a new day. Something to look into, I think I did see a "set time" command that I ignored. But how to interpret the 2 packets? ```java public static boolean parserAndDispatchReqData(byte[] bArr) { ICommandResponse iCommandResponse; int i = bArr[0] & (~Constants.FLAG_MASK_ERROR); byte b = bArr[0]; int i2 = Constants.FLAG_MASK_ERROR; LocalWriteRequest localWriteRequest = BleOperateManager.getInstance().getLocalWriteRequestConcurrentHashMap().get(Integer.valueOf(i)); if (localWriteRequest != null && (iCommandResponse = localWriteRequest.getiOpResponse()) != null) { BaseRspCmd baseRspCmd = tempRspDataSparseArray.get(i); if (baseRspCmd == null) { try { baseRspCmd = BeanFactory.createBean(i, localWriteRequest.getType()); } catch (Exception e) { e.printStackTrace(); BleOperateManager.getInstance().getLocalWriteRequestConcurrentHashMap().clear(); } } if (baseRspCmd != null) { if (baseRspCmd.acceptData(Arrays.copyOfRange(bArr, 1, bArr.length - 1))) { tempRspDataSparseArray.put(i, baseRspCmd); return true; } iCommandResponse.onDataResponse(baseRspCmd); tempRspDataSparseArray.delete(i); return true; } } return false; } ``` Ah, now I see that if `acceptData` returns `true` then we don't dispatch the response yet, we just store the `baseRspCmd` (which is shared for each packet type) and then call `acceptData` with the new payloads until it returns `false` That means I need to make my parsing stateful. I started with writing some code using `asyncio.Event` for a single simple request / response ```python async def get_battery(client: BleakClient, rx_char): data_received = asyncio.Event() data = None def handle_rx(_char, incoming_data): nonlocal data data = incoming_data data_received.set() await client.start_notify(UART_TX_CHAR_UUID, handle_rx) await client.write_gatt_char(rx_char, BATTERY_PACKET, response=False) await data_received.wait() await client.stop_notify(UART_TX_CHAR_UUID) return { "batteryLevel": data[1], "charging": bool(data[2]), } ``` This works, but it doesn't handle multi part packets (like the sport details seem to be) and it doesn't handle unsolicited packets, which I've seen. Also, fun fact, I learned a bit about how python does scoping and capture. Note the `nonlocal data` line. This is because you can't assign variables inside a closure like this because it wouldn't let you shadow the variable I guess? But it does capture the reference to `event` which confused me. One suggestion I saw was to have `data = [None]` and then you can do `data[0] = incoming_data` But I think I'm going to rewrite this so that each packet type has it's own queue. I ended up writing a parsing function for each command we expect a response from, then making a queue for each like ```python queues = { cmd: asyncio.Queue() for cmd in COMMAND_HANDLERS.keys() } def handle_rx(_: BleakGATTCharacteristic, packet: bytearray): packet_type = packet[0] assert packet_type < 127, f"Packet has error bit set {packet}" if packet_type in COMMAND_HANDLERS: queues[packet_type].put_nowait(COMMAND_HANDLERS[packet_type](packet)) else: print("Did not expect this packet") ``` That means I can write a nice function like ```python async def get_battery(client, rx_char, queues: dict[int, asyncio.Queue]): await send_packet(client, rx_char, BATTERY_PACKET) return await queues[CMD_BATTERY].get() ``` (note this will cause problems if we got an unsolicited packet, I guess I could empty the queue first?) But it's much better for things like the get heart rate command where I want to know if we got data yet ```python async def get_heart_rate(client, rx_char, queues: dict[int, asyncio.Queue]) -> None: await send_packet(client, rx_char, START_HEART_RATE_PACKET) print("wrote HR reading packet, waiting...") valid_hr = [] tries = 0 while len(valid_hr) < 6 and tries < 20: try: data = await asyncio.wait_for(queues[CMD_START_HEART_RATE].get(), 2) if data["error_code"] == 1: print("No heart rate detected, probably not on") break if data["value"] != 0: valid_hr.append(data["value"]) except TimeoutError: print(".") tries += 1 await client.write_gatt_char(rx_char, CONTINUE_HEART_RATE_PACKET, response=False) await client.write_gatt_char(rx_char, STOP_HEART_RATE_PACKET, response=False) print(valid_hr) ``` Not sure how the multi part packet will work yet, but my brain is fried for the night