onju-v2/serial_monitor.py
justLV c3514ceb49 Add Opus compression for speaker audio
Implements Opus decoding on ESP32 for TTS playback, achieving 14-16x
compression over raw PCM. This improves WiFi throughput margin from 2.2x
to 30x+, enabling reliable operation throughout the home even with poor
WiFi conditions.

Key changes:
- Add Opus decoder to ESP32 firmware with dedicated 32KB FreeRTOS task
- Implement length-prefixed TCP framing for variable-bitrate Opus frames
- Update header protocol: header[5] = compression type (0=PCM, 1=μ-law, 2=Opus)
- Auto-detect USB port in flash and serial monitor scripts
- Add test script with opuslib encoder supporting WAV/M4A/MP3 input
- Document architecture and design rationale for μ-law/UDP (mic) vs Opus/TCP (speaker)

Performance:
- Compression: 640 bytes PCM → 35-50 bytes Opus per 20ms frame (14-16x)
- Bandwidth: 256 kbps → 16 kbps (94% reduction)
- WiFi margin: 2.2x → 30x+ throughput safety margin
- CPU usage: ~10-20% during playback on ESP32-S3
- Quality: High-fidelity voice suitable for human listening

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-01-31 17:41:16 -08:00

137 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Interactive serial monitor for ESP32
- Auto-reconnects on disconnect
- Sends keyboard input to device
- Type 'r' to reset ESP32
- Press Ctrl+C to exit
"""
import serial
import sys
import time
import select
import termios
import tty
import glob
def find_usb_port():
"""Auto-detect USB serial port"""
# Look for ESP32 USB ports
ports = glob.glob('/dev/cu.usbmodem*')
if ports:
return sorted(ports)[0] # Return first match
return None
def connect_serial(port, baud=115200, timeout=1):
"""Attempt to connect to serial port"""
try:
ser = serial.Serial(port, baud, timeout=timeout)
time.sleep(0.1)
return ser
except Exception as e:
print(f" Error: {e}", flush=True)
return None
def main():
# Auto-detect port if not specified
if len(sys.argv) > 1:
port = sys.argv[1]
else:
port = find_usb_port()
if not port:
print("Error: No USB serial port found (looking for /dev/cu.usbmodem*)")
print("Usage: serial_monitor.py [port]")
sys.exit(1)
baud = 115200
print(f"Serial Monitor - {port} @ {baud} baud")
print("Commands: 'r' = reset, 'M' = enable mic, 'A' = send multicast, Ctrl+C = exit")
print("=" * 60)
# Set terminal to raw mode for immediate key input
old_settings = None
if sys.platform != 'win32':
try:
old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
except Exception as e:
print(f"Warning: Could not set terminal to raw mode: {e}")
print("Continuing without raw mode (press Enter after each command)")
ser = None
while True:
# Connect/reconnect
if ser is None or not ser.is_open:
if ser is not None:
try:
ser.close()
except:
pass
print(f"\nConnecting to {port}...", end='', flush=True)
ser = connect_serial(port, baud)
if ser is None:
print(" Failed. Retrying in 2s...")
time.sleep(2)
continue
else:
print(" Connected!")
try:
# Check for incoming serial data
if ser.in_waiting > 0:
line = ser.readline().decode('utf-8', errors='ignore').rstrip()
if line:
print(line)
# Check for keyboard input (non-blocking on Unix)
if sys.platform != 'win32':
if select.select([sys.stdin], [], [], 0)[0]:
key = sys.stdin.read(1)
ser.write(key.encode())
if key == 'r':
print("\n[Sent reset command]")
time.sleep(0.5) # Give time for reset before reconnect
elif key == 'M':
print("\n[Sent mic enable command]")
time.sleep(0.01)
except serial.SerialException as e:
print(f"\n[Disconnected: {e}]")
try:
ser.close()
except:
pass
ser = None
time.sleep(1)
except KeyboardInterrupt:
print("\n\nExiting...")
if ser and ser.is_open:
ser.close()
if old_settings:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
sys.exit(0)
except OSError as e:
# Device not configured - port disappeared
if ser:
try:
ser.close()
except:
pass
ser = None
time.sleep(1)
except Exception as e:
print(f"\n[Error: {e}]")
try:
ser.close()
except:
pass
ser = None
time.sleep(1)
if __name__ == '__main__':
main()