Files
id5100/civ_probe.py
T

136 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""
ICOM ID-5100 CI-V link probe (pure stdlib, no pyserial).
Opens the RT Systems USB-29A cable (FTDI, plugged into the radio's [SP2]/CI-V
jack), sends a "read operating frequency" CI-V frame, and decodes the reply.
A valid reply proves the PC <-> radio control link end-to-end.
Defaults: /dev/ttyUSB0, 9600 baud, radio CI-V address 0x8C, controller 0xE0.
Usage:
python3 civ_probe.py [PORT] [BAUD]
python3 civ_probe.py /dev/ttyUSB0 9600
"""
import os
import sys
import time
import termios
RADIO_ADDR = 0x8C # ID-5100 default CI-V address
CTRL_ADDR = 0xE0 # this controller's address
PREAMBLE = b"\xFE\xFE"
EOM = 0xFD
BAUD_CONST = {
1200: termios.B1200, 4800: termios.B4800, 9600: termios.B9600,
19200: termios.B19200, 38400: termios.B38400, 115200: termios.B115200,
}
def open_port(port, baud):
fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
speed = BAUD_CONST.get(baud)
if speed is None:
raise ValueError(f"unsupported baud {baud}; choose one of {sorted(BAUD_CONST)}")
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(fd)
# raw 8N1, receiver on, ignore modem control lines
iflag = 0
oflag = 0
lflag = 0
cflag = termios.CS8 | termios.CREAD | termios.CLOCAL
cc = list(cc)
cc[termios.VMIN] = 0
cc[termios.VTIME] = 0
termios.tcsetattr(fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, speed, speed, cc])
termios.tcflush(fd, termios.TCIOFLUSH)
return fd
def read_for(fd, seconds):
"""Collect raw bytes for `seconds`, return bytearray."""
buf = bytearray()
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
try:
chunk = os.read(fd, 256)
except BlockingIOError:
chunk = b""
if chunk:
buf.extend(chunk)
else:
time.sleep(0.01)
return buf
def split_frames(buf):
"""Yield CI-V frames (FE FE ... FD), skipping leading echo/noise."""
i = 0
n = 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:
yield bytes(buf[i:j + 1])
i = j + 1
continue
i += 1
def decode_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 main():
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
baud = int(sys.argv[2]) if len(sys.argv) > 2 else 9600
print(f"Port {port} @ {baud} baud | radio 0x{RADIO_ADDR:02X} ctrl 0x{CTRL_ADDR:02X}")
fd = open_port(port, baud)
try:
# read operating frequency: FE FE <radio> <ctrl> 03 FD
cmd = PREAMBLE + bytes([RADIO_ADDR, CTRL_ADDR, 0x03, EOM])
print("TX:", cmd.hex(" "))
os.write(fd, cmd)
buf = read_for(fd, 1.0)
finally:
os.close(fd)
if not buf:
print("RX: (nothing)")
print("\nNo bytes back. Check: SP2 jack seated, radio CI-V baud = "
f"{baud}, radio powered on. Single-device port was confirmed as this one.")
sys.exit(2)
print("RX raw:", buf.hex(" "))
got_freq = False
for frame in split_frames(buf):
# frame: FE FE <to> <from> <cn> [data...] FD
if len(frame) < 6:
continue
to, frm, cn = frame[2], frame[3], frame[4]
body = frame[5:-1]
tag = "echo" if (to == RADIO_ADDR and frm == CTRL_ADDR) else "radio->us"
print(f" frame [{tag}] to=0x{to:02X} from=0x{frm:02X} cn=0x{cn:02X} body={body.hex(' ')}")
if frm == RADIO_ADDR and cn == 0x03 and len(body) >= 5:
hz = decode_freq(body)
print(f"\n*** LINK OK -- VFO frequency: {hz/1e6:.6f} MHz ***")
got_freq = True
if not got_freq:
print("\nGot bytes but no frequency frame. Link is alive but reply was "
"unexpected -- check baud match and CI-V address.")
sys.exit(1)
if __name__ == "__main__":
main()