chore: baseline existing CI-V suite before RepeaterBook integration

This commit is contained in:
2026-06-14 16:02:36 -04:00
commit 03000708b6
14 changed files with 3550 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""
Web control panel server for the ICOM ID-5100.
Holds one CI-V serial connection (so don't run cli.py at the same time),
serves the instrument-panel UI, and exposes a tiny JSON API on top of radio.py.
python3 server.py [--port 8550] [--serial /dev/ttyUSB0] [--baud 9600]
Then browse to http://<host>:8550/ (binds 0.0.0.0 -> reachable from phone).
"""
import argparse
import json
import os
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from radio import Radio, CIVError
HERE = os.path.dirname(os.path.abspath(__file__))
PANEL = os.path.join(HERE, "panel.html")
radio = None
lock = threading.Lock()
def read_status():
with lock:
freq = radio.get_frequency()
mode, code = radio.get_mode()
return {
"ok": True,
"freq_mhz": round(freq / 1e6, 5),
"mode": mode,
"mode_code": code,
"volume": radio.get_volume(),
"squelch_level": radio.get_squelch_level(),
"squelch_open": radio.squelch_open(),
"smeter": radio.get_smeter(),
}
def apply_control(path, body):
with lock:
if path == "/api/freq":
radio.set_frequency(int(round(float(body["mhz"]) * 1e6)))
elif path == "/api/mode":
radio.set_mode(str(body["mode"]))
elif path == "/api/vol":
radio.set_volume(int(body["value"]))
elif path == "/api/sql":
radio.set_squelch_level(int(body["value"]))
else:
raise CIVError("unknown control")
return {"ok": True}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *a):
pass # quiet
def _json(self, obj, code=200):
payload = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_GET(self):
if self.path == "/" or self.path == "/index.html":
with open(PANEL, "rb") as f:
body = f.read()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif self.path == "/api/status":
try:
self._json(read_status())
except (CIVError, OSError) as e:
self._json({"ok": False, "error": str(e)}, 200)
else:
self._json({"ok": False, "error": "not found"}, 404)
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
try:
body = json.loads(self.rfile.read(length) or b"{}")
self._json(apply_control(self.path, body))
except (CIVError, OSError, ValueError, KeyError) as e:
self._json({"ok": False, "error": str(e)}, 200)
def main():
global radio
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8550)
ap.add_argument("--serial", default="/dev/ttyUSB0")
ap.add_argument("--baud", type=int, default=9600)
args = ap.parse_args()
radio = Radio(args.serial, args.baud).open()
httpd = ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
print(f"ID-5100 panel on http://0.0.0.0:{args.port}/ "
f"(serial {args.serial} @ {args.baud})")
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
finally:
radio.close()
if __name__ == "__main__":
main()