colmi_r02_client.steps

  1from dataclasses import dataclass
  2
  3from colmi_r02_client.packet import make_packet
  4
  5CMD_GET_STEP_SOMEDAY = 67  # 0x43
  6
  7
  8def read_steps_packet(day_offset: int = 0) -> bytearray:
  9    """
 10    Read the steps for a given day offset from "today" relative to the ring's internal clock.
 11
 12    There's also 4 more bytes I don't fully understand but seem constant
 13    - 0x0f # constant
 14    - 0x00 # idk
 15    - 0x5f # less than 95 and greater than byte
 16    - 0x01 # constant
 17    """
 18    sub_data = bytearray(b"\x00\x0f\x00\x5f\x01")
 19    sub_data[0] = day_offset
 20
 21    return make_packet(CMD_GET_STEP_SOMEDAY, sub_data)
 22
 23
 24@dataclass
 25class SportDetail:
 26    year: int
 27    month: int
 28    day: int
 29    time_index: int
 30    """I'm not sure about this one yet"""
 31    calories: int
 32    steps: int
 33    distance: int
 34    """Distance in meters"""
 35
 36
 37class NoData:
 38    """Returned when there's no heart rate data"""
 39
 40
 41class SportDetailParser:
 42    r"""
 43    Parse SportDetailPacket, of which there will be several
 44
 45    example data:
 46    bytearray(b'C\xf0\x05\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009')
 47    bytearray(b'C#\x08\x13\x10\x00\x05\xc8\x000\x00\x1b\x00\x00\x00\xa9')
 48    bytearray(b'C#\x08\x13\x14\x01\x05\xb6\x18\xaa\x04i\x03\x00\x00\x83')
 49    bytearray(b'C#\x08\x13\x18\x02\x058\x04\xe1\x00\x95\x00\x00\x00R')
 50    bytearray(b'C#\x08\x13\x1c\x03\x05\x05\x02l\x00H\x00\x00\x00`')
 51    bytearray(b'C#\x08\x13L\x04\x05\xef\x01c\x00D\x00\x00\x00m')
 52    """
 53
 54    def __init__(self):
 55        self.reset()
 56
 57    def reset(self) -> None:
 58        self.new_calorie_protocol = False
 59        self.index = 0
 60        self.details: list[SportDetail] = []
 61
 62    def parse(self, packet: bytearray) -> list[SportDetail] | None | NoData:
 63        assert len(packet) == 16
 64        assert packet[0] == CMD_GET_STEP_SOMEDAY
 65
 66        if self.index == 0 and packet[1] == 255:
 67            self.reset()
 68            return NoData()
 69
 70        if self.index == 0 and packet[1] == 240:
 71            if packet[3] == 1:
 72                self.new_calorie_protocol = True
 73            self.index += 1
 74            return None
 75
 76        year = bcd_to_decimal(packet[1]) + 2000
 77        month = bcd_to_decimal(packet[2])
 78        day = bcd_to_decimal(packet[3])
 79        time_index = packet[4]
 80        calories = packet[7] | (packet[8] << 8)
 81        if self.new_calorie_protocol:
 82            calories *= 10
 83        steps = packet[9] | (packet[10] << 8)
 84        distance = packet[11] | (packet[12] << 8)
 85
 86        details = SportDetail(
 87            year=year,
 88            month=month,
 89            day=day,
 90            time_index=time_index,
 91            calories=calories,
 92            steps=steps,
 93            distance=distance,
 94        )
 95        self.details.append(details)
 96
 97        if packet[5] == packet[6] - 1:
 98            x = self.details
 99            self.reset()
100            return x
101        else:
102            self.index += 1
103            return None
104
105
106def bcd_to_decimal(b: int) -> int:
107    return (((b >> 4) & 15) * 10) + (b & 15)
CMD_GET_STEP_SOMEDAY = 67
def read_steps_packet(day_offset: int = 0) -> bytearray:
 9def read_steps_packet(day_offset: int = 0) -> bytearray:
10    """
11    Read the steps for a given day offset from "today" relative to the ring's internal clock.
12
13    There's also 4 more bytes I don't fully understand but seem constant
14    - 0x0f # constant
15    - 0x00 # idk
16    - 0x5f # less than 95 and greater than byte
17    - 0x01 # constant
18    """
19    sub_data = bytearray(b"\x00\x0f\x00\x5f\x01")
20    sub_data[0] = day_offset
21
22    return make_packet(CMD_GET_STEP_SOMEDAY, sub_data)

Read the steps for a given day offset from "today" relative to the ring's internal clock.

There's also 4 more bytes I don't fully understand but seem constant

  • 0x0f # constant
  • 0x00 # idk
  • 0x5f # less than 95 and greater than byte
  • 0x01 # constant
@dataclass
class SportDetail:
25@dataclass
26class SportDetail:
27    year: int
28    month: int
29    day: int
30    time_index: int
31    """I'm not sure about this one yet"""
32    calories: int
33    steps: int
34    distance: int
35    """Distance in meters"""
SportDetail( year: int, month: int, day: int, time_index: int, calories: int, steps: int, distance: int)
year: int
month: int
day: int
time_index: int

I'm not sure about this one yet

calories: int
steps: int
distance: int

Distance in meters

class NoData:
38class NoData:
39    """Returned when there's no heart rate data"""

Returned when there's no heart rate data

class SportDetailParser:
 42class SportDetailParser:
 43    r"""
 44    Parse SportDetailPacket, of which there will be several
 45
 46    example data:
 47    bytearray(b'C\xf0\x05\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009')
 48    bytearray(b'C#\x08\x13\x10\x00\x05\xc8\x000\x00\x1b\x00\x00\x00\xa9')
 49    bytearray(b'C#\x08\x13\x14\x01\x05\xb6\x18\xaa\x04i\x03\x00\x00\x83')
 50    bytearray(b'C#\x08\x13\x18\x02\x058\x04\xe1\x00\x95\x00\x00\x00R')
 51    bytearray(b'C#\x08\x13\x1c\x03\x05\x05\x02l\x00H\x00\x00\x00`')
 52    bytearray(b'C#\x08\x13L\x04\x05\xef\x01c\x00D\x00\x00\x00m')
 53    """
 54
 55    def __init__(self):
 56        self.reset()
 57
 58    def reset(self) -> None:
 59        self.new_calorie_protocol = False
 60        self.index = 0
 61        self.details: list[SportDetail] = []
 62
 63    def parse(self, packet: bytearray) -> list[SportDetail] | None | NoData:
 64        assert len(packet) == 16
 65        assert packet[0] == CMD_GET_STEP_SOMEDAY
 66
 67        if self.index == 0 and packet[1] == 255:
 68            self.reset()
 69            return NoData()
 70
 71        if self.index == 0 and packet[1] == 240:
 72            if packet[3] == 1:
 73                self.new_calorie_protocol = True
 74            self.index += 1
 75            return None
 76
 77        year = bcd_to_decimal(packet[1]) + 2000
 78        month = bcd_to_decimal(packet[2])
 79        day = bcd_to_decimal(packet[3])
 80        time_index = packet[4]
 81        calories = packet[7] | (packet[8] << 8)
 82        if self.new_calorie_protocol:
 83            calories *= 10
 84        steps = packet[9] | (packet[10] << 8)
 85        distance = packet[11] | (packet[12] << 8)
 86
 87        details = SportDetail(
 88            year=year,
 89            month=month,
 90            day=day,
 91            time_index=time_index,
 92            calories=calories,
 93            steps=steps,
 94            distance=distance,
 95        )
 96        self.details.append(details)
 97
 98        if packet[5] == packet[6] - 1:
 99            x = self.details
100            self.reset()
101            return x
102        else:
103            self.index += 1
104            return None

Parse SportDetailPacket, of which there will be several

example data: bytearray(b'C\xf0\x05\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009') bytearray(b'C#\x08\x13\x10\x00\x05\xc8\x000\x00\x1b\x00\x00\x00\xa9') bytearray(b'C#\x08\x13\x14\x01\x05\xb6\x18\xaa\x04i\x03\x00\x00\x83') bytearray(b'C#\x08\x13\x18\x02\x058\x04\xe1\x00\x95\x00\x00\x00R') bytearray(b'C#\x08\x13\x1c\x03\x05\x05\x02l\x00H\x00\x00\x00`') bytearray(b'C#\x08\x13L\x04\x05\xef\x01c\x00D\x00\x00\x00m')

def reset(self) -> None:
58    def reset(self) -> None:
59        self.new_calorie_protocol = False
60        self.index = 0
61        self.details: list[SportDetail] = []
def parse( self, packet: bytearray) -> list[SportDetail] | None | NoData:
 63    def parse(self, packet: bytearray) -> list[SportDetail] | None | NoData:
 64        assert len(packet) == 16
 65        assert packet[0] == CMD_GET_STEP_SOMEDAY
 66
 67        if self.index == 0 and packet[1] == 255:
 68            self.reset()
 69            return NoData()
 70
 71        if self.index == 0 and packet[1] == 240:
 72            if packet[3] == 1:
 73                self.new_calorie_protocol = True
 74            self.index += 1
 75            return None
 76
 77        year = bcd_to_decimal(packet[1]) + 2000
 78        month = bcd_to_decimal(packet[2])
 79        day = bcd_to_decimal(packet[3])
 80        time_index = packet[4]
 81        calories = packet[7] | (packet[8] << 8)
 82        if self.new_calorie_protocol:
 83            calories *= 10
 84        steps = packet[9] | (packet[10] << 8)
 85        distance = packet[11] | (packet[12] << 8)
 86
 87        details = SportDetail(
 88            year=year,
 89            month=month,
 90            day=day,
 91            time_index=time_index,
 92            calories=calories,
 93            steps=steps,
 94            distance=distance,
 95        )
 96        self.details.append(details)
 97
 98        if packet[5] == packet[6] - 1:
 99            x = self.details
100            self.reset()
101            return x
102        else:
103            self.index += 1
104            return None
def bcd_to_decimal(b: int) -> int:
107def bcd_to_decimal(b: int) -> int:
108    return (((b >> 4) & 15) * 10) + (b & 15)