From 08fd5dbb4010ce564348923aaef6e62b08998d2b Mon Sep 17 00:00:00 2001 From: Seth Freiberg Date: Sun, 14 Jun 2026 16:12:50 -0400 Subject: [PATCH] feat: radio duplex/offset/tone control + tune_repeater --- radio.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/radio.py b/radio.py index 986d881..080af07 100644 --- a/radio.py +++ b/radio.py @@ -156,3 +156,51 @@ class Radio: def squelch_open(self): data = self.transact(civ.CMD_METER, bytes([civ.SUB_SQL_STATUS])).data return bool(data[1]) if len(data) > 1 else False + + # ---- repeater config (candidate commands; verify live) ---------------- + def set_duplex(self, direction): + """direction: '+', '-', or 'simplex'.""" + code = {"+": civ.DUP_PLUS, "-": civ.DUP_MINUS, + "simplex": civ.DUP_SIMPLEX}.get(direction) + if code is None: + raise ValueError(f"bad duplex {direction!r}") + return self._command(civ.CMD_DUPLEX, bytes([code])) + + def set_offset(self, hz): + return self._command(civ.CMD_OFFSET, civ.offset_to_bcd(hz)) + + def get_offset(self): + return civ.bcd_to_offset(self.transact(civ.CMD_OFFSET).data) + + def set_tone(self, hz): + return self._command(civ.CMD_TONE_PARAM, + bytes([civ.SUB_RPT_TONE]) + civ.tone_to_bcd(hz)) + + def get_tone(self): + return civ.bcd_to_tone(self.transact(civ.CMD_TONE_PARAM, + bytes([civ.SUB_RPT_TONE])).data[1:]) + + def set_tone_on(self, on): + return self._command(civ.CMD_TONE_SWITCH, + bytes([civ.SUB_TONE_ENC, 0x01 if on else 0x00])) + + def tune_repeater(self, rec): + """Apply a normalized record: freq -> duplex/offset -> tone. + Returns (steps, errors): each step True/False; errors keyed by step.""" + steps, errors = {}, {} + + def attempt(name, fn): + try: + fn(); steps[name] = True + except (CIVError, ValueError, OSError) as e: + steps[name] = False; errors[name] = str(e) + + attempt("freq", lambda: self.set_frequency(int(round(rec["output_mhz"] * 1e6)))) + duplex = rec.get("duplex", "simplex") + attempt("duplex", lambda: self.set_duplex(duplex)) + if duplex != "simplex": + attempt("offset", lambda: self.set_offset(int(round(rec.get("offset_mhz", 0) * 1e6)))) + if rec.get("tone_mode") == "ctcss" and rec.get("tone_hz"): + attempt("tone_freq", lambda: self.set_tone(rec["tone_hz"])) + attempt("tone_on", lambda: self.set_tone_on(True)) + return steps, errors