diff --git a/cliserver.py b/cliserver.py new file mode 100644 index 0000000..86c64b9 --- /dev/null +++ b/cliserver.py @@ -0,0 +1,395 @@ +from driver.tef import TEF6686 +from driver.protocol import I2CPCClient # cpcc? so close (cccp?), hammer and sicle? +import os +from multiprocessing import Queue, Process, freeze_support +from queue import Empty +import libtimer +import asyncio +import websockets +from websockets import Headers, Response +import json +from cliserver_rdsDecoder import decodeRds + +NAME = "CLI Server" +DESCRIPTION = "" + +INITIAL_FREQ = 95.0 + +LATITUDE = 0 +LONGITUDE = 0 + +PRESET0 = 87.5 +PRESET1 = 87.5 +PRESET2 = 87.5 +PRESET3 = 87.5 + +# Mint - 1 +# Cappucino - 2 +# Nature - 3 +# Ocean - 4 +# Terminal - 5 +# Nightlife - 6 +# Blurple - 7 +# Construction - 8 +# Amoled - 9 +# Dark Red - 10 +# Ocean Blue - 11 +DEFAULT_THEME = 1 + +BG_IMAGE = None + +RBDS = False +RDS_TIMEOUT = 0 + +# "af":[99800,102600,90300], +# "txInfo":{"tx":"Radio Plus","pol":"V","erp":0.2,"city":"Pila","itu":"POL","dist":"3","azi":"238","id":6400902,"pi":"321C","reg":false,"otherMatches":[],"score":1}, + +def serialpart(inq: Queue, outq: Queue, rds: Queue): + p = I2CPCClient(os.getenv("PORT", "COM17"), int(os.getenv("BAUD", 115200))) + tef = TEF6686(p) + tef.init(clock=12_000_000) + tef.FM_Set_RDS(1) + tef.AUDIO_Set_Mute(False) + tef.FM_Set_ChannelEqualizer() + tef.FM_Set_MphSuppression() + tef.FM_Set_Stereo_Max(False) + tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, int(INITIAL_FREQ * 100)) + + signal_timer = libtimer.Timer() + rds_timer = libtimer.Timer() + data_timer = libtimer.Timer() + + rds_dropout_time = libtimer.Timer() + + fm = True + + wsdata = { + "pi": "?", + "freq": f"{INITIAL_FREQ:.3f}", + "sig": 11.25, # dBf + "sigRaw": "", # Raw signal from xrd proto + "sigTop": -1000, + "bw": "", + "st": False, + "stForced": False, # its forced for mono, lol + "rds": False, + "ps": "".ljust(8), + "tp": 0, + "ta": 0, + "ms": -1, + "pty": 0, + "ecc": None, + "af": [], + "rt0": "".ljust(64), + "rt1": "".ljust(64), + "rt_flag": 0, + "ims": 0, + "eq": 0, + "agc": "0", # static + "ant": "1", # static + "txInfo": {}, + "country_name": "", + "country_iso": "UN", + "users": 0, + "ps_errors": "0,0,0,0,0,0,0,0", + "rt1_errors":"0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0", + "rt0_errors":"0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + } + + def resetRds(): + wsdata["rds"] = False + wsdata["pi"] = "?" + wsdata["ps"] = "".ljust(8) + wsdata["tp"] = 0 + wsdata["ta"] = 0 + wsdata["ms"] = -1 + wsdata["pty"] = 0 + wsdata["ecc"] = None + wsdata["af"] = [] + wsdata["rt0"] = "".ljust(64) + wsdata["rt1"] = "".ljust(64) + wsdata["rt_flag"] = 0 + wsdata["country_name"] = "" + wsdata["country_iso"] = "UN" + wsdata["ps_errors"] = "0,0,0,0,0,0,0,0" + wsdata["rt0_errors"] = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + wsdata["rt1_errors"] = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" + rds.put("G:\r\nRESET-------\r\n\r\n") + + while not p.ser.closed: + try: + msg = str(inq.get(False)) + if msg == "!x": + tef.close() + break + elif msg == ".": raise Empty + elif msg == "+": + wsdata["users"] += 1 + raise Empty + elif msg == "-": + wsdata["users"] -= 1 + raise Empty + out = "" + if msg.startswith("M"): + mod = int(msg.removeprefix("M").strip(), 2) + if mod: fm = False + else: fm = True + out += f"M{mod}\n" + elif msg.startswith("T"): + freq = int(msg.removeprefix("T").strip()) // 10 + if fm: + tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, freq) + wsdata["freq"] = f"{(freq / 100):.3f}" + else: + tef.AM_Tune_To(TEF6686.TuneTo_Mode.Preset, freq) + wsdata["freq"] = f"{(freq / 1000):.3f}" + out += f"T{freq*10}\n" + wsdata["sigTop"] = -1000 + resetRds() + elif msg.startswith("G"): + eqims = int(msg.removeprefix("G").strip(), 2) + tef.FM_Set_ChannelEqualizer((eqims & 1) == 1) + tef.FM_Set_MphSuppression((eqims & 2) == 2) + out += f"G{bin(eqims).removeprefix('0b').zfill(2)}\n" + wsdata["ims"] = (eqims & 1) + wsdata["eq"] = (eqims & 2) >> 1 + elif msg.startswith("B"): + wsdata["stForced"] = bool(int(msg.removeprefix("B").strip(), 2)) + tef.FM_Set_Stereo_Min(2 if wsdata["stForced"] else 0) + out += f"B{int(wsdata["stForced"])}\n" + elif msg.startswith("D"): + deemp = int(msg.removeprefix("D").strip()) + dtime = 500 if deemp == 0 else (750 if deemp == 1 else 0) + tef.FM_Set_Deemphasis(dtime) + out += f"D{deemp}\n" + elif msg.startswith("W"): + bw = int(msg.removeprefix("W").strip()) + auto = (bw == 0) + tef.FM_Set_Bandwidth(auto, 2360 if auto else (bw // 100)) + out += f"W{bw}\n" + if out.strip(): outq.put(out) + except Empty: + if (RDS_TIMEOUT and RDS_TIMEOUT != 0) and rds_dropout_time.get_time() > RDS_TIMEOUT: resetRds() + + if signal_timer.get_time() > 0.1: + signal_timer.reset() + res = None + if fm: res = tef.FM_Get_Quality_Data() + else: res = tef.AM_Get_Quality_Data() + + if res: + if fm: + stereo_pilot, _ = d if (d := tef.FM_Get_Signal_Status()) else (None, None) + else: stereo_pilot = False + _, level, usn, wam, _, bandwidth, *_ = res + + level /= 10 + bandwidth /= 10 + + data = "S" + if wsdata["stForced"]: data += "M" + elif stereo_pilot: data += "s" + else: data += "m" + + wsdata["st"] = stereo_pilot + + level += 11.25 # to dBf from dBμV + + data += f"{level},{wam//10},{usn//10},{bandwidth}" + wsdata["bw"] = str(bandwidth) + wsdata["sigRaw"] = data + wsdata["sig"] = level + wsdata["sigTop"] = wsdata["sigTop"] if level < wsdata["sigTop"] else level + if rds_timer.get_time() > 0.086 and fm: + rds_timer.reset() + + res = tef.FM_Get_RDS_Data__decoder() + if res is None: continue + status, A, B, C, D, dec_error = res + dec_error >>= 8 + + if (status & (1 << 9) == 0) or (status & (1 << 15) == 0): continue + + gdata = "G:\r\n" + a_error = dec_error >> 6 + b_error = (dec_error >> 4) & 0b11 + c_error = (dec_error >> 2) & 0b11 + d_error = dec_error & 0b11 + + wsdata["rds"] = True + pi_error = decodeRds(wsdata, status, A, B, C, D, dec_error) + + def threshold(group: str, error: int): return group if (error < 3) else "----" + + if (status & (1 << 13) == 0): + if (status & (1 << 12) == 0): + # Type A, PI in A + gdata += threshold(f"{A:04X}", a_error) + gdata += threshold(f"{B:04X}", b_error) + gdata += threshold(f"{C:04X}", c_error) + elif (status >> 12) & 1: + # Type B, PI in A and C + pi = wsdata["pi"] + gdata += threshold(pi, pi_error) + gdata += threshold(f"{B:04X}", b_error) + gdata += threshold(pi, pi_error) + gdata += threshold(f"{D:04X}", d_error) + gdata += "\r\n\r\n" + + rds.put(gdata) + rds_dropout_time.reset() + + if data_timer.get_time() > 0.1: + data_timer.reset() + outq.put(json.dumps(wsdata)) + +inq = Queue() +outq = Queue() +rds = Queue() + +wsc: list[websockets.ServerConnection] = [] +rds_wsc: list[websockets.ServerConnection] = [] + +async def worker(): + while True: + try: + msg = await asyncio.to_thread(outq.get, False) + if msg == "!x": break + for websocket in wsc[:]: + try: await websocket.send(msg) + except websockets.exceptions.ConnectionClosed: pass + except Empty: await asyncio.sleep(0.015) +async def rds_worker(): + while True: + try: + msg = await asyncio.to_thread(rds.get, False) + if msg == "!x": break + for websocket in rds_wsc[:]: + try: await websocket.send(msg) + except websockets.exceptions.ConnectionClosed: pass + except Empty: await asyncio.sleep(0.015) + +async def handle_text(websocket: websockets.ServerConnection): + wsc.append(websocket) + await asyncio.to_thread(inq.put, "+") + try: + async for message in websocket: + if message not in ("+", "-"): + print(message) + await asyncio.to_thread(inq.put, message) + await websocket.recv() # For loop exits normally if closed normally. This should raise an ConnectionClosed if the loop exited, because the connection was closed + except websockets.exceptions.ConnectionClosed: + wsc.remove(websocket) + await asyncio.to_thread(inq.put, "-") + print("Client disconnected") + +async def handle_rds(websocket: websockets.ServerConnection): + rds_wsc.append(websocket) + await websocket.wait_closed() + rds_wsc.remove(websocket) + +async def handler(websocket: websockets.ServerConnection): + if not websocket.request: + await websocket.close(1011) + return + print(f"Client connected: {websocket.remote_address} path: {websocket.request.path}") + if websocket.request.path == "/text": await handle_text(websocket) + elif websocket.request.path == "/rds": await handle_rds(websocket) + else: await websocket.close(1013) + +async def handle_http(websocket: websockets.ServerConnection, reqeust: websockets.Request) -> Response | None: + tosend = None + mimetype = "text/text" + match reqeust.path: + case "/static_data": + json_data = { + "qthLatitude": f"{LATITUDE:.6f}", + "qthLongitude": f"{LONGITUDE:.6f}", + "presets": [ + f"{PRESET0:.1f}", + f"{PRESET1:.1f}", + f"{PRESET2:.1f}", + f"{PRESET3:.1f}" + ], + "defaultTheme": f"theme{DEFAULT_THEME}", + "bgImage": BG_IMAGE, + "rdsMode": RBDS, + "rdsTimeout": f"{RDS_TIMEOUT}", + "tunerName": NAME, + "tunerDesc": DESCRIPTION, + "ant": { + "enabled": False, + "ant1": { + "enabled": False, + "name": "Ant H" + }, + "ant2": { + "enabled": False, + "name": "Ant V" + }, + "ant3": { + "enabled": False, + "name": "Ant C" + }, + "ant4": { + "enabled": False, + "name": "Ant D" + } + } + } + tosend = json.dumps(json_data).encode() + mimetype = "application/json" + case "/ping": + tosend = b"pong" + if tosend is not None: + return Response(200, "OK", + Headers([("Content-Type", mimetype), ("Content-Length", f"{len(tosend)}")]), + tosend) + return None + +async def process_requests(websocket: websockets.ServerConnection, request: websockets.Request): + print(request) + if request.path not in ("/text", "/rds"): + handle = await handle_http(websocket, request) + if not handle: + data = b"This path does not have an handler" + return Response( + 404, + "Not Found", + Headers([("Content-Type", "text/text"), ("Content-Length", f"{len(data)}")]), + data + ) + else: return handle + if not "upgrade" in request.headers.get("Connection", "").lower(): + return Response( + 426, + "Upgrade Required", + Headers([("Connection", "Upgrade"), ("Upgrade", "websocket")]), + b"WebSocket upgrade required\n" + ) + +async def main(): + wtask = asyncio.create_task(worker()) + wtask2 = asyncio.create_task(rds_worker()) + async with websockets.serve(handler, "localhost", 8765, process_request=process_requests, server_header="FM-DX-Cliserver"): + print("Server running on ws://localhost:8765") + await wtask + await wtask2 + +def start_process(): + proc = Process(target=serialpart, args=(inq,outq,rds)) + proc.start() + return proc + +if __name__ == "__main__": + # I fucking hate windows, i wish they'd hang gates in a city center like they did mussolini + freeze_support() + proc = start_process() + try: asyncio.run(main()) + except SystemExit: + inq.put("!x") + outq.put("!x") + rds.put("!x") + proc.terminate() + proc.join() + raise \ No newline at end of file diff --git a/cliserver_Countries.py b/cliserver_Countries.py new file mode 100644 index 0000000..739e700 --- /dev/null +++ b/cliserver_Countries.py @@ -0,0 +1,616 @@ +countries = [ + "Albania", + "Estonia", + "Algeria", + "Ethiopia", + "Andorra", + "Angola", + "Finland", + "Armenia", + "France", + "Ascension Island", + "Gabon", + "Austria", + "Gambia", + "Azerbaijan", + "Georgia", + "Germany", + "Bahrein", + "Ghana", + "Belarus", + "Gibraltar", + "Belgium", + "Greece", + "Benin", + "Guinea", + "Bosnia Herzegovina", + "Guinea-Bissau", + "Botswana", + "Hungary", + "Bulgaria", + "Iceland", + "Burkina Faso", + "Iraq", + "Burundi", + "Ireland", + "Cabinda", + "Israel", + "Cameroon", + "Italy", + "Jordan", + "Cape Verde", + "Kazakhstan", + "Central African Republic", + "Kenya", + "Chad", + "Kosovo", + "Comoros", + "Kuwait", + "DR Congo", + "Kyrgyzstan", + "Republic of Congo", + "Latvia", + "Cote d'Ivoire", + "Lebanon", + "Croatia", + "Lesotho", + "Cyprus", + "Liberia", + "Czechia", + "Libya", + "Denmark", + "Liechtenstein", + "Djiboutia", + "Lithuania", + "Egypt", + "Luxembourg", + "Equatorial Guinea", + "Macedonia", + "Eritrea", + "Madagascar", + "Seychelles", + "Malawi", + "Sierra Leone", + "Mali", + "Slovakia", + "Malta", + "Slovenia", + "Mauritania", + "Somalia", + "Mauritius", + "South Africa", + "Moldova", + "South Sudan", + "Monaco", + "Spain", + "Mongolia", + "Sudan", + "Montenegro", + "Swaziland", + "Morocco", + "Sweden", + "Mozambique", + "Switzerland", + "Namibia", + "Syria", + "Netherlands", + "Tajikistan", + "Niger", + "Tanzania", + "Nigeria", + "Togo", + "Norway", + "Tunisia", + "Oman", + "Turkey", + "Palestine", + "Turkmenistan", + "Poland", + "Uganda", + "Portugal", + "Ukraine", + "Qatar", + "United Arab Emirates", + "Romania", + "United Kingdom", + "Russia", + "Uzbekistan", + "Rwanda", + "Vatican", + "San Marino", + "Western Sahara", + "Sao Tome and Principe", + "Yemen", + "Saudi Arabia", + "Zambia", + "Senegal", + "Zimbabwe", + "Serbia", + "Anguilla", + "Guyana", + "Antigua and Barbuda", + "Haiti", + "Argentina", + "Honduras", + "Aruba", + "Jamaica", + "Bahamas", + "Martinique", + "Barbados", + "Mexico", + "Belize", + "Montserrat", + "Brazil/Bermuda", + "Brazil/AN", + "Bolivia", + "Nicaragua", + "Brazil", + "Panama", + "Canada", + "Paraguay", + "Cayman Islands", + "Peru", + "Chile", + "USA/VI/PR", + "Colombia", + "St. Kitts", + "Costa Rica", + "St. Lucia", + "Cuba", + "St. Pierre and Miquelon", + "Dominica", + "St. Vincent", + "Dominican Republic", + "Suriname", + "El Salvador", + "Trinidad and Tobago", + "Turks and Caicos islands", + "Falkland Islands", + "Greenland", + "Uruguay", + "Grenada", + "Venezuela", + "Guadeloupe", + "Virgin Islands", + "Guatemala", + "Afghanistan", + "South Korea", + "Laos", + "Australia Capital Territory", + "Macao", + "Australia New South Wales", + "Malaysia", + "Australia Victoria", + "Maldives", + "Australia Queensland", + "Marshall Islands", + "Australia South Australia", + "Micronesia", + "Australia Western Australia", + "Myanmar", + "Australia Tasmania", + "Nauru", + "Australia Northern Territory", + "Nepal", + "Bangladesh", + "New Zealand", + "Bhutan", + "Pakistan", + "Brunei Darussalam", + "Papua New Guinea", + "Cambodia", + "Philippines", + "China", + "Samoa", + "Singapore", + "Solomon Islands", + "Fiji", + "Sri Lanka", + "Hong Kong", + "Taiwan", + "India", + "Thailand", + "Indonesia", + "Tonga", + "Iran", + "Vanuatu", + "Japan", + "Vietnam", + "Kiribati", + "North Korea", + "Brazil/Equator" +] + +iso = [ + "AL", + "EE", + "DZ", + "ET", + "AD", + "AO", + "FI", + "AM", + "FR", + "SH", + "GA", + "AT", + "GM", + "AZ", + "GE", + "DE", + "BH", + "GH", + "BY", + "GI", + "BE", + "GR", + "BJ", + "GN", + "BA", + "GW", + "BW", + "HU", + "BG", + "IS", + "BF", + "IQ", + "BI", + "IE", + "--", + "IL", + "CM", + "IT", + "JO", + "CV", + "KZ", + "CF", + "KE", + "TD", + "XK", + "KM", + "KW", + "CD", + "KG", + "CG", + "LV", + "CI", + "LB", + "HR", + "LS", + "CY", + "LR", + "CZ", + "LY", + "DK", + "LI", + "DJ", + "LT", + "EG", + "LU", + "GQ", + "MK", + "ER", + "MG", + "SC", + "MW", + "SL", + "ML", + "SK", + "MT", + "SI", + "MR", + "SO", + "MU", + "ZA", + "MD", + "SS", + "MC", + "ES", + "MN", + "SD", + "ME", + "SZ", + "MA", + "SE", + "MZ", + "CH", + "NA", + "SY", + "NL", + "TJ", + "NE", + "TZ", + "NG", + "TG", + "NO", + "TN", + "OM", + "TR", + "PS", + "TM", + "PL", + "UG", + "PT", + "UA", + "QA", + "AE", + "RO", + "GB", + "RU", + "UZ", + "RW", + "VA", + "SM", + "EH", + "ST", + "YE", + "SA", + "ZM", + "SN", + "ZW", + "RS", + "AI", + "GY", + "AG", + "HT", + "AR", + "HN", + "AW", + "JM", + "BS", + "MQ", + "BB", + "MX", + "BZ", + "MS", + "--", + "--", + "BO", + "NI", + "BR", + "PA", + "CA", + "PY", + "KY", + "PE", + "CL", + "--", + "CO", + "KN", + "CR", + "LC", + "CU", + "PM", + "DM", + "VC", + "DO", + "SR", + "SN", + "TT", + "TB", + "FK", + "GL", + "UY", + "GD", + "VE", + "GP", + "VG", + "GT", + "AF", + "KR", + "LA", + "AU", + "MO", + "AU", + "MY", + "AU", + "MV", + "AU", + "MH", + "AU", + "FM", + "AU", + "MM", + "AU", + "NR", + "AU", + "NP", + "BD", + "NZ", + "BT", + "PK", + "BN", + "PG", + "KH", + "PH", + "CN", + "WS", + "SG", + "SB", + "FJ", + "LK", + "HK", + "TW", + "IN", + "TH", + "ID", + "TO", + "IR", + "VU", + "JP", + "VN", + "KI", + "KP", + "--" +] + +rdsEccA0A6Lut = [ + # A0 + [ + "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", + "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", + "USA/VI/PR", None, "USA/VI/PR", "USA/VI/PR", None + ], + # A1 + [ + None, None, None, None, None, + None, None, None, None, None, + "Canada", "Canada", "Canada", "Canada", "Greenland" + ], + # A2 + [ + "Anguilla", "Antigua and Barbuda", "Brazil/Equator", "Falkland Islands", "Barbados", + "Belize", "Cayman Islands", "Costa Rica", "Cuba", "Argentina", + "Brazil", "Brazil/Bermuda", "Brazil/AN", "Guadeloupe", "Bahamas" + ], + # A3 + [ + "Bolivia", "Colombia", "Jamaica", "Martinique", None, + "Paraguay", "Nicaragua", None, "Panama", "Dominica", + "Dominican Republic", "Chile", "Grenada", "Turks and Caicos islands", "Guyana" + ], + # A4 + [ + "Guatemala", "Honduras", "Aruba", None, "Montserrat", + "Trinidad and Tobago", "Peru", "Suriname", "Uruguay", "St. Kitts", + "St. Lucia", "El Salvador", "Haiti", "Venezuela", "Virgin Islands" + ], + # A5 + [ + None, None, None, None, None, + None, None, None, None, None, + "Mexico", "St. Vincent", "Mexico", "Mexico", "Mexico" + ], + # A6 + [ + None, None, None, None, None, + None, None, None, None, None, + None, None, None, None, "St. Pierre and Miquelon" + ] +] + +rdsEccD0D4Lut = [ + # D0 + [ + "Cameroon", "Central African Republic", "Djiboutia", "Madagascar", "Mali", + "Angola", "Equatorial Guinea", "Gabon", "Guinea", "South Africa", + "Burkina Faso", "Republic of Congo", "Togo", "Benin", "Malawi" + ], + # D1 + [ + "Namibia", "Liberia", "Ghana", "Mauritania", "Sao Tome and Principe", + "Cape Verde", "Senegal", "Gambia", "Burundi", "Ascension Island", + "Botswana", "Comoros", "Tanzania", "Ethiopia", "Nigeria" + ], + # D2 + [ + "Sierra Leone", "Zimbabwe", "Mozambique", "Uganda", "Swaziland", + "Kenya", "Somalia", "Niger", "Chad", "Guinea-Bissau", + "DR Congo", "Cote d'Ivoire", None, "Zambia", "Eritrea" + ], + # D3 + [ + None, None, "Western Sahara", "Cabinda", "Rwanda", + "Lesotho", None, "Seychelles", None, "Mauritius", + None, "Sudan", None, None, None + ], + # D4 + [ + None, None, None, None, None, + None, None, None, None, "South Sudan", + None, None, None, None, None + ] +] + +rdsEccE0E5Lut = [ + # E0 + [ + "Germany", "Algeria", "Andorra", "Israel", "Italy", + "Belgium", "Russia", "Palestine", "Albania", "Austria", + "Hungary", "Malta", "Germany", None, "Egypt" + ], + # E1 + [ + "Greece", "Cyprus", "San Marino", "Switzerland", "Jordan", + "Finland", "Luxembourg", "Bulgaria", "Denmark", "Gibraltar", + "Iraq", "United Kingdom", "Libya", "Romania", "France" + ], + # E2 + [ + "Morocco", "Czechia", "Poland", "Vatican", "Slovakia", + "Syria", "Tunisia", None, "Liechtenstein", "Iceland", + "Monaco", "Lithuania", "Serbia", "Spain", "Norway" + ], + # E3 + [ + "Montenegro", "Ireland", "Turkey", None, "Tajikistan", + None, None, "Netherlands", "Latvia", "Lebanon", + "Azerbaijan", "Croatia", "Kazakhstan", "Sweden", "Belarus" + ], + # E4 + [ + "Moldova", "Estonia", "Macedonia", None, None, + "Ukraine", "Kosovo", "Portugal", "Slovenia", "Armenia", + "Uzbekistan", "Georgia", None, "Turkmenistan", "Bosnia Herzegovina" + ], + # E5 + [ + None, None, "Kyrgyzstan", None, None, + None, None, None, None, None, + None, None, None, None, None + ] +] + +rdsEccF0F4Lut = [ + # F0 + [ + "Australia Capital Territory", "Australia New South Wales", "Australia Victoria", "Australia Queensland", "Australia South Australia", + "Australia Western Australia", "Australia Tasmania", "Australia Northern Territory", "Saudi Arabia", "Afghanistan", + "Myanmar", "China", "North Korea", "Bahrein", "Malaysia" + ], + # F1 + [ + "Kiribati", "Bhutan", "Bangladesh", "Pakistan", "Fiji", + "Oman", "Nauru", "Iran", "New Zealand", "Solomon Islands", + "Brunei Darussalam", "Sri Lanka", "Taiwan", "South Korea", "Hong Kong" + ], + # F2 + [ + "Kuwait", "Qatar", "Cambodia", "Samoa", "India", + "Macao", "Vietnam", "Philippines", "Japan", "Singapore", + "Maldives", "Indonesia", "United Arab Emirates", "Nepal", "Vanuatu" + ], + # F3 + [ + "Laos", "Thailand", "Tonga", None, None, + None, None, "China", "Papua New Guinea", None, + "Yemen", None, None, "Micronesia", "Mongolia" + ], + # F4 + [ + None, None, None, None, None, + None, None, None, "China", None, + "Marshall Islands", None, None, None, None + ] +] + +def rds_ecc_lookup(pi, ecc): + PI_UNKNOWN = -1 + pi_country = (pi >> 12) & 0xF + if pi == PI_UNKNOWN or pi_country == 0: + return "" + + pi_id = pi_country - 1 + ecc_ranges = [ + {"min": 0xA0, "max": 0xA6, "lut": rdsEccA0A6Lut}, + {"min": 0xD0, "max": 0xD4, "lut": rdsEccD0D4Lut}, + {"min": 0xE0, "max": 0xE5, "lut": rdsEccE0E5Lut}, + {"min": 0xF0, "max": 0xF4, "lut": rdsEccF0F4Lut}, + ] + + for range_ in ecc_ranges: + if range_["min"] <= ecc <= range_["max"]: + ecc_id = ecc - range_["min"] + return range_["lut"][ecc_id][pi_id] + + return "" \ No newline at end of file diff --git a/cliserver_rdsDecoder.py b/cliserver_rdsDecoder.py new file mode 100644 index 0000000..6b2ffc7 --- /dev/null +++ b/cliserver_rdsDecoder.py @@ -0,0 +1,100 @@ +import struct +import librds.charset +from librds.comfort import SubstituteCharacterAtPosition +from cliserver_Countries import rds_ecc_lookup, iso, countries + +def decodeRds(wsdata: dict, status: int, A: int, B: int, C: int, D: int, dec_error: int): + a_error = dec_error >> 6 + b_error = (dec_error >> 4) & 0b11 + c_error = (dec_error >> 2) & 0b11 + d_error = dec_error & 0b11 + + pi = A + pi_error = a_error + if (status >> 12) & 1 and c_error < a_error: + pi = C + pi_error = c_error + + if pi_error < 3: wsdata["pi"] = f"{pi:04X}" + ("?" * pi_error) + + if b_error > 3: return pi_error + + group = B >> 11 + print(group) + wsdata["tp"] = (B >> 10) & 1 + wsdata["pty"] = (B >> 5) & 0b11111 + + match group: + case 0 | 1: + # 0A or 0B + ps_errors: list[str] = wsdata["ps_errors"].split(",") + + wsdata["ta"] = (B >> 4) & 1 + wsdata["ms"] = (B >> 3) & 1 + + if d_error > 2: return pi_error + segment = B & 0b11 + + old_chars = wsdata["ps"][2*segment:(2*segment)+2] + chars = "".join([librds.charset.RDSCharsetDecode.translate(i) for i in struct.pack(">H", D)]) + + err = int(d_error * (10/3)) + if (old_chars == chars and int(ps_errors[2*segment]) > err) or old_chars != chars: + for offset, char in enumerate(chars): + wsdata["ps"] = SubstituteCharacterAtPosition(wsdata["ps"], char, (2 * segment) + offset) + ps_errors[2*segment] = str(err) + ps_errors[2*segment+1] = str(err) + wsdata["ps_errors"] = ",".join(ps_errors) + # TODO: AF + case 2: + # 1A + if c_error > 2: return pi_error + + variant_code = (C >> 12) & 0b111 + match variant_code: + case 0: + # ECC + wsdata["ecc"] = f"{(C & 0xff):02X}" + wsdata["country_name"] = rds_ecc_lookup(pi, C & 0xff) + wsdata["country_iso"] = "UN" if wsdata["country_name"] == 0 else ( + iso[countries.index(wsdata["country_name"])] + ) + case 4 | 5: + # 2A or 2B + segment = B & 0b1111 + segment_pointer = (4 if group == 4 else 2) * segment + d_offset = 2 if group == 4 else 0 + ab = bool((B >> 4) & 1) + + if wsdata["rt_flag"] != int(ab): + wsdata["rt1" if ab else "rt0"] = "".ljust(64) + wsdata["rt1_errors" if ab else "rt0_errors"] = ",".join((["0"]*64)) + wsdata["rt_flag"] = int(ab) + + chars_c = "".join([librds.charset.RDSCharsetDecode.translate(i) for i in struct.pack(">H", C)]).replace("\r", "") + chars_d = "".join([librds.charset.RDSCharsetDecode.translate(i) for i in struct.pack(">H", D)]).replace("\r", "") + + rt_errors: list[str] = wsdata["rt1_errors" if ab else "rt0_errors"].split(",") + old_rt: str = wsdata["rt1" if ab else "rt0"] + + old_c = old_rt[segment_pointer:segment_pointer+2] + old_c_error = int(rt_errors[segment_pointer]) + _c_error = int(c_error * (10/3)) + old_d = old_rt[segment_pointer+d_offset:segment_pointer+d_offset+2] + old_d_error = int(rt_errors[segment_pointer+d_offset]) + _d_error = int(d_error * (10/3)) + + if group == 4 and _c_error < 3 and (chars_c != old_c or _c_error < old_c_error): + for offset, char in enumerate(chars_c): + old_rt = SubstituteCharacterAtPosition(old_rt, char, segment_pointer + offset) + rt_errors[segment_pointer] = str(_c_error) + rt_errors[segment_pointer+1] = str(_c_error) + if _d_error < 3 and (chars_d != old_d or _d_error < old_d_error): + for offset, char in enumerate(chars_d): + old_rt = SubstituteCharacterAtPosition(old_rt, char, segment_pointer + offset + d_offset) + rt_errors[segment_pointer+d_offset] = str(_d_error) + rt_errors[segment_pointer+d_offset+1] = str(_d_error) + wsdata["rt1_errors" if ab else "rt0_errors"] = ",".join(rt_errors) + wsdata["rt1" if ab else "rt0"] = old_rt + + return pi_error \ No newline at end of file diff --git a/base_tef.py b/driver/base_tef.py similarity index 96% rename from base_tef.py rename to driver/base_tef.py index 35a29e1..eb2fdc3 100644 --- a/base_tef.py +++ b/driver/base_tef.py @@ -1,6 +1,6 @@ import struct -from protocol import I2CPCClient, time -from patches import * +from driver.protocol import I2CPCClient, time +from driver.patches import * from typing import TypeVar, ParamSpec diff --git a/patches/__init__.py b/driver/patches/__init__.py similarity index 100% rename from patches/__init__.py rename to driver/patches/__init__.py diff --git a/patches/tef_102_patch.py b/driver/patches/tef_102_patch.py similarity index 100% rename from patches/tef_102_patch.py rename to driver/patches/tef_102_patch.py diff --git a/patches/tef_205_patch.py b/driver/patches/tef_205_patch.py similarity index 100% rename from patches/tef_205_patch.py rename to driver/patches/tef_205_patch.py diff --git a/protocol.py b/driver/protocol.py similarity index 95% rename from protocol.py rename to driver/protocol.py index 2ea85fc..e2768d7 100644 --- a/protocol.py +++ b/driver/protocol.py @@ -27,12 +27,12 @@ class I2CPCClient: start = time.monotonic() while not ((d := self.ser.read_all()) and b"\xff" in d): time.sleep(0.01) - if ((time.monotonic()) - start) > 2: return False + if ((time.monotonic()) - start) > 3: return False return True if not wake(None): for baud in wake_bauds: if wake(baud): - warn(f"Initial baud was changed to {baud}, due to the device being in it") + if baud != baudrate: warn(f"Initial baud was changed to {baud}, due to the device being in it") break else: raise ConnectionError("Could not wake") diff --git a/tef.py b/driver/tef.py similarity index 99% rename from tef.py rename to driver/tef.py index a0fe6a6..127e278 100644 --- a/tef.py +++ b/driver/tef.py @@ -1,5 +1,5 @@ from __future__ import annotations -from base_tef import BaseTEF668X +from driver.base_tef import BaseTEF668X from typing import overload, ParamSpec, TypeVar, Concatenate, Callable from functools import wraps import struct diff --git a/rtc.py b/rtc.py index e7764ef..3f8f25f 100644 --- a/rtc.py +++ b/rtc.py @@ -1,5 +1,5 @@ import datetime -from protocol import I2CPCClient +from driver.protocol import I2CPCClient def sync_rtc(p: I2CPCClient): TIMEOFFSET_ADDR = 2275 diff --git a/test.py b/test.py index 084701b..dfe0cac 100644 --- a/test.py +++ b/test.py @@ -1,15 +1,17 @@ -from tef import TEF6686 -from protocol import I2CPCClient +from driver.tef import TEF6686 +from driver.protocol import I2CPCClient import time p = I2CPCClient("COM17") +print(p.version()) with TEF6686(p) as tef: - tef.init(clock=12000000) + tef.init() tef.FM_Tune_To(TEF6686.TuneTo_Mode.Preset, 9500) tef.AUDIO_Set_Mute(False) tef.AUDIO_Set_Volume(70) tef.FM_Set_MphSuppression(True) tef.FM_Set_ChannelEqualizer(True) + tef.FM_Set_Stereo_Max(False) # tef.FM_Set_Specials(1) tef.FM_Set_Bandwidth(True) time.sleep(0.032) diff --git a/xrd.py b/xrd.py index edc4082..2f6a686 100644 --- a/xrd.py +++ b/xrd.py @@ -8,8 +8,8 @@ import secrets import string import libtimer import time -from tef import TEF6686 -from protocol import I2CPCClient +from driver.tef import TEF6686 +from driver.protocol import I2CPCClient import os from functools import wraps from typing import Callable @@ -193,22 +193,20 @@ def send_rds_data(tef: TEF6686, conn: socket.socket, state: dict): status, A, B, C, D, dec_error = res dec_error >>= 8 - if (status & (1 << 9) == 0) or (status & (1 << 15) == 0): return + if (status & (1 << 9) == 0) or (status & (1 << 15) == 0) or (status & (1 << 13) == (1 << 13)): return data = b"" a_error = dec_error >> 6 c_error = (dec_error >> 2) & 0b11 - if (status & (1 << 13) == 0): - if (status & (1 << 12) == 0): - # Type A, PI in A - data = f"R{A:04X}{B:04X}{C:04X}".encode() - elif (status >> 12) & 1: - # Type B, PI in A and C - pi = A - if c_error < a_error: pi = C - data = f"R{pi:04X}{B:04X}{pi:04X}".encode() - data += f"{D:04X}{dec_error:02X}\n".encode() - else: data = f"P{A:04X}{"?" * a_error}\n".encode() # No group data, only PI + if (status & (1 << 12) == 0): + # Type A, PI in A + data = f"R{A:04X}{B:04X}{C:04X}".encode() + elif (status >> 12) & 1: + # Type B, PI in A and C + pi = A + if c_error < a_error: pi = C + data = f"R{pi:04X}{B:04X}{pi:04X}".encode() + data += f"{D:04X}{dec_error:02X}\n".encode() if not data.strip(): return conn.sendall(data)