|
1 | 1 | import asyncio |
| 2 | +import contextlib |
2 | 3 | import socket |
3 | 4 | import struct |
4 | 5 | import time |
|
14 | 15 | from src.config import ( |
15 | 16 | REMOTE_IP, UDP_PORT, TCP_PORT, |
16 | 17 | REDIS_URL, REDIS_CAN_CHANNEL, REDIS_UPLINK_CHANNEL, ENABLE_UPLINK, |
17 | | - REDIS_WS_CLIENTS_KEY, |
| 18 | + REDIS_WS_CLIENTS_KEY, REDIS_HEARTBEAT_CHANNEL, |
18 | 19 | ) |
19 | 20 | from src import redis_utils, utils |
| 21 | +from src.heartbeat import pump_pubsub_with_heartbeat, run_heartbeat_writer |
20 | 22 | from src.version import get_git_hash |
21 | 23 |
|
22 | 24 | BATCH_SIZE = 20 |
@@ -863,51 +865,63 @@ async def uplink_relay(): |
863 | 865 | uplink_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
864 | 866 | uplink_seq = 0 |
865 | 867 |
|
866 | | - try: |
867 | | - r = aioredis.from_url(REDIS_URL) |
868 | | - pubsub = r.pubsub() |
869 | | - await pubsub.subscribe(REDIS_UPLINK_CHANNEL) |
870 | | - logger.info(f"Subscribed to Redis channel: {REDIS_UPLINK_CHANNEL}") |
871 | | - |
872 | | - async for message in pubsub.listen(): |
873 | | - if message['type'] != 'message': |
874 | | - continue |
| 868 | + async def _relay(msg): |
| 869 | + nonlocal uplink_seq |
| 870 | + if msg['type'] != 'message': |
| 871 | + return |
| 872 | + try: |
| 873 | + data = redis_utils.decode_message(msg['data']) |
| 874 | + uplink_msg = json.loads(data) |
875 | 875 |
|
876 | | - try: |
877 | | - data = redis_utils.decode_message(message['data']) |
878 | | - uplink_msg = json.loads(data) |
| 876 | + can_id = uplink_msg.get("canId") |
| 877 | + can_data = uplink_msg.get("data", []) |
| 878 | + ref = uplink_msg.get("ref", "unknown") |
879 | 879 |
|
880 | | - can_id = uplink_msg.get("canId") |
881 | | - can_data = uplink_msg.get("data", []) |
882 | | - ref = uplink_msg.get("ref", "unknown") |
| 880 | + if can_id is None or not isinstance(can_id, int) or can_id < 0: |
| 881 | + logger.warning(f"Uplink relay: invalid canId in ref={ref}") |
| 882 | + return |
| 883 | + if not isinstance(can_data, list) or len(can_data) < 1 or len(can_data) > 8: |
| 884 | + logger.warning(f"Uplink relay: invalid data in ref={ref}") |
| 885 | + return |
883 | 886 |
|
884 | | - if can_id is None or not isinstance(can_id, int) or can_id < 0: |
885 | | - logger.warning(f"Uplink relay: invalid canId in ref={ref}") |
886 | | - continue |
887 | | - if not isinstance(can_data, list) or len(can_data) < 1 or len(can_data) > 8: |
888 | | - logger.warning(f"Uplink relay: invalid data in ref={ref}") |
889 | | - continue |
| 887 | + # Pack as uplink UDP packet: 0xCAFE + seq + count(1) + CAN message |
| 888 | + uplink_seq += 1 |
| 889 | + data_bytes = bytes(can_data) + b'\x00' * (8 - len(can_data)) |
| 890 | + can_msg = CANMessage(time.time(), can_id, data_bytes) |
890 | 891 |
|
891 | | - # Pack as uplink UDP packet: 0xCAFE + seq + count(1) + CAN message |
892 | | - uplink_seq += 1 |
893 | | - data_bytes = bytes(can_data) + b'\x00' * (8 - len(can_data)) |
894 | | - can_msg = CANMessage(time.time(), can_id, data_bytes) |
| 892 | + payload = UPLINK_MAGIC |
| 893 | + payload += struct.pack("!QH", uplink_seq, 1) |
| 894 | + payload += can_msg.pack() |
895 | 895 |
|
896 | | - payload = UPLINK_MAGIC |
897 | | - payload += struct.pack("!QH", uplink_seq, 1) |
898 | | - payload += can_msg.pack() |
| 896 | + try: |
| 897 | + uplink_sock.sendto(payload, (REMOTE_IP, UDP_PORT)) |
| 898 | + logger.info(f"Uplink relayed to car: canId={can_id} ref={ref} seq={uplink_seq}") |
| 899 | + except (PermissionError, OSError) as e: |
| 900 | + logger.error(f"Uplink UDP send failed: {e}") |
899 | 901 |
|
900 | | - try: |
901 | | - uplink_sock.sendto(payload, (REMOTE_IP, UDP_PORT)) |
902 | | - logger.info(f"Uplink relayed to car: canId={can_id} ref={ref} seq={uplink_seq}") |
903 | | - except (PermissionError, OSError) as e: |
904 | | - logger.error(f"Uplink UDP send failed: {e}") |
| 902 | + except Exception as e: |
| 903 | + logger.error(f"Uplink relay error: {e}") |
905 | 904 |
|
| 905 | + # Reconnect loop: the pump returns whenever the pubsub connection |
| 906 | + # goes silent past HEARTBEAT_STALE_S, and we re-subscribe here. |
| 907 | + try: |
| 908 | + while True: |
| 909 | + r = None |
| 910 | + try: |
| 911 | + r = aioredis.from_url(REDIS_URL) |
| 912 | + pubsub = r.pubsub() |
| 913 | + await pubsub.subscribe(REDIS_UPLINK_CHANNEL, REDIS_HEARTBEAT_CHANNEL) |
| 914 | + logger.info(f"Subscribed to Redis channels: {REDIS_UPLINK_CHANNEL}, {REDIS_HEARTBEAT_CHANNEL}") |
| 915 | + await pump_pubsub_with_heartbeat(pubsub, _relay, log=logger) |
| 916 | + except asyncio.CancelledError: |
| 917 | + raise |
906 | 918 | except Exception as e: |
907 | | - logger.error(f"Uplink relay error: {e}") |
908 | | - |
909 | | - except Exception as e: |
910 | | - logger.error(f"Uplink relay Redis error: {e}") |
| 919 | + logger.error(f"Uplink relay Redis error: {e}") |
| 920 | + await asyncio.sleep(1.0) |
| 921 | + finally: |
| 922 | + if r is not None: |
| 923 | + with contextlib.suppress(Exception): |
| 924 | + await r.aclose() |
911 | 925 | finally: |
912 | 926 | uplink_sock.close() |
913 | 927 |
|
@@ -983,7 +997,11 @@ async def version_checker(): |
983 | 997 | logger.debug(f"Version check error: {e}") |
984 | 998 | await asyncio.sleep(30.0) |
985 | 999 |
|
986 | | - tasks = [udp_receiver(), missing_reporter(), stats_publisher(), raw_csv_logger(), car_time_injector(), version_checker(), utils.heartbeat_coro(self.telemetry_event)] |
| 1000 | + # Base mode has a real Redis server; create an async client for the |
| 1001 | + # heartbeat writer. The writer publishes on the heartbeat channel every |
| 1002 | + # 1s so pubsub subscribers can detect a dead subscription and reconnect. |
| 1003 | + _async_redis = aioredis.from_url(REDIS_URL) |
| 1004 | + tasks = [udp_receiver(), missing_reporter(), stats_publisher(), raw_csv_logger(), car_time_injector(), version_checker(), utils.heartbeat_coro(self.telemetry_event), run_heartbeat_writer(_async_redis)] |
987 | 1005 | if ENABLE_UPLINK: |
988 | 1006 | tasks.append(uplink_relay()) |
989 | 1007 | await asyncio.gather(*tasks) |
|
0 commit comments