#!/usr/bin/env python3 """Probe — does the G1 audio multicast actually deliver packets? Run on the Jetson: /home/unitree/miniconda3/envs/gemini_sdk/bin/python ~/Marcus/Voice/_probe_multicast.py This is a STRIPPED-DOWN version of Voice/audio_io.py::BuiltinMic.start() plus a 5-second packet counter. Tells us in plain numbers whether the G1 audio service is publishing on 239.168.123.161:5555 — independent of any Marcus state. Prints: interfaces with 192.168.123.x: [list] chosen local_ip: x.x.x.x joined multicast 239.168.123.161:5555 ✓ packets received in 5s: N total bytes received: M first packet bytes (hex preview): ... If packets=0 → G1 audio service isn't publishing OR Jetson can't reach the multicast group. Either way Marcus's voice can never work until THIS probe shows packets > 0. """ import socket import struct import subprocess import sys import time GROUP = "239.168.123.161" PORT = 5555 def find_g1_ips() -> list: """Return all local IPs in 192.168.123.0/24 (the G1 internal subnet).""" out = subprocess.run( ["ip", "-4", "-o", "addr"], capture_output=True, text=True, ).stdout found = [] for line in out.splitlines(): for tok in line.split(): if tok.startswith("192.168.123."): found.append(tok.split("/")[0]) return found def main() -> int: ips = find_g1_ips() print(f" interfaces with 192.168.123.x: {ips or '(none — Jetson NOT on G1 subnet)'}") if not ips: print(" ✗ Cannot proceed — no 192.168.123.x interface on this host.") print(" The G1 audio multicast only delivers to that subnet.") print(" Check `ip addr show` and reconnect to the G1's network.") return 1 local_ip = ips[0] print(f" chosen local_ip: {local_ip}") sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind(("", PORT)) except Exception as e: print(f" ✗ bind on UDP port {PORT} failed: {e}") return 2 mreq = struct.pack( "4s4s", socket.inet_aton(GROUP), socket.inet_aton(local_ip), ) try: sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) except Exception as e: print(f" ✗ multicast join failed: {e}") return 3 sock.settimeout(0.5) print(f" joined multicast {GROUP}:{PORT} ✓") print(f" listening for 5 seconds...") deadline = time.time() + 5.0 pkt_count = 0 byte_count = 0 first_pkt = None while time.time() < deadline: try: data, addr = sock.recvfrom(4096) except socket.timeout: continue except Exception as e: print(f" ✗ recvfrom error: {e}") break pkt_count += 1 byte_count += len(data) if first_pkt is None: first_pkt = data sock.close() print(f" packets received in 5s: {pkt_count}") print(f" total bytes received: {byte_count}") if first_pkt is not None: preview = first_pkt[:24].hex(" ") print(f" first packet bytes (hex preview): {preview} ({len(first_pkt)} bytes total)") if pkt_count == 0: print() print(" ✗ ZERO packets received. Diagnosis:") print(" - G1's audio service is not publishing, OR") print(" - This Jetson is on the right subnet but a firewall or") print(" switch is blocking multicast (rare on G1's setup).") print(" Fix: restart the G1 audio service or reboot the robot.") return 4 print() print(f" ✓ Multicast IS delivering ({pkt_count} packets / {byte_count} bytes in 5s).") print(f" Expected for 16 kHz mono int16: ~{int(5*16000*2/byte_count*pkt_count)} packets/s if continuous") print(f" If audio is non-zero too → the issue is somewhere in the runner; not the network.") return 0 if __name__ == "__main__": sys.exit(main())