From d0fcc5fc7bf3b431c7c79eb3cc4878a76428d0c0 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Thu, 18 Jun 2026 11:35:06 -0400 Subject: [PATCH] Fix socket read stream crashes and NameError on boot --- lib/audio_util.py | 2 +- main.py | 306 +++++++++++++++++++++++++++++---------------- mcp_server.py | 2 +- voice_assistant.py | 8 +- 4 files changed, 208 insertions(+), 110 deletions(-) diff --git a/lib/audio_util.py b/lib/audio_util.py index 906958f..d805a11 100644 --- a/lib/audio_util.py +++ b/lib/audio_util.py @@ -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: diff --git a/main.py b/main.py index 1cbb6be..e199bde 100644 --- a/main.py +++ b/main.py @@ -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(' 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: diff --git a/mcp_server.py b/mcp_server.py index 6973601..2ffb530 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -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)) diff --git a/voice_assistant.py b/voice_assistant.py index 880d764..9884489 100755 --- a/voice_assistant.py +++ b/voice_assistant.py @@ -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"