sok-ble 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sok_ble/sok_bluetooth_device.py +37 -12
- sok_ble/sok_parser.py +25 -33
- {sok_ble-0.1.0.dist-info → sok_ble-0.1.1.dist-info}/METADATA +1 -1
- sok_ble-0.1.1.dist-info/RECORD +9 -0
- sok_ble-0.1.0.dist-info/RECORD +0 -9
- {sok_ble-0.1.0.dist-info → sok_ble-0.1.1.dist-info}/WHEEL +0 -0
- {sok_ble-0.1.0.dist-info → sok_ble-0.1.1.dist-info}/top_level.txt +0 -0
sok_ble/sok_bluetooth_device.py
CHANGED
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from contextlib import asynccontextmanager
|
|
6
6
|
from typing import AsyncIterator, Optional
|
|
7
|
+
import asyncio
|
|
8
|
+
import struct
|
|
7
9
|
import logging
|
|
8
10
|
import statistics
|
|
9
11
|
|
|
@@ -65,32 +67,55 @@ class SokBluetoothDevice:
|
|
|
65
67
|
await client.disconnect()
|
|
66
68
|
logger.debug("Disconnected from %s", self._ble_device.address)
|
|
67
69
|
|
|
70
|
+
async def _send_command(
|
|
71
|
+
self, client: BleakClientWithServiceCache, cmd: int, expected: int
|
|
72
|
+
) -> bytes:
|
|
73
|
+
"""Send a command and return the response bytes with the given header."""
|
|
74
|
+
|
|
75
|
+
# If the client supports notifications (real BLE client), prefer that
|
|
76
|
+
start_notify = getattr(client, "start_notify", None)
|
|
77
|
+
if start_notify is None:
|
|
78
|
+
await client.write_gatt_char(UUID_TX, _sok_command(cmd))
|
|
79
|
+
data = bytes(await client.read_gatt_char(UUID_RX))
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
83
|
+
|
|
84
|
+
def handler(_: int, data: bytearray) -> None:
|
|
85
|
+
queue.put_nowait(bytes(data))
|
|
86
|
+
|
|
87
|
+
await client.start_notify(UUID_RX, handler)
|
|
88
|
+
try:
|
|
89
|
+
await client.write_gatt_char(UUID_TX, _sok_command(cmd))
|
|
90
|
+
while True:
|
|
91
|
+
data = await asyncio.wait_for(queue.get(), 5.0)
|
|
92
|
+
if struct.unpack_from(">H", data)[0] == expected:
|
|
93
|
+
return data
|
|
94
|
+
finally:
|
|
95
|
+
await client.stop_notify(UUID_RX)
|
|
96
|
+
|
|
68
97
|
async def async_update(self) -> None:
|
|
69
98
|
"""Poll the device for all telemetry and update attributes."""
|
|
70
99
|
responses: dict[int, bytes] = {}
|
|
71
100
|
async with self._connect() as client:
|
|
72
101
|
logger.debug("Send C1")
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
logger.debug("Recv 0xCCF0: %s", data.hex())
|
|
102
|
+
data = await self._send_command(client, 0xC1, 0xCCF0)
|
|
103
|
+
logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
|
|
76
104
|
responses[0xCCF0] = data
|
|
77
105
|
|
|
78
106
|
logger.debug("Send C1")
|
|
79
|
-
await
|
|
80
|
-
|
|
81
|
-
logger.debug("Recv 0xCCF2: %s", data.hex())
|
|
107
|
+
data = await self._send_command(client, 0xC1, 0xCCF2)
|
|
108
|
+
logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
|
|
82
109
|
responses[0xCCF2] = data
|
|
83
110
|
|
|
84
111
|
logger.debug("Send C2")
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
logger.debug("Recv 0xCCF3: %s", data.hex())
|
|
112
|
+
data = await self._send_command(client, 0xC2, 0xCCF3)
|
|
113
|
+
logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
|
|
88
114
|
responses[0xCCF3] = data
|
|
89
115
|
|
|
90
116
|
logger.debug("Send C2")
|
|
91
|
-
await
|
|
92
|
-
|
|
93
|
-
logger.debug("Recv 0xCCF4: %s", data.hex())
|
|
117
|
+
data = await self._send_command(client, 0xC2, 0xCCF4)
|
|
118
|
+
logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
|
|
94
119
|
responses[0xCCF4] = data
|
|
95
120
|
|
|
96
121
|
parsed = SokParser.parse_all(responses)
|
sok_ble/sok_parser.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import struct
|
|
7
|
+
import statistics
|
|
7
8
|
from typing import Dict, Sequence
|
|
8
9
|
|
|
9
10
|
from sok_ble.exceptions import InvalidResponseError
|
|
@@ -43,27 +44,19 @@ class SokParser:
|
|
|
43
44
|
|
|
44
45
|
@staticmethod
|
|
45
46
|
def parse_info(buf: bytes) -> Dict[str, float | int]:
|
|
46
|
-
"""Parse the information frame for
|
|
47
|
+
"""Parse the information frame for current, SOC and cycles."""
|
|
47
48
|
logger.debug("parse_info input: %s", buf.hex())
|
|
48
|
-
if len(buf) <
|
|
49
|
+
if len(buf) < 20:
|
|
49
50
|
raise InvalidResponseError("Info buffer too short")
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
get_le_ushort(buf, 4),
|
|
55
|
-
get_le_ushort(buf, 6),
|
|
56
|
-
]
|
|
57
|
-
voltage = (sum(cells) / len(cells) * 4) / 1000
|
|
58
|
-
|
|
59
|
-
current = get_le_int3(buf, 8) / 10
|
|
60
|
-
|
|
61
|
-
soc = struct.unpack_from('<H', buf, 16)[0]
|
|
52
|
+
current = get_le_int3(buf, 5) / 1000
|
|
53
|
+
num_cycles = get_le_ushort(buf, 14)
|
|
54
|
+
soc = get_le_ushort(buf, 16)
|
|
62
55
|
|
|
63
56
|
result = {
|
|
64
|
-
"voltage": voltage,
|
|
65
57
|
"current": current,
|
|
66
58
|
"soc": soc,
|
|
59
|
+
"num_cycles": num_cycles,
|
|
67
60
|
}
|
|
68
61
|
logger.debug("parse_info result: %s", result)
|
|
69
62
|
return result
|
|
@@ -72,27 +65,23 @@ class SokParser:
|
|
|
72
65
|
def parse_temps(buf: bytes) -> float:
|
|
73
66
|
"""Parse the temperature from the temperature frame."""
|
|
74
67
|
logger.debug("parse_temps input: %s", buf.hex())
|
|
75
|
-
if len(buf) <
|
|
68
|
+
if len(buf) < 20:
|
|
76
69
|
raise InvalidResponseError("Temp buffer too short")
|
|
77
70
|
|
|
78
|
-
temperature = get_le_short(buf, 5)
|
|
71
|
+
temperature = get_le_short(buf, 5)
|
|
79
72
|
logger.debug("parse_temps result: %s", temperature)
|
|
80
73
|
return temperature
|
|
81
74
|
|
|
82
75
|
@staticmethod
|
|
83
76
|
def parse_capacity_cycles(buf: bytes) -> Dict[str, float | int]:
|
|
84
|
-
"""Parse rated capacity
|
|
77
|
+
"""Parse rated capacity."""
|
|
85
78
|
logger.debug("parse_capacity_cycles input: %s", buf.hex())
|
|
86
|
-
if len(buf) <
|
|
79
|
+
if len(buf) < 20:
|
|
87
80
|
raise InvalidResponseError("Capacity buffer too short")
|
|
88
81
|
|
|
89
|
-
capacity =
|
|
90
|
-
num_cycles = get_le_ushort(buf, 4)
|
|
82
|
+
capacity = get_be_uint3(buf, 5) / 128
|
|
91
83
|
|
|
92
|
-
result = {
|
|
93
|
-
"capacity": capacity,
|
|
94
|
-
"num_cycles": num_cycles,
|
|
95
|
-
}
|
|
84
|
+
result = {"capacity": capacity}
|
|
96
85
|
logger.debug("parse_capacity_cycles result: %s", result)
|
|
97
86
|
return result
|
|
98
87
|
|
|
@@ -100,15 +89,13 @@ class SokParser:
|
|
|
100
89
|
def parse_cells(buf: bytes) -> list[float]:
|
|
101
90
|
"""Parse individual cell voltages."""
|
|
102
91
|
logger.debug("parse_cells input: %s", buf.hex())
|
|
103
|
-
if len(buf) <
|
|
92
|
+
if len(buf) < 20:
|
|
104
93
|
raise InvalidResponseError("Cells buffer too short")
|
|
105
94
|
|
|
106
|
-
cells = [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
get_le_ushort(buf, 4) / 1000
|
|
110
|
-
get_le_ushort(buf, 6) / 1000,
|
|
111
|
-
]
|
|
95
|
+
cells = [0.0, 0.0, 0.0, 0.0]
|
|
96
|
+
for x in range(4):
|
|
97
|
+
cell_idx = buf[2 + x * 4]
|
|
98
|
+
cells[cell_idx - 1] = get_le_ushort(buf, 3 + x * 4) / 1000
|
|
112
99
|
logger.debug("parse_cells result: %s", cells)
|
|
113
100
|
return cells
|
|
114
101
|
|
|
@@ -125,10 +112,15 @@ class SokParser:
|
|
|
125
112
|
capacity_info = cls.parse_capacity_cycles(responses[0xCCF3])
|
|
126
113
|
cells = cls.parse_cells(responses[0xCCF4])
|
|
127
114
|
|
|
115
|
+
voltage = statistics.mean(cells) * 4
|
|
116
|
+
|
|
128
117
|
result = {
|
|
129
|
-
|
|
118
|
+
"voltage": voltage,
|
|
119
|
+
"current": info["current"],
|
|
120
|
+
"soc": info["soc"],
|
|
130
121
|
"temperature": temperature,
|
|
131
|
-
|
|
122
|
+
"capacity": capacity_info["capacity"],
|
|
123
|
+
"num_cycles": info["num_cycles"],
|
|
132
124
|
"cell_voltages": cells,
|
|
133
125
|
}
|
|
134
126
|
logger.debug("parse_all result: %s", result)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
sok_ble/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sok_ble/const.py,sha256=mHtJTbWz_dG3v1lhZrLDzMf-QAG9v5QWlHU9ZKsyYdg,998
|
|
3
|
+
sok_ble/exceptions.py,sha256=7H1yUqqnrKyi3LwxRCMF7RkuGp_rgdy9j35nrMOgz44,247
|
|
4
|
+
sok_ble/sok_bluetooth_device.py,sha256=ig11wLwjZFC6HEFi7hzB7_Jy1T8syyNDOTYWUbkJR4Y,6353
|
|
5
|
+
sok_ble/sok_parser.py,sha256=KXYxsR4868vWFs2AAI7o7XfzjNZiDgfpsmz3dFgjLco,4323
|
|
6
|
+
sok_ble-0.1.1.dist-info/METADATA,sha256=Hub0gJn6lH2WJVXpSFaiYQ2vKv4nVdDvypZyNITBN2g,1233
|
|
7
|
+
sok_ble-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
sok_ble-0.1.1.dist-info/top_level.txt,sha256=WTVtlZ2sADYAxKEBkDJPUn4hFAH9NfIZ9Q2HP2RKpbw,8
|
|
9
|
+
sok_ble-0.1.1.dist-info/RECORD,,
|
sok_ble-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
sok_ble/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
sok_ble/const.py,sha256=mHtJTbWz_dG3v1lhZrLDzMf-QAG9v5QWlHU9ZKsyYdg,998
|
|
3
|
-
sok_ble/exceptions.py,sha256=7H1yUqqnrKyi3LwxRCMF7RkuGp_rgdy9j35nrMOgz44,247
|
|
4
|
-
sok_ble/sok_bluetooth_device.py,sha256=-dMuHzM0FtBh_fu2MURiriOVhOGaLDM2ZQIRk6zDC84,5384
|
|
5
|
-
sok_ble/sok_parser.py,sha256=zyrZwjOc4yeFfQWo_1B3VFxEGXLJBX7UQPL9e5ag1jQ,4441
|
|
6
|
-
sok_ble-0.1.0.dist-info/METADATA,sha256=u1fvysxad3Cg-N2ipnnuhB2V4N8iVQEzel4-INBRpnU,1233
|
|
7
|
-
sok_ble-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
sok_ble-0.1.0.dist-info/top_level.txt,sha256=WTVtlZ2sADYAxKEBkDJPUn4hFAH9NfIZ9Q2HP2RKpbw,8
|
|
9
|
-
sok_ble-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|