xrd improvements

This commit is contained in:
2026-03-20 18:16:37 +01:00
parent 06f9fdf83c
commit 5ca98b993e
2 changed files with 106 additions and 99 deletions
+14
View File
@@ -0,0 +1,14 @@
import time
class Timer:
def __init__(self) -> None:
self.ctime = 0.0
self.reset()
def reset(self) -> float:
self.ctime = time.monotonic()
return self.ctime
def subtract(self, value: float) -> float:
self.ctime -= value
return self.ctime
def get_time(self) -> float:
return (time.monotonic()-self.ctime)
+92 -99
View File
@@ -1,18 +1,15 @@
# This is a compatible server for the TEF6686 firmware, but instead using the i2c control mode # This is a compatible server for the TEF6686 firmware, but instead using the i2c control mode
# This fully works with FM-DX-Webserver (no plugins) # This fully works with FM-DX-Webserver (no plugins) + Spectrum plugin
# Released under the Unlicense (however both FM-DX-Webserver and the firmware are under the tyranny of GPL-3) # Released under the Unlicense (however both FM-DX-Webserver and the firmware are under the tyranny of GPL-3)
import socket import socket, os, time
import hashlib import hashlib, secrets, string
import secrets from libtimer2 import Timer
import string
import libtimer
import time
from driver.tef import TEF6686 from driver.tef import TEF6686
from driver.protocol import I2CPCClient from driver.protocol import I2CPCClient
import os
from functools import wraps from functools import wraps
from typing import Callable from typing import Callable
from dataclasses import dataclass
INITIAL_FREQ = 9500 INITIAL_FREQ = 9500
INITIAL_EQ = True INITIAL_EQ = True
@@ -20,37 +17,47 @@ INITIAL_IMS = True
HOST = os.getenv("HOST") or '0.0.0.0' HOST = os.getenv("HOST") or '0.0.0.0'
PORT = int(os.getenv("PORT") or 0) or 7373 PORT = int(os.getenv("PORT") or 0) or 7373
DEVICE = os.getenv("DEV") or "COM6" DEVICE = os.getenv("DEV", "COM6")
FREQ_NOT_ALLOWED_RANGE = [] PASSWORD = os.getenv("PW", "test")
if PASSWORD == "test": print("Password left at default. Please change if neccesary")
PASSWORD = os.getenv("PW") or "test" CLOCK_ENV = os.getenv("CLK", "dp").strip().lower()
CLOCK_ENV = os.getenv("CLK", "").strip().lower() or "dp"
if CLOCK_ENV == "dp": CLOCK = 12000000 # DP-666 if CLOCK_ENV == "dp": CLOCK = 12000000 # DP-666
elif CLOCK_ENV == "x": CLOCK = 9216000 elif CLOCK_ENV == "x": CLOCK = 9216000
else: CLOCK = int(CLOCK_ENV) else: CLOCK = int(CLOCK_ENV)
SALT_LENGTH = 16 SALT_LENGTH = 16
SS_UPDATE_INTERVAL = 0.095 SS_UPDATE_INTERVAL = 0.098
RDS_UPDATE_INTERVAL = 0.086 RDS_UPDATE_INTERVAL = 0.0856
def freq_allowed(freq: int): @dataclass
if not FREQ_NOT_ALLOWED_RANGE: return True class State:
for (lower, upper) in FREQ_NOT_ALLOWED_RANGE: last_tune: int = INITIAL_FREQ
if freq >= lower and freq <= upper: return False last_eqims: int = (INITIAL_EQ << 1) | INITIAL_IMS
return True forced_mono: bool = False
deemp: int = 0
bw: int = 0
scan_start: int = 8750
scan_stop: int = 10800
scan_step: int = 10
scan_bw: int = 0
def init_tef(): def init_tef():
p = I2CPCClient(DEVICE, int(os.getenv("BAUD") or 0) or 115200) tef = TEF6686(I2CPCClient(DEVICE, int(os.getenv("BAUD") or 0) or 115200))
tef = TEF6686(p)
tef.init(clock=CLOCK) tef.init(clock=CLOCK)
tef.AUDIO_Set_Mute(False) tef.AUDIO_Set_Mute(False)
tef.AUDIO_Set_Volume(20)
tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, INITIAL_FREQ) tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, INITIAL_FREQ)
tef.FM_Set_RDS(1) tef.FM_Set_RDS(1)
tef.FM_Set_ChannelEqualizer(INITIAL_EQ) tef.FM_Set_ChannelEqualizer(INITIAL_EQ)
tef.FM_Set_MphSuppression(INITIAL_IMS) tef.FM_Set_MphSuppression(INITIAL_IMS)
tef.FM_Set_LevelStep(-1, -1, -1, -1, -4, -8, 0)
tef.FM_Set_Highcut_Mph(0)
tef.FM_Set_Highcut_Max(False)
tef.FM_Set_Lowcut_Max(True, 60)
tef.FM_Set_Stereo_Time(60, 120, 100, 200)
tef.FM_Set_Stereo_Max(False) # Disables stereo blend??!!??!? tef.FM_Set_Stereo_Max(False) # Disables stereo blend??!!??!?
tef.APPL_Set_OperationMode(True) # Turn off tef.APPL_Set_OperationMode(True) # Turn off
return tef return tef
@@ -59,100 +66,96 @@ def authenticate(conn: socket.socket):
salt = "".join(secrets.choice(string.ascii_lowercase) for _ in range(SALT_LENGTH)) salt = "".join(secrets.choice(string.ascii_lowercase) for _ in range(SALT_LENGTH))
conn.sendall(salt.encode() + b"\n") conn.sendall(salt.encode() + b"\n")
expected_hash = hashlib.sha1((salt + PASSWORD).encode()).hexdigest().encode() expected_hash = hashlib.sha1((salt + PASSWORD).encode()).hexdigest().encode()
start = time.monotonic() tries = 0
while True: while True:
try: try:
data = conn.recv(len(expected_hash)) data = conn.recv(len(expected_hash))
if not data: return False if not data: break
if data.strip() == expected_hash.strip(): return True
except BlockingIOError:
if (time.monotonic() - start) > 3: conn.close() # Close connection if they can't fucking bruteforce the password in 3 seconds
def process_command(tef: TEF6686, data: bytes, state: dict, conn: socket.socket): if data.strip() == expected_hash.strip():
conn.sendall(b"o1,0\n")
return True
else:
tries += 1
conn.sendall(b"a0\n") # You failed
except BlockingIOError:
if tries > 2:
conn.close()
break
return False
def process_command(tef: TEF6686, data: bytes, state: State, conn: socket.socket):
out = b"" out = b""
for cmd in data.splitlines(): for cmd in data.splitlines():
if cmd.startswith(b"T"): if cmd.startswith(b"T"):
freq = int(cmd.decode().removeprefix("T").strip()) // 10 freq = int(cmd.decode().removeprefix("T").strip()) // 10
if freq < 6500 or freq > 10800: continue if freq < 6500 or freq > 10800: continue
tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, freq) tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, freq)
if not freq_allowed(freq): tef.AUDIO_Set_Mute(True) state.last_tune = freq
else: tef.AUDIO_Set_Mute(False)
state['last_tune'] = freq
out += f"T{freq*10}\n".encode() out += f"T{freq*10}\n".encode()
elif cmd.startswith(b"G"): elif cmd.startswith(b"G"):
eqims = int(cmd.decode().removeprefix("G").strip(), 2) eqims = int(cmd.decode().removeprefix("G").strip(), 2)
tef.FM_Set_ChannelEqualizer((eqims & 1) == 1) tef.FM_Set_ChannelEqualizer((eqims & 1) == 1)
tef.FM_Set_MphSuppression((eqims & 2) == 2) tef.FM_Set_MphSuppression((eqims & 2) == 2)
out += f"G{bin(eqims).removeprefix('0b').zfill(2)}\n".encode() out += f"G{bin(eqims).removeprefix('0b').zfill(2)}\n".encode()
state['last_eqims'] = eqims state.last_eqims = eqims
elif cmd.startswith(b"B"): elif cmd.startswith(b"B"):
mono = bool(int(cmd.decode().removeprefix("B").strip(), 2)) mono = bool(int(cmd.decode().removeprefix("B").strip(), 2))
tef.FM_Set_Stereo_Min(2 if mono else 0) tef.FM_Set_Stereo_Min(2 if mono else 0)
out += f"B{int(mono)}\n".encode() out += f"B{int(mono)}\n".encode()
state['forced_mono'] = mono state.forced_mono = mono
elif cmd.startswith(b"D"): elif cmd.startswith(b"D"):
deemp = int(cmd.decode().removeprefix("D").strip()) state.deemp = int(cmd.decode().removeprefix("D").strip())
dtime = 500 if deemp == 0 else (750 if deemp == 1 else 0) dtime = 500 if state.deemp == 0 else (750 if state.deemp == 1 else 0)
tef.FM_Set_Deemphasis(dtime) tef.FM_Set_Deemphasis(dtime)
out += f"D{deemp}\n".encode() out += f"D{state.deemp}\n".encode()
state['deemp'] = deemp
elif cmd.startswith(b"x"): elif cmd.startswith(b"x"):
out += b"OK\n" out += b"OK\n"
tef.APPL_Set_OperationMode(False) # Enable tef.APPL_Set_OperationMode(False) # Enable
tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, state["last_tune"]) tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, state.last_tune)
if not freq_allowed(state["last_tune"]): tef.AUDIO_Set_Mute(True) elif cmd.startswith(b"X"): tef.APPL_Set_OperationMode(True) # turn off
else: tef.AUDIO_Set_Mute(False)
elif cmd.startswith(b"X"):
tef.APPL_Set_OperationMode(True) # turn off
elif cmd.startswith(b"W"): elif cmd.startswith(b"W"):
bw = int(cmd.decode().removeprefix("W").strip()) state.bw = int(cmd.decode().removeprefix("W").strip())
auto = (bw == 0) auto = (state.bw == 0)
tef.FM_Set_Bandwidth(auto, 2360 if auto else (bw // 100)) tef.FM_Set_Bandwidth(auto, 2360 if auto else (state.bw // 100))
state['bw'] = bw out += f"W{state.bw}\n".encode()
out += f"W{bw}\n".encode() elif cmd.startswith(b"?"): out += b".XRD Python driver\n"
elif cmd.startswith(b"?"):
out += b"XRD Python driver\n"
elif cmd.startswith(b"S"): elif cmd.startswith(b"S"):
cmd = cmd[1:] cmd = cmd[1:]
if cmd != b"": if cmd != b"":
arg = int(cmd.decode()[1:].strip()) arg = int(cmd.decode()[1:].strip())
match cmd[0]: match cmd[0]:
case 97: state['scan_start'] = (arg + 5) // 10 case 97: state.scan_start = (arg + 5) // 10
case 98: state['scan_stop'] = (arg + 5) // 10 case 98: state.scan_stop = (arg + 5) // 10
case 99: state['scan_step'] = (arg + 5) // 10 case 99: state.scan_step = (arg + 5) // 10
case 119: state['scan_bw'] = arg case 119: state.scan_bw = arg
else: else:
start = True start = True
tef.FM_Set_Bandwidth((state["scan_bw"] == 0), state["scan_bw"]) tef.FM_Set_Bandwidth((state.scan_bw == 0), state.scan_bw)
conn.sendall(b"U") conn.sendall(b"U")
for freq in range(state["scan_start"], state["scan_stop"] + state["scan_step"], state["scan_step"]): for freq in range(state.scan_start, state.scan_stop + state.scan_step, state.scan_step):
if not start: conn.sendall(b", ") # Prevent trailing comma, because the FM-DX-Webserver spectrum plugin treats us as actual firmware and throws as harder api, without it we're treated as a module if not start: conn.sendall(b", ") # Prevent trailing comma, because the FM-DX-Webserver spectrum plugin treats us as actual firmware and throws as harder api, without it we're treated as a module
start = False start = False
tef.FM_Tune_To(TEF6686.TuneTo_Mode.Search, freq) # Auto mutes, less commands sent tef.FM_Tune_To(TEF6686.TuneTo_Mode.Search, freq) # Auto mutes, less commands sent
time.sleep(0.0067) # sick seven time.sleep(0.0067) # sick seven
if not freq_allowed(freq):
conn.sendall(f"{freq * 10} = 11.25".encode())
continue
_, level, *_ = d if (d := tef.FM_Get_Quality_Data()) else (None, None) _, level, *_ = d if (d := tef.FM_Get_Quality_Data()) else (None, None)
if level is None: continue if level is None: continue
conn.sendall(str(freq * 10).encode() + b" = " + str((level / 10) + 11.25).encode()) conn.sendall(str(freq * 10).encode() + b" = " + str((level / 10) + 11.25).encode())
conn.sendall(b"\n") conn.sendall(b"\n")
tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, state["last_tune"]) tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, state.last_tune)
if not freq_allowed(state["last_tune"]): tef.AUDIO_Set_Mute(True) tef.FM_Set_Bandwidth((state.bw == 0), 2360 if (state.bw == 0) else (state.bw // 100))
else: tef.AUDIO_Set_Mute(False)
tef.FM_Set_Bandwidth((state["bw"] == 0), 2360 if (state["bw"] == 0) else (state["bw"] // 100))
return out return out
PERIODIC_FUNCTIONS: list[tuple[Callable, float, libtimer.Timer]] = [] PERIODIC_FUNCTIONS: list[tuple[Callable, float, Timer]] = []
def periodic(t: float): def periodic(t: float):
def decorator(func): def decorator(func):
PERIODIC_FUNCTIONS.append((func, t, libtimer.Timer())) PERIODIC_FUNCTIONS.append((func, t, Timer()))
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): return func(*args, **kwargs) def wrapper(*args, **kwargs): return func(*args, **kwargs)
return wrapper return wrapper
@@ -163,15 +166,15 @@ def reset_periodic():
def run_periodic(*args, **kwargs): def run_periodic(*args, **kwargs):
for (func, t, timer) in PERIODIC_FUNCTIONS: for (func, t, timer) in PERIODIC_FUNCTIONS:
if timer.get_time() > t: max_catchup = 10
count = 0
while timer.get_time() > t and count < max_catchup:
func(*args, **kwargs) func(*args, **kwargs)
timer.reset() timer.subtract(t)
count += 1
@periodic(SS_UPDATE_INTERVAL) @periodic(SS_UPDATE_INTERVAL)
def send_signal_status(tef: TEF6686, conn: socket.socket, state: dict): def send_signal_status(tef: TEF6686, conn: socket.socket, state: State):
if not freq_allowed(state["last_tune"]):
conn.sendall(b"Sm11.25,0,0,0\n\n")
return
stereo_pilot, _ = d if (d := tef.FM_Get_Signal_Status()) else (None, None) stereo_pilot, _ = d if (d := tef.FM_Get_Signal_Status()) else (None, None)
res = tef.FM_Get_Quality_Data() res = tef.FM_Get_Quality_Data()
@@ -181,17 +184,15 @@ def send_signal_status(tef: TEF6686, conn: socket.socket, state: dict):
level = level / 10 level = level / 10
data = b"S" data = b"S"
if state['forced_mono']: data += b"M"
if state.forced_mono: data += b"M"
elif stereo_pilot: data += b"s" elif stereo_pilot: data += b"s"
else: data += b"m" else: data += b"m"
data += f"{level + 11.25},{wam//10},{usn//10},{bandwidth}\n\n".encode() conn.sendall(data + f"{level + 11.25},{wam//10},{usn//10},{bandwidth}\n\n".encode())
conn.sendall(data)
@periodic(RDS_UPDATE_INTERVAL) @periodic(RDS_UPDATE_INTERVAL)
def send_rds_data(tef: TEF6686, conn: socket.socket, state: dict): def send_rds_data(tef: TEF6686, conn: socket.socket, state: State):
if not freq_allowed(state["last_tune"]): return
res = tef.FM_Get_RDS_Data__decoder() res = tef.FM_Get_RDS_Data__decoder()
if res is None: return if res is None: return
status, A, B, C, D, dec_error = res status, A, B, C, D, dec_error = res
@@ -209,7 +210,7 @@ def send_rds_data(tef: TEF6686, conn: socket.socket, state: dict):
data = b"R" data = b"R"
data += f"{B:04X}{C:04X}{D:04X}{err:02X}\n".encode() data += f"{B:04X}{C:04X}{D:04X}{err:02X}\n".encode()
pi_error = (dec_error >> 8) & 0b11 pi_error = (dec_error >> 6) & 0b11
if pi_error < 3: if pi_error < 3:
data += b"P" + f"{A:04X}".encode() data += b"P" + f"{A:04X}".encode()
data += b"?" * pi_error + b"\n" data += b"?" * pi_error + b"\n"
@@ -218,7 +219,6 @@ def send_rds_data(tef: TEF6686, conn: socket.socket, state: dict):
if pi_error < 3: if pi_error < 3:
data += b"P" + f"{C:04X}".encode() data += b"P" + f"{C:04X}".encode()
data += b"?" * pi_error + b"\n" data += b"?" * pi_error + b"\n"
if not data.strip(): return
conn.sendall(data) conn.sendall(data)
def run_server(): def run_server():
@@ -229,25 +229,17 @@ def run_server():
hw_major = hw_version >> 8 hw_major = hw_version >> 8
hw_minor = hw_version & 127 hw_minor = hw_version & 127
sw_major = sw_version >> 8 sw_major = sw_version >> 8
sw_minor = sw_version & 127
variant_str = "TEF6686" variant_str = "TEF6686"
if variant == 1: variant_str = "TEF6687" if variant == 1: variant_str = "TEF6687"
elif variant == 9: variant_str = "TEF6688" elif variant == 9: variant_str = "TEF6688"
elif variant == 3: variant_str = "TEF6689" elif variant == 3: variant_str = "TEF6689"
print(f"{variant_str} (V{hw_major}{hw_minor}{sw_major})") print(f"{variant_str} (V{hw_major}{hw_minor}{sw_major}.{sw_minor})")
else: print("Could not fetch variant")
state = {
'last_tune': INITIAL_FREQ, state = State()
'last_eqims': (INITIAL_EQ << 1) | INITIAL_IMS,
'forced_mono': False,
'deemp': 0,
'bw': 0,
'scan_start': 87500,
'scan_stop': 108000,
'scan_step': 100,
'scan_bw': 0,
}
with socket.socket() as s: with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -266,11 +258,11 @@ def run_server():
continue continue
# Send initial state # Send initial state
conn.sendall(f"T{state['last_tune']*10}\n".encode()) conn.sendall(f"T{state.last_tune*10}\n".encode())
conn.sendall(f"G{bin(state['last_eqims']).removeprefix('0b').zfill(2)}\n".encode()) conn.sendall(f"G{bin(state.last_eqims).removeprefix('0b').zfill(2)}\n".encode())
conn.sendall(f"B{int(state['forced_mono'])}\n".encode()) conn.sendall(f"B{int(state.forced_mono)}\n".encode())
conn.sendall(f"D{state['deemp']}\n".encode()) conn.sendall(f"D{state.deemp}\n".encode())
conn.sendall(f"W{state['bw']}\n".encode()) conn.sendall(f"W{state.bw}\n".encode())
conn.sendall(f"M0\n".encode()) conn.sendall(f"M0\n".encode())
while True: while True:
@@ -282,8 +274,9 @@ def run_server():
except ConnectionAbortedError: break except ConnectionAbortedError: break
except BlockingIOError: except BlockingIOError:
run_periodic(tef, conn, state) run_periodic(tef, conn, state)
time.sleep(0.025) time.sleep(0.03)
import traceback
if __name__ == "__main__": if __name__ == "__main__":
try: run_server() try: run_server()
except Exception: pass except Exception as e: traceback.print_exception(e)