Fix BullTron MOS refresh ordering
This commit is contained in:
+32
-8
@@ -300,7 +300,7 @@ def decode_alarm_words(words: list[int]) -> list[str]:
|
|||||||
return active
|
return active
|
||||||
|
|
||||||
|
|
||||||
def decode_response(frame: bytes, state: DeviceState) -> None:
|
def decode_response(frame: bytes, state: DeviceState, read_start: int | None = None) -> None:
|
||||||
if frame[1] != 0x03:
|
if frame[1] != 0x03:
|
||||||
state.raw_blocks["Last ACK"] = frame.hex(" ").upper()
|
state.raw_blocks["Last ACK"] = frame.hex(" ").upper()
|
||||||
return
|
return
|
||||||
@@ -318,21 +318,30 @@ def decode_response(frame: bytes, state: DeviceState) -> None:
|
|||||||
parse_version_block(payload, state)
|
parse_version_block(payload, state)
|
||||||
elif byte_count == 0x18:
|
elif byte_count == 0x18:
|
||||||
state.battery_code = clean_ascii(payload) or payload.hex().upper()
|
state.battery_code = clean_ascii(payload) or payload.hex().upper()
|
||||||
|
elif byte_count == 0x04 and read_start == 0x00A5:
|
||||||
|
words = words_from_payload(payload)
|
||||||
|
if len(words) >= 2:
|
||||||
|
state.control_charge_mos = words[0] != 0
|
||||||
|
state.control_discharge_mos = words[1] != 0
|
||||||
elif byte_count == 0x04:
|
elif byte_count == 0x04:
|
||||||
state.production_date = parse_production_date(payload)
|
state.production_date = parse_production_date(payload)
|
||||||
elif byte_count == 0x4A:
|
elif byte_count == 0x4A:
|
||||||
parse_device_parameter_block(payload, state)
|
parse_device_parameter_block(payload, state)
|
||||||
elif byte_count == 0x52:
|
elif byte_count == 0x52:
|
||||||
parse_settings(payload, state)
|
parse_settings(payload, state, read_start)
|
||||||
|
|
||||||
|
|
||||||
def parse_settings(payload: bytes, state: DeviceState) -> None:
|
def parse_settings(payload: bytes, state: DeviceState, read_start: int | None = 0x0080) -> None:
|
||||||
words = words_from_payload(payload)
|
words = words_from_payload(payload)
|
||||||
if words:
|
if words:
|
||||||
state.rated_capacity_ah = words[0] * 0.1
|
state.rated_capacity_ah = words[0] * 0.1
|
||||||
if len(words) > 38:
|
start = 0x0080 if read_start is None else read_start
|
||||||
state.control_charge_mos = words[0x00A5 - 0x0080] != 0
|
charge_index = 0x00A5 - start
|
||||||
state.control_discharge_mos = words[0x00A6 - 0x0080] != 0
|
discharge_index = 0x00A6 - start
|
||||||
|
if 0 <= charge_index < len(words):
|
||||||
|
state.control_charge_mos = words[charge_index] != 0
|
||||||
|
if 0 <= discharge_index < len(words):
|
||||||
|
state.control_discharge_mos = words[discharge_index] != 0
|
||||||
|
|
||||||
|
|
||||||
def parse_version_block(payload: bytes, state: DeviceState) -> None:
|
def parse_version_block(payload: bytes, state: DeviceState) -> None:
|
||||||
@@ -371,6 +380,8 @@ class BleWorker:
|
|||||||
self.thread.start()
|
self.thread.start()
|
||||||
self.client: Any = None
|
self.client: Any = None
|
||||||
self.buffer = bytearray()
|
self.buffer = bytearray()
|
||||||
|
self.pending_reads: list[tuple[int, int]] = []
|
||||||
|
self.command_lock = asyncio.Lock()
|
||||||
self.state = DeviceState()
|
self.state = DeviceState()
|
||||||
self.polling = False
|
self.polling = False
|
||||||
|
|
||||||
@@ -441,6 +452,8 @@ class BleWorker:
|
|||||||
|
|
||||||
async def _write_command(self, data: bytes, response: bool = False) -> None:
|
async def _write_command(self, data: bytes, response: bool = False) -> None:
|
||||||
if self.client and self.client.is_connected:
|
if self.client and self.client.is_connected:
|
||||||
|
if len(data) >= 6 and data[0] == 0xD2 and data[1] == 0x03:
|
||||||
|
self.pending_reads.append((int.from_bytes(data[2:4], "big"), int.from_bytes(data[4:6], "big") * 2))
|
||||||
await self.client.write_gatt_char(WRITE_CHAR, data, response=response)
|
await self.client.write_gatt_char(WRITE_CHAR, data, response=response)
|
||||||
self.ui_queue.put(("tx", data.hex(" ").upper()))
|
self.ui_queue.put(("tx", data.hex(" ").upper()))
|
||||||
|
|
||||||
@@ -448,6 +461,7 @@ class BleWorker:
|
|||||||
slow_counter = 0
|
slow_counter = 0
|
||||||
while self.polling and self.client and self.client.is_connected:
|
while self.polling and self.client and self.client.is_connected:
|
||||||
try:
|
try:
|
||||||
|
async with self.command_lock:
|
||||||
await self._write_command(read_frame(0x0000, 0x003E))
|
await self._write_command(read_frame(0x0000, 0x003E))
|
||||||
slow_counter += 1
|
slow_counter += 1
|
||||||
if slow_counter % 5 == 1:
|
if slow_counter % 5 == 1:
|
||||||
@@ -463,7 +477,14 @@ class BleWorker:
|
|||||||
def _notification(self, _sender: Any, data: bytearray) -> None:
|
def _notification(self, _sender: Any, data: bytearray) -> None:
|
||||||
self.buffer.extend(bytes(data))
|
self.buffer.extend(bytes(data))
|
||||||
for frame in split_frames(self.buffer):
|
for frame in split_frames(self.buffer):
|
||||||
decode_response(frame, self.state)
|
read_start = None
|
||||||
|
if frame[1] == 0x03 and self.pending_reads:
|
||||||
|
byte_count = frame[2]
|
||||||
|
match_index = next((i for i, (_, expected_bytes) in enumerate(self.pending_reads) if expected_bytes == byte_count), 0)
|
||||||
|
read_start, _ = self.pending_reads.pop(match_index)
|
||||||
|
if match_index:
|
||||||
|
del self.pending_reads[:match_index]
|
||||||
|
decode_response(frame, self.state, read_start)
|
||||||
self.ui_queue.put(("rx", frame.hex(" ").upper()))
|
self.ui_queue.put(("rx", frame.hex(" ").upper()))
|
||||||
self.ui_queue.put(("state", self.state))
|
self.ui_queue.put(("state", self.state))
|
||||||
|
|
||||||
@@ -484,12 +505,15 @@ class BleWorker:
|
|||||||
|
|
||||||
async def _write_mos(self, register: int, enabled: bool) -> None:
|
async def _write_mos(self, register: int, enabled: bool) -> None:
|
||||||
self.ui_queue.put(("status", f"Writing MOS register 0x{register:04X}..."))
|
self.ui_queue.put(("status", f"Writing MOS register 0x{register:04X}..."))
|
||||||
|
async with self.command_lock:
|
||||||
await self._write_command(write_single_frame(register, 1 if enabled else 0), response=True)
|
await self._write_command(write_single_frame(register, 1 if enabled else 0), response=True)
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
await self._write_command(read_frame(0x00A5, 0x0002))
|
||||||
|
await asyncio.sleep(0.12)
|
||||||
await self._write_command(read_frame(0x0000, 0x003E))
|
await self._write_command(read_frame(0x0000, 0x003E))
|
||||||
await asyncio.sleep(0.12)
|
await asyncio.sleep(0.12)
|
||||||
await self._write_command(read_frame(0x0080, 0x0029))
|
await self._write_command(read_frame(0x0080, 0x0029))
|
||||||
self.ui_queue.put(("status", f"MOS write sent; refreshed 0x0035/0x0036 and 0x00A5/0x00A6"))
|
self.ui_queue.put(("status", f"MOS write acknowledged; refreshed 0x00A5/0x00A6 and live MOS status"))
|
||||||
|
|
||||||
async def disconnect(self) -> None:
|
async def disconnect(self) -> None:
|
||||||
self.polling = False
|
self.polling = False
|
||||||
|
|||||||
@@ -121,6 +121,29 @@ class BullTronParserTest(unittest.TestCase):
|
|||||||
self.assertTrue(state.control_charge_mos)
|
self.assertTrue(state.control_charge_mos)
|
||||||
self.assertFalse(state.control_discharge_mos)
|
self.assertFalse(state.control_discharge_mos)
|
||||||
|
|
||||||
|
def test_direct_mos_control_read_uses_register_context(self):
|
||||||
|
state = DeviceState()
|
||||||
|
payload = b"\x00\x00\x00\x01"
|
||||||
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
||||||
|
from bulltron_gui import crc16_modbus_swapped
|
||||||
|
|
||||||
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
||||||
|
decode_response(frame, state, 0x00A5)
|
||||||
|
|
||||||
|
self.assertFalse(state.control_charge_mos)
|
||||||
|
self.assertTrue(state.control_discharge_mos)
|
||||||
|
|
||||||
|
def test_four_byte_non_mos_read_still_decodes_production_date(self):
|
||||||
|
state = DeviceState()
|
||||||
|
payload = b"\x19\x01\x01\x00"
|
||||||
|
body = bytes([0xD2, 0x03, len(payload)]) + payload
|
||||||
|
from bulltron_gui import crc16_modbus_swapped
|
||||||
|
|
||||||
|
frame = body + crc16_modbus_swapped(body).to_bytes(2, "big")
|
||||||
|
decode_response(frame, state, 0x00C9)
|
||||||
|
|
||||||
|
self.assertEqual(state.production_date, "2025-01-01")
|
||||||
|
|
||||||
def test_system_mos_write_registers(self):
|
def test_system_mos_write_registers(self):
|
||||||
self.assertEqual(write_single_frame(0x00A5, 1).hex().upper()[:8], "D20600A5")
|
self.assertEqual(write_single_frame(0x00A5, 1).hex().upper()[:8], "D20600A5")
|
||||||
self.assertEqual(write_single_frame(0x00A6, 0).hex().upper()[:8], "D20600A6")
|
self.assertEqual(write_single_frame(0x00A6, 0).hex().upper()[:8], "D20600A6")
|
||||||
|
|||||||
Reference in New Issue
Block a user