Fix socket read stream crashes and NameError on boot
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user