- 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>
81 lines
2.7 KiB
Python
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()
|