96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
"""
|
|
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
|