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
59 lines
2.2 KiB
Python
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")
|