Files
bm200/findings/reference-python/test_bm200.py
Vibe Myass 13b95345c7 BM200 protocol findings and test bench
Spec with pseudocode and test vectors, APK disassembly, analysis tools,
shelved reference Python and the macOS BLE probe. Findings are not yet
validated against a real tag. The vendor XAPK and nested lib clone are
excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EinWd3UT2Xw7UakFtTp6hn
2026-11-01 02:58:16 +00:00

59 lines
2.2 KiB
Python

"""Run: PYTHONPATH=src python test_bm200.py
Frames here are built from the APK's parser, not captured from a tag.
"""
from datetime import datetime, timezone
from bm2.bm200 import BM200_KEY, BM200Decoder, HistoryBlock, LiveReading, Version, _history_base, history_command
from bm2.encryption import encrypt
def enc(hex_str: str) -> bytes:
return encrypt(bytes.fromhex(hex_str), BM200_KEY)
def test() -> None:
d = BM200Decoder()
now = 1_700_000_000.0
assert d.feed(enc("d15501010700"))[0] == Version(1.7)
assert history_command(0xCA80).hex() == "d1550500000000ca80"
# temp sign=1 temp=5 status=2 soc=0x55 volt=0x0508 acc=3 dec=4
live = d.feed(enc("d155070105025505080003000400"), now)[0]
assert isinstance(live, LiveReading)
assert (live.temperature, live.status, live.soc, live.voltage) == (-5, 2, 0x55, 12.88)
assert (live.rapid_acceleration, live.rapid_deceleration) == (3, 4)
assert d.feed(enc("d15507ff")) == []
assert d.feed(enc("d155050003"), now) == []
# newest first: leading zeros, stop record + 4 trailer bytes, plain record
assert d.feed(enc("00000000" + "50055194" + "00030004" + "4f050140"), now) == []
# a live frame in the middle of a transfer stays out of the history
assert isinstance(d.feed(enc("d155070005005505080000000000"), now)[0], LiveReading)
# negative-temperature start record, then the end marker
block = d.feed(enc("4e0000aa" + "fffefe00"), now)[0]
assert isinstance(block, HistoryBlock)
r = block.records
assert [x.voltage for x in r] == [12.48, 12.64, 12.80] # oldest first
assert [x.mark for x in r] == ["start", None, "stop"]
assert (r[2].soc, r[2].temperature) == (0x55, 0x19)
assert (r[0].soc, r[0].temperature) == (0, -0x0a)
base = _history_base(now)
assert datetime.fromtimestamp(base).minute % 2 == 1
assert r[2].time == datetime.fromtimestamp(base, timezone.utc)
assert (r[2].time - r[0].time).total_seconds() == 240
# back to normal once the block ended
assert d.feed(enc("d155050000"), now)[0] == HistoryBlock([])
try:
d.feed(b"\x00" * 15)
raise AssertionError("short frame accepted")
except ValueError:
pass
test()
print("ok")