Add Berger LFP BLE command notes and client

This commit is contained in:
2026-07-27 01:20:56 +02:00
commit b390792e34
10 changed files with 861 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+70
View File
@@ -0,0 +1,70 @@
import unittest
from berger_gui import DeviceState, berger_match_reason, decode_response, read_frame, split_frames, write_frame
class BergerParserTest(unittest.TestCase):
def test_read_frame_checksum(self):
self.assertEqual(read_frame(0x03).hex().upper(), "DDA50300FFFD77")
self.assertEqual(read_frame(0x04).hex().upper(), "DDA50400FFFC77")
def test_write_switch_checksum(self):
self.assertEqual(write_frame(0xE1, b"\x00\x00").hex().upper(), "DD5AE1020000FF1D77")
def test_decode_base_data(self):
payload = bytearray(27 + 4)
payload[0:2] = (1328).to_bytes(2, "big")
payload[2:4] = (65536 - 752).to_bytes(2, "big")
payload[4:6] = (10750).to_bytes(2, "big")
payload[6:8] = (16000).to_bytes(2, "big")
payload[8:10] = (3).to_bytes(2, "big")
payload[10:12] = ((25 << 9) | (7 << 5) | 27).to_bytes(2, "big")
payload[16:18] = (0x0010).to_bytes(2, "big")
payload[18] = 21
payload[19] = 67
payload[20] = 3
payload[22] = 2
payload[23:25] = (2981).to_bytes(2, "big")
payload[25:27] = (2991).to_bytes(2, "big")
body = bytes([0xDD, 0x03, 0x00, len(payload), *payload])
total = sum(body[1:]) & 0xFFFF
check = ((total ^ 0xFFFF) + 1) & 0xFFFF
frame = body + check.to_bytes(2, "big") + b"\x77"
buffer = bytearray(b"\x00" + frame + frame)
frames = split_frames(buffer)
self.assertEqual(len(frames), 2)
state = DeviceState()
decode_response(frames[0], state)
self.assertAlmostEqual(state.telemetry.voltage, 13.28)
self.assertAlmostEqual(state.telemetry.current, -7.52)
self.assertEqual(state.telemetry.soc, 67)
self.assertAlmostEqual(state.telemetry.remaining_capacity, 107.5)
self.assertAlmostEqual(state.telemetry.nominal_capacity, 160.0)
self.assertEqual(state.telemetry.cycle_count, 3)
self.assertEqual(state.telemetry.production_date, "2025-7-27")
self.assertTrue(state.telemetry.charge_mos)
self.assertTrue(state.telemetry.discharge_mos)
self.assertTrue(state.telemetry.protection)
self.assertEqual(state.telemetry.temperatures, [25.0, 26.0])
self.assertEqual(state.telemetry.time_label, "Time till empty")
def test_decode_cell_voltages(self):
payload = b"".join(v.to_bytes(2, "big") for v in [3306, 3312, 3308, 3311])
body = bytes([0xDD, 0x04, 0x00, len(payload), *payload])
total = sum(body[1:]) & 0xFFFF
check = ((total ^ 0xFFFF) + 1) & 0xFFFF
frame = body + check.to_bytes(2, "big") + b"\x77"
state = DeviceState()
decode_response(frame, state)
self.assertEqual(state.telemetry.cells, [3.306, 3.312, 3.308, 3.311])
def test_scan_filter(self):
self.assertEqual(berger_match_reason(None, ["0000ff00-0000-1000-8000-00805f9b34fb"]), "service:ff00")
self.assertEqual(berger_match_reason("JBD-SP04S", []), "name")
self.assertIsNone(berger_match_reason("Keyboard", []))
if __name__ == "__main__":
unittest.main()