#!/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://:8550/ (binds 0.0.0.0 -> reachable from phone). """ import argparse import json import os import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import repeaters from radio import Radio, CIVError HERE = os.path.dirname(os.path.abspath(__file__)) PANEL = os.path.join(HERE, "panel.html") REPEATERS_JSON = os.path.join(HERE, "repeaters.json") 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"])) elif path == "/api/tune-repeater": steps, errors = radio.tune_repeater(body) return {"ok": bool(steps.get("freq")), "steps": steps, "errors": errors} 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) elif self.path.startswith("/api/repeaters"): from urllib.parse import urlparse, parse_qs qs = parse_qs(urlparse(self.path).query) q = qs.get("q", [""])[0] limit = int(qs.get("limit", ["25"])[0]) recs = repeaters.search(repeaters.load(REPEATERS_JSON), q, limit) self._json({"ok": True, "repeaters": recs}) 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()