import io import json import tempfile import unittest from pathlib import Path from h8536.protocol_trace import ( checksum_for, decode_trace, format_text_report, main, parse_byte_text, ) def frame(prefix: list[int]) -> list[int]: return prefix + [checksum_for(prefix)] class ProtocolTraceTest(unittest.TestCase): def test_decodes_six_byte_frame_fields_and_checksum(self): decoded = decode_trace(frame([0x08, 0x85, 0x34, 0x12, 0xAB]), direction="rx") only = decoded["frames"][0] self.assertTrue(only["checksum"]["valid"]) self.assertEqual(only["direction"], "rx") self.assertEqual(only["command"]["value"], 0) self.assertEqual(only["index"]["byte1_low3"], 5) self.assertEqual(only["index"]["combined"], 0x0534) self.assertEqual(only["payload_value"]["word_be"], 0x12AB) self.assertEqual(only["payload_value"]["word_le"], 0xAB12) def test_reports_bad_checksum_and_trailing_bytes(self): decoded = decode_trace([0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x99]) only = decoded["frames"][0] self.assertFalse(only["checksum"]["valid"]) self.assertEqual(only["checksum"]["expected"], checksum_for([1, 2, 3, 4, 5])) self.assertEqual(decoded["trailing_bytes"], ["0x99"]) def test_loads_semantic_command_names_from_decompiler_json(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "rom.json" path.write_text( json.dumps( { "serial_semantics": { "protocol_semantics": [ { "command_effects": [ { "command_value": 1, "name_candidate": "read_value", } ], "response_schema": [], } ] } } ), encoding="utf-8", ) decoded = decode_trace(frame([0x09, 0, 1, 2, 3]), semantics_path=path) self.assertTrue(decoded["semantics"]["loaded"]) self.assertEqual(decoded["frames"][0]["command"]["name_candidate"], "read_value") def test_marks_rx_cmd_7_as_retransmit_or_error_candidate_with_previous_frame(self): prior = frame([0x01, 0, 0, 0, 0]) retry = frame([0x07, 0, 0, 0, 0]) decoded = decode_trace(prior + retry, direction="rx") annotation = decoded["frames"][1]["stateful_annotations"][0] self.assertEqual(annotation["kind"], "retransmit_or_error_candidate") self.assertEqual(annotation["previous_valid_same_direction"]["frame_index"], 0) def test_decodes_observed_tx_frames_as_reports(self): samples = [ ([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA], 0x0000, 0x0080, "heartbeat_alive_candidate", None), ([0x00, 0x00, 0x15, 0x80, 0x00, 0xCF], 0x0015, 0x8000, "call_button_candidate", "active"), ([0x00, 0x00, 0x15, 0x00, 0x00, 0x4F], 0x0015, 0x0000, "call_button_candidate", "inactive"), ([0x00, 0x00, 0x07, 0x80, 0x00, 0xDD], 0x0007, 0x8000, "cam_power_button_candidate", "active"), ] decoded = decode_trace([byte for sample, *_ in samples for byte in sample], direction="tx") for actual, (_, index, value, name, state) in zip(decoded["frames"], samples): self.assertTrue(actual["checksum"]["valid"]) self.assertFalse(actual["command"]["applicable"]) self.assertIsNone(actual["command"]["name_candidate"]) self.assertEqual(actual["response_schema_candidates"], []) self.assertEqual(actual["stateful_annotations"], []) self.assertEqual(actual["report"]["index"], index) self.assertEqual(actual["report"]["value"], value) self.assertEqual(actual["report"]["observed_candidate"]["name_candidate"], name) self.assertEqual(actual["report"]["observed_candidate"].get("state_candidate"), state) def test_tx_text_report_does_not_render_cmd_or_set_value_acked(self): with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "rom.json" path.write_text( json.dumps( { "serial_semantics": { "command_effects": [ { "command_value": 0, "name_candidate": "set_value_acked", } ] } } ), encoding="utf-8", ) decoded = decode_trace( [0x00, 0x00, 0x15, 0x80, 0x00, 0xCF], direction="tx", semantics_path=path, ) text = format_text_report(decoded) self.assertIn("report_index=0x0015", text) self.assertIn("observed_candidate=call_button_candidate", text) self.assertNotIn("cmd=", text) self.assertNotIn("set_value_acked", text) def test_auto_direction_uses_rx_tx_prefixes(self): events = parse_byte_text( "rx: 01 00 00 00 00 5B\n" "tx: 04 00 00 00 00 5E\n" ) decoded = decode_trace(events, direction="auto") self.assertEqual(decoded["frames"][0]["direction"], "rx") self.assertEqual(decoded["frames"][1]["direction"], "tx") def test_cli_json_output(self): output = io.StringIO() rc = main(["--json", "01", "00", "00", "00", "00", "5B"], stdout=output) self.assertEqual(rc, 0) payload = json.loads(output.getvalue()) self.assertEqual(payload["frames"][0]["command"]["value"], 1) if __name__ == "__main__": unittest.main()