142 lines
4.3 KiB
Python
142 lines
4.3 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
|
|
|
|
# --- repeater config (candidate commands; verify on live radio) ---
|
|
CMD_OFFSET = 0x0D # set duplex offset frequency
|
|
CMD_DUPLEX = 0x0F # set duplex direction
|
|
DUP_SIMPLEX = 0x10
|
|
DUP_MINUS = 0x11
|
|
DUP_PLUS = 0x12
|
|
CMD_TONE_PARAM = 0x1B # sub 0x00 = repeater (TX) tone frequency
|
|
SUB_RPT_TONE = 0x00
|
|
CMD_TONE_SWITCH = 0x16 # sub 0x42 = tone encode on/off
|
|
SUB_TONE_ENC = 0x42
|
|
|
|
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 offset_to_bcd(hz):
|
|
"""Duplex offset Hz -> 3-byte little-endian BCD in 100 Hz units (PROVISIONAL)."""
|
|
if hz < 0:
|
|
raise ValueError(f"offset {hz} must be >= 0")
|
|
units = round(hz / 100)
|
|
digits = f"{units:06d}"
|
|
out = bytearray()
|
|
for i in range(0, 6, 2):
|
|
out.append((int(digits[4 - i]) << 4) | int(digits[5 - i]))
|
|
return bytes(out)
|
|
|
|
|
|
def bcd_to_offset(data):
|
|
"""3-byte little-endian BCD (100 Hz units) -> Hz (PROVISIONAL)."""
|
|
units = 0
|
|
for i, byte in enumerate(data[:3]):
|
|
units += (byte & 0x0F) * (10 ** (2 * i)) + (byte >> 4) * (10 ** (2 * i + 1))
|
|
return units * 100
|
|
|
|
|
|
def tone_to_bcd(hz):
|
|
"""CTCSS tone Hz -> 2-byte BCD of tone*10 (88.5 -> 08 85)."""
|
|
digits = f"{round(hz * 10):04d}"
|
|
return bytes([(int(digits[0]) << 4) | int(digits[1]),
|
|
(int(digits[2]) << 4) | int(digits[3])])
|
|
|
|
|
|
def bcd_to_tone(data):
|
|
"""2-byte BCD of tone*10 -> CTCSS tone Hz."""
|
|
t = 0
|
|
for byte in data[:2]:
|
|
t = t * 100 + (byte >> 4) * 10 + (byte & 0x0F)
|
|
return t / 10.0
|
|
|
|
|
|
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
|