chore: baseline existing CI-V suite before RepeaterBook integration
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Pure CI-V codec for the ICOM ID-5100 — no I/O, no dependencies.
|
||||
|
||||
CI-V frame: FE FE <to> <from> <cmd> [sub] [data...] FD
|
||||
All multi-byte numeric fields are BCD. Frequency is 5-byte little-endian BCD;
|
||||
levels/meters are 2-byte big-endian BCD in the range 0000..0255.
|
||||
"""
|
||||
from collections import namedtuple
|
||||
|
||||
RADIO_ADDR = 0x8C # ID-5100 default CI-V address
|
||||
CTRL_ADDR = 0xE0 # this controller's address
|
||||
PREAMBLE = b"\xFE\xFE"
|
||||
EOM = 0xFD
|
||||
|
||||
# Command bytes (generic ICOM CI-V; verified subset for the ID-5100)
|
||||
CMD_READ_FREQ = 0x03
|
||||
CMD_READ_MODE = 0x04
|
||||
CMD_SET_FREQ = 0x05
|
||||
CMD_SET_MODE = 0x06
|
||||
CMD_LEVEL = 0x14 # sub 0x01 = AF (volume), 0x03 = squelch level
|
||||
CMD_METER = 0x15 # sub 0x01 = squelch status, 0x02 = S-meter
|
||||
SUB_AF = 0x01
|
||||
SUB_SQL_LEVEL = 0x03
|
||||
SUB_SQL_STATUS = 0x01
|
||||
SUB_SMETER = 0x02
|
||||
|
||||
Frame = namedtuple("Frame", "to frm cn data")
|
||||
|
||||
|
||||
def freq_to_bcd(hz):
|
||||
"""Hz -> 5-byte little-endian BCD."""
|
||||
digits = f"{hz:010d}" # 10 decimal digits (up to 9.999 GHz)
|
||||
out = bytearray()
|
||||
# pairs from least significant; byte0 holds the lowest two digits
|
||||
for i in range(0, 10, 2):
|
||||
hi = digits[8 - i]
|
||||
lo = digits[9 - i]
|
||||
out.append((int(hi) << 4) | int(lo))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def bcd_to_freq(data):
|
||||
"""5-byte little-endian BCD -> Hz."""
|
||||
hz = 0
|
||||
for i, byte in enumerate(data[:5]):
|
||||
ones = byte & 0x0F
|
||||
tens = byte >> 4
|
||||
hz += ones * (10 ** (2 * i)) + tens * (10 ** (2 * i + 1))
|
||||
return hz
|
||||
|
||||
|
||||
def level_to_bcd(value):
|
||||
"""0..255 -> 2-byte BCD (e.g. 128 -> 01 28)."""
|
||||
if not 0 <= value <= 255:
|
||||
raise ValueError(f"level {value} out of range 0..255")
|
||||
digits = f"{value:04d}"
|
||||
return bytes([(int(digits[0]) << 4) | int(digits[1]),
|
||||
(int(digits[2]) << 4) | int(digits[3])])
|
||||
|
||||
|
||||
def bcd_to_level(data):
|
||||
"""2-byte BCD -> int 0..255."""
|
||||
value = 0
|
||||
for byte in data[:2]:
|
||||
value = value * 100 + (byte >> 4) * 10 + (byte & 0x0F)
|
||||
return value
|
||||
|
||||
|
||||
def build_frame(cmd, data=b"", to=RADIO_ADDR, frm=CTRL_ADDR):
|
||||
"""Assemble FE FE <to> <from> <cmd> [data] FD."""
|
||||
return PREAMBLE + bytes([to, frm, cmd]) + bytes(data) + bytes([EOM])
|
||||
|
||||
|
||||
def parse_frames(buf):
|
||||
"""Extract complete CI-V frames from a byte buffer, skipping noise/echo.
|
||||
|
||||
Returns a list of Frame(to, frm, cn, data) where `data` is everything
|
||||
between the command byte and the EOM (so it includes any subcommand byte).
|
||||
"""
|
||||
frames = []
|
||||
i, n = 0, len(buf)
|
||||
while i < n - 1:
|
||||
if buf[i] == 0xFE and buf[i + 1] == 0xFE:
|
||||
j = i + 2
|
||||
while j < n and buf[j] != EOM:
|
||||
j += 1
|
||||
if j >= n:
|
||||
break # incomplete trailing frame
|
||||
inner = buf[i + 2:j]
|
||||
if len(inner) >= 3:
|
||||
frames.append(Frame(inner[0], inner[1], inner[2], bytes(inner[3:])))
|
||||
i = j + 1
|
||||
continue
|
||||
i += 1
|
||||
return frames
|
||||
Reference in New Issue
Block a user