Fix socket read stream crashes and NameError on boot

This commit is contained in:
Adolfo Reyna
2026-06-18 11:35:06 -04:00
parent 5968b4934d
commit d0fcc5fc7b
4 changed files with 208 additions and 110 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ class ES7210:
self.i2c.writeto_mem(self.ADDR, reg, bytes([val]))
def record_audio(duration_seconds=5, filename='recording.pcm'):
def record_audio(duration_seconds=10, filename='recording.pcm'):
"""Records raw stereo PCM data from the dual microphones to a file.
Args:
+201 -105
View File
@@ -56,6 +56,99 @@ def create_wav_header(data_size):
audio_format, channels, sample_rate, byte_rate,
block_align, bits_per_sample, data_label, data_size)
def socket_readline(s):
line = bytearray()
while True:
try:
char = s.recv(1)
except OSError:
break
if not char:
break
line.extend(char)
if char == b'\n':
break
return line
def socket_read_exactly(s, n):
res = bytearray()
while len(res) < n:
try:
chunk = s.recv(n - len(res))
except OSError:
break
if not chunk:
break
res.extend(chunk)
return res
def stream_hermes_request(url, headers, filename):
# Parse URL
proto, _, host_port_path = url.split('/', 2)
host_port = host_port_path.split('/', 1)[0]
path = '/' + host_port_path.split('/', 1)[1] if '/' in host_port_path else '/'
if ':' in host_port:
host, port = host_port.split(':')
port = int(port)
else:
host, port = host_port, 80
import socket
addr = socket.getaddrinfo(host, port)[0][-1]
s = socket.socket()
s.settimeout(30.0)
s.connect(addr)
# Calculate file size
import os
try:
file_size = os.stat(filename)[6]
except OSError:
file_size = 0
content_length = file_size + 44 # WAV header + PCM
# Send request headers
s.write(f"POST {path} HTTP/1.1\r\n".encode())
s.write(f"Host: {host_port}\r\n".encode())
for k, v in headers.items():
s.write(f"{k}: {v}\r\n".encode())
s.write(f"Content-Length: {content_length}\r\n".encode())
s.write(b"\r\n")
# Write WAV header
s.write(create_wav_header(file_size))
# Stream audio file from flash
if file_size > 0:
buf = bytearray(2048)
with open(filename, "rb") as f:
while True:
n = f.readinto(buf)
if n == 0:
break
s.write(buf[:n])
# Read status line
status_line = socket_readline(s).decode()
parts = status_line.split(' ')
status_code = int(parts[1]) if len(parts) >= 2 else 500
# Read headers
resp_headers = {}
while True:
line = socket_readline(s)
if line == b"\r\n" or not line:
break
p = line.decode().split(':', 1)
if len(p) == 2:
resp_headers[p[0].strip().lower()] = p[1].strip()
return status_code, resp_headers, s
# LED mode options for manual cycling
led_modes = [
("Red (Breathing)", lambda led: led.set_color(40, 0, 0), "breath"),
@@ -244,29 +337,10 @@ def main():
draw_status_bar("PTT Voice: Initializing...")
# State 1: Wait for user to release the initial trigger press
draw_status_bar("Release button/screen...")
release_start = time.ticks_ms()
while is_talk_trigger_active():
if time.ticks_diff(time.ticks_ms(), release_start) > 2000:
print("Release timeout occurred")
break
time.sleep_ms(30)
# State 2: Wait for tap-to-start recording
draw_status_bar("Ready. Tap key/screen to record...")
tap_started = False
start_wait = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start_wait) < 10000: # 10s timeout
if is_talk_trigger_active():
tap_started = True
break
time.sleep_ms(30)
# Start recording immediately!
tap_started = True
if tap_started:
# User tapped. Let's record!
draw_status_bar("Recording: 15s (Tap to stop)")
draw_status_bar("Recording: 10s...")
# 1. Start MCLK PWM and configure mic path based on board config
mclk_pwm = None
@@ -303,40 +377,26 @@ def main():
bits=16,
format=I2S.MONO)
audio_chunks = []
total_data_bytes = 0
buffer = bytearray(1024)
# Track release of the start-tap first
start_tap_released = False
rec_start_time = time.ticks_ms()
max_rec_duration_ms = 15000 # 15 seconds max duration
max_rec_bytes = 480000 # 15 seconds of mono 16kHz 16-bit PCM (32KB/s)
max_rec_duration_ms = 10000 # 10 seconds max duration
try:
while True:
# Safety: check duration and byte size
elapsed = time.ticks_diff(time.ticks_ms(), rec_start_time)
if elapsed >= max_rec_duration_ms or total_data_bytes >= max_rec_bytes:
print("Recording stopped: maximum duration/size reached")
break
# Read I2S chunk
bytes_read = i2s_rx.readinto(buffer)
if bytes_read > 0:
audio_chunks.append(bytes(buffer[:bytes_read]))
total_data_bytes += bytes_read
# Check release and stop-tap
touched = is_talk_trigger_active()
if not start_tap_released:
if not touched:
start_tap_released = True
print("Start-tap released. Listening for stop-tap...")
else:
if touched:
print("Stop-tap detected. Stopping recording...")
with open("voice_rec.pcm", "wb") as f:
while True:
# Safety: check duration
elapsed = time.ticks_diff(time.ticks_ms(), rec_start_time)
if elapsed >= max_rec_duration_ms:
print("Recording stopped: maximum duration reached")
break
# Read I2S chunk
bytes_read = i2s_rx.readinto(buffer)
if bytes_read > 0:
f.write(buffer[:bytes_read])
total_data_bytes += bytes_read
except Exception as e:
print("Error recording:", e)
finally:
@@ -350,80 +410,116 @@ def main():
if total_data_bytes >= 1000:
draw_status_bar("Sending & processing...")
# Prepend WAV header
raw_pcm = b"".join(audio_chunks)
wav_data = create_wav_header(len(raw_pcm)) + raw_pcm
# Determine dynamic device ID based on board configuration
device_id = "esp32_screen" if board_config.BOARD_TYPE == 'WAVESHARE_RLCD' else "little32"
# Headers
headers = {
"Authorization": "Bearer mcT1YA1vOr9wXSiHpCYalweEGGZKX-PIfZv2drp8BSg",
"Content-Type": "audio/wav",
"X-Device-ID": "kitchen-button"
"X-Device-ID": device_id,
"X-Hermes-Screen-Device": device_id,
"X-Hermes-Reply-Mode": "ack"
}
s = None
try:
import urequests
res = urequests.post("http://192.168.68.126:8642/api/esp32/voice",
data=wav_data,
headers=headers,
timeout=30)
if res.status_code == 200:
response_data = res.content
status_code, resp_headers, s = stream_hermes_request(
"http://192.168.68.126:8642/api/esp32/voice",
headers,
"voice_rec.pcm"
)
if status_code == 200:
content_type = resp_headers.get("content-type", "")
content_len = int(resp_headers.get("content-length", 0))
# Parse response WAV header
if len(response_data) >= 44 and response_data[0:4] == b'RIFF' and response_data[8:12] == b'WAVE':
if "audio" in content_type and content_len >= 44:
draw_status_bar("Playing response...")
# Parse channel count, sample rate, bits
import struct
fmt_chunk_size = struct.unpack('<I', response_data[16:20])[0]
channels, sample_rate, byte_rate, block_align, bits = struct.unpack('<HHIIH', response_data[22:34])
audio_start = 20 + fmt_chunk_size
while audio_start < len(response_data) - 8:
if response_data[audio_start:audio_start+4] == b'data':
audio_start += 8
break
chunk_size = struct.unpack('<I', response_data[audio_start+4:audio_start+8])[0]
audio_start += 8 + chunk_size
if audio_start < len(response_data):
# Play response using board_config parameters
on_val = 0 if board_config.audio_amp_active_level == 0 else 1
off_val = 1 if board_config.audio_amp_active_level == 0 else 0
amp_pin.value(on_val) # Enable Amp
i2s_format = I2S.MONO if channels == 1 else I2S.STEREO
i2s_tx = I2S(1,
sck=Pin(board_config.audio_i2s_sck),
ws=Pin(board_config.audio_i2s_ws),
sd=Pin(board_config.audio_i2s_tx_sd),
mode=I2S.TX,
ibuf=4096,
rate=sample_rate,
bits=bits,
format=i2s_format)
try:
pos = audio_start
chunk_size = 2048
while pos < len(response_data):
chunk = response_data[pos:pos+chunk_size]
i2s_tx.write(chunk)
pos += chunk_size
finally:
time.sleep_ms(150)
amp_pin.value(off_val) # Disable Amp
i2s_tx.deinit()
# Read and parse WAV header (first 44 bytes)
header = socket_read_exactly(s, 44)
if len(header) == 44 and header[0:4] == b'RIFF' and header[8:12] == b'WAVE':
import struct
fmt_idx = header.find(b'fmt ')
if fmt_idx != -1:
fmt_data = header[fmt_idx+8 : fmt_idx+24]
audio_format, channels, sample_rate, byte_rate, block_align, bits = struct.unpack('<HHIIHH', fmt_data)
data_idx = header.find(b'data')
if data_idx != -1:
# Skip remaining bytes to start of audio data
bytes_to_skip = (data_idx + 8) - 44
if bytes_to_skip > 0:
socket_read_exactly(s, bytes_to_skip)
# Play using board_config parameters
on_val = 0 if board_config.audio_amp_active_level == 0 else 1
off_val = 1 if board_config.audio_amp_active_level == 0 else 0
amp_pin.value(on_val) # Enable Amp
i2s_format = I2S.MONO if channels == 1 else I2S.STEREO
i2s_tx = I2S(1,
sck=Pin(board_config.audio_i2s_sck),
ws=Pin(board_config.audio_i2s_ws),
sd=Pin(board_config.audio_i2s_tx_sd),
mode=I2S.TX,
ibuf=4096,
rate=sample_rate,
bits=bits,
format=i2s_format)
try:
remaining_bytes = content_len - (data_idx + 8)
while remaining_bytes > 0:
chunk_to_read = min(remaining_bytes, 2048)
try:
chunk = s.recv(chunk_to_read)
except OSError:
break
if not chunk:
break
i2s_tx.write(chunk)
remaining_bytes -= len(chunk)
finally:
time.sleep_ms(150)
amp_pin.value(off_val) # Disable Amp
i2s_tx.deinit()
else:
draw_status_bar("Error: Gateway returned MP3")
time.sleep(3)
# Ack or text response
resp_text = ""
if content_len > 0:
try:
resp_text = socket_read_exactly(s, content_len).decode('utf-8', errors='ignore')
except:
pass
print("Non-WAV response received:", resp_text)
if "ack" in resp_text.lower() or "ok" in resp_text.lower() or "success" in resp_text.lower():
draw_status_bar("Success (Ack received)")
else:
draw_status_bar("Response processed.")
time.sleep(2)
else:
draw_status_bar(f"HTTP Error: {res.status_code}")
content_len = int(resp_headers.get("content-length", 0))
resp_text = "Unknown error"
if content_len > 0:
try:
resp_text = socket_read_exactly(s, content_len).decode('utf-8', errors='ignore')
except:
pass
print("Error response:", resp_text)
draw_status_bar(f"Error: {status_code}")
time.sleep(2)
except Exception as he:
print("Hermes HTTP post failed:", he)
sys.print_exception(he)
draw_status_bar("Connection Error")
time.sleep(2)
finally:
if s:
try:
s.close()
except:
pass
# Deinit MCLK PWM
if mclk_pwm:
+1 -1
View File
@@ -595,7 +595,7 @@ class MCPServer:
raise RuntimeError(f"Failed to play audio file '{filename}'. Check format (16kHz 16-bit PCM WAV recommended).")
elif name == "record_voice":
duration = int(args.get("duration_sec", 4))
duration = int(args.get("duration_sec", 10))
filename = str(args.get("filename", "recording.pcm"))
# Clamp duration to a reasonable range
duration = max(1, min(15, duration))
+5 -3
View File
@@ -10,7 +10,9 @@ import wave
import time
# Default ESP32 IP
ESP32_IP = "192.168.68.122"
ESP32_IP = "192.168.68.124"
if len(sys.argv) > 1:
ESP32_IP = sys.argv[1]
PORT = 80
URL = f"http://{ESP32_IP}:{PORT}/api/mcp"
@@ -268,7 +270,7 @@ def main():
while True:
print("\nReady for command. Choose an option:")
print(" [Enter] Record a voice command (4 seconds) on the board")
print(" [Enter] Record a voice command (10 seconds) on the board")
print(" [t] Type a text command manually")
print(" [q] Quit")
@@ -284,7 +286,7 @@ def main():
continue
else:
# Voice recording path
duration = 4
duration = 10
pcm_filename = "recording.pcm"
wav_filename = "temp_recording.wav"