Files
dgm20-linux/tests/test_protocol.py
T
justinandClaude Opus 5.5 1fcf73ed9d Drop the brightness control, survive and explain a frozen mic
- Remove RGB brightness: firmware 2.1.2 stores the value but the LEDs
  never change, so the control did nothing. PROTOCOL.md notes why.
- Lock the device per transaction (flock) so the CLI and the web
  service can't interleave and steal each other's replies.
- Detect a mic that is plugged in but no longer answering and tell the
  user to replug it (a USB reset is not enough; settings survive).
  Seen when hardware buttons are held while settings are being written.
- Web page keeps the last known value when a poll comes back partial,
  instead of drawing unanswered settings as off.
- New screenshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:45:18 -05:00

81 lines
2.7 KiB
Python

"""Framing tests against packets captured from a real DGM20 (firmware 2.1.2)."""
import unittest
from dgm20.device import decode, encode, frame, parse, words_to_ascii
def h(s):
return bytes.fromhex(s) + bytes(64 - len(bytes.fromhex(s)))
class Framing(unittest.TestCase):
def test_read_request_matches_app(self):
# checksum = -(0xC4+0x09+0x04+0x28+0x20) = -0x119 = 0xFEE7
p = frame(4, [0x2028])
self.assertEqual(p[:10].hex(" "), "51 c4 09 00 00 04 28 20 e7 fe")
self.assertEqual(len(p), 65)
def test_write_request_checksum(self):
p = frame(3, [0x2028, 64])
self.assertEqual(p[1:3], bytes([0xC4, 0x0B]))
self.assertEqual((sum(p[1:10]) + (p[10] | p[11] << 8)) & 0xFFFF, 0)
def test_parse_read_reply(self):
self.assertEqual(parse(h("11 c4 0b 00 00 04 28 20 40 00 a5 fe")), (4, [0x2028, 64]))
def test_parse_range_reply(self):
self.assertEqual(parse(h("11 c4 0f 00 00 06 27 20 02 00 01 00 40 00 9d fe")),
(6, [0x2027, 2, 1, 64]))
def test_parse_ack(self):
self.assertEqual(parse(h("11 c4 09 00 00 02 00 00 31 ff")), (2, [0]))
def test_parse_rejects_bad_checksum(self):
self.assertIsNone(parse(h("11 c4 0b 00 00 04 28 20 40 00 a5 ff")))
def test_model_name(self):
self.assertEqual(words_to_ascii([0x4744, 0x324D, 0x0030, 0]), "DGM20")
class Encoding(unittest.TestCase):
def test_bool(self):
self.assertEqual(encode("nc", "on"), 1)
self.assertEqual(encode("mute", False), 0)
self.assertIs(decode("nc", 1), True)
def test_monitor_uses_2000_3000(self):
self.assertEqual(encode("monitor", True), 2000)
self.assertEqual(encode("monitor", "off"), 3000)
self.assertEqual(decode("monitor", 2000), "on")
def test_range(self):
self.assertEqual(encode("nc-level", "0"), 0)
self.assertEqual(encode("nc-level", 100), 100)
for bad in (101, -1, True, "loud"):
with self.assertRaises(ValueError):
encode("nc-level", bad)
def test_unknown(self):
with self.assertRaises(ValueError):
encode("volume", 3)
class NotRespondingDetection(unittest.TestCase):
def test_all_unanswered_reads_raise(self):
from dgm20.device import Mic, NotResponding
mic = Mic.__new__(Mic) # no device needed: reads always time out
mic.read = lambda addr: None
with self.assertRaises(NotResponding):
mic.state()
def test_partial_answers_are_fine(self):
from dgm20.device import Mic
mic = Mic.__new__(Mic)
mic.read = lambda addr: 1 if addr == 0x2022 else None
self.assertTrue(mic.state()["mute"])
if __name__ == "__main__":
unittest.main()