Marcus/Voice/_probe_multicast.py

124 lines
4.0 KiB
Python

#!/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())