78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for the pure CI-V codec (civ.py). Stdlib only: `python3 -m unittest`."""
|
|
import unittest
|
|
|
|
import civ
|
|
|
|
|
|
class TestFreqCodec(unittest.TestCase):
|
|
# Real capture from the radio: VFO was 147.300000 MHz.
|
|
REAL_BCD = bytes.fromhex("0000304701")
|
|
REAL_HZ = 147_300_000
|
|
|
|
def test_bcd_to_freq_decodes_real_capture(self):
|
|
self.assertEqual(civ.bcd_to_freq(self.REAL_BCD), self.REAL_HZ)
|
|
|
|
def test_freq_to_bcd_matches_real_capture(self):
|
|
self.assertEqual(civ.freq_to_bcd(self.REAL_HZ), self.REAL_BCD)
|
|
|
|
def test_freq_roundtrip(self):
|
|
for hz in (146_520_000, 446_000_000, 147_300_000, 1_240_000_000):
|
|
self.assertEqual(civ.bcd_to_freq(civ.freq_to_bcd(hz)), hz)
|
|
|
|
|
|
class TestLevelCodec(unittest.TestCase):
|
|
# ICOM levels are 0..255 sent as 2-byte BCD "0000".."0255".
|
|
def test_level_to_bcd_known(self):
|
|
self.assertEqual(civ.level_to_bcd(0), b"\x00\x00")
|
|
self.assertEqual(civ.level_to_bcd(128), b"\x01\x28")
|
|
self.assertEqual(civ.level_to_bcd(255), b"\x02\x55")
|
|
|
|
def test_bcd_to_level_known(self):
|
|
self.assertEqual(civ.bcd_to_level(b"\x00\x00"), 0)
|
|
self.assertEqual(civ.bcd_to_level(b"\x01\x28"), 128)
|
|
self.assertEqual(civ.bcd_to_level(b"\x02\x55"), 255)
|
|
|
|
def test_level_rejects_out_of_range(self):
|
|
with self.assertRaises(ValueError):
|
|
civ.level_to_bcd(256)
|
|
with self.assertRaises(ValueError):
|
|
civ.level_to_bcd(-1)
|
|
|
|
|
|
class TestBuildFrame(unittest.TestCase):
|
|
def test_read_freq_frame(self):
|
|
# FE FE <radio=8C> <ctrl=E0> 03 FD
|
|
self.assertEqual(civ.build_frame(0x03), bytes.fromhex("fefe8ce003fd"))
|
|
|
|
def test_frame_with_data(self):
|
|
# set freq 147.3 MHz: FE FE 8C E0 05 <5 BCD> FD
|
|
frame = civ.build_frame(0x05, civ.freq_to_bcd(147_300_000))
|
|
self.assertEqual(frame, bytes.fromhex("fefe8ce005" + "0000304701" + "fd"))
|
|
|
|
|
|
class TestParseFrames(unittest.TestCase):
|
|
# Exact bytes the radio returned during the probe (echo + reply).
|
|
REAL_RX = bytes.fromhex("fefe8ce003fd" + "fefee08c030000304701fd")
|
|
|
|
def test_splits_echo_and_reply(self):
|
|
frames = civ.parse_frames(self.REAL_RX)
|
|
self.assertEqual(len(frames), 2)
|
|
echo, reply = frames
|
|
self.assertEqual((echo.to, echo.frm, echo.cn), (0x8C, 0xE0, 0x03))
|
|
self.assertEqual(echo.data, b"")
|
|
self.assertEqual((reply.to, reply.frm, reply.cn), (0xE0, 0x8C, 0x03))
|
|
self.assertEqual(reply.data, bytes.fromhex("0000304701"))
|
|
|
|
def test_ignores_leading_noise_and_partial_tail(self):
|
|
noisy = b"\x00\xff" + self.REAL_RX + b"\xfe\xfe\x99" # partial frame at end
|
|
frames = civ.parse_frames(noisy)
|
|
self.assertEqual(len(frames), 2)
|
|
|
|
def test_empty_buffer(self):
|
|
self.assertEqual(civ.parse_frames(b""), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|