Fix I2C scan on Waveshare RLCD board using SoftI2C and implement button PTT trigger fallback when touch is missing

This commit is contained in:
Adolfo Reyna
2026-06-17 22:40:21 -04:00
parent 31363734c5
commit 82de0f328b
+110 -83
View File
@@ -146,9 +146,19 @@ def main():
# Last user actions # Last user actions
last_action_str = "Boot finished." last_action_str = "Boot finished."
force_dashboard_redraw = True force_dashboard_redraw = True
voice_assistant_active = False
def is_talk_trigger_active():
if touch:
return touch.is_touched()
elif buttons and buttons.key:
return buttons.key.is_pressed()
return False
# 5. Register Button Handlers # 5. Register Button Handlers
def on_key_click(): def on_key_click():
if voice_assistant_active:
return
global local_led_mode_idx global local_led_mode_idx
local_led_mode_idx = (local_led_mode_idx + 1) % len(led_modes) local_led_mode_idx = (local_led_mode_idx + 1) % len(led_modes)
mode_name, color_fn, mode_type = led_modes[local_led_mode_idx] mode_name, color_fn, mode_type = led_modes[local_led_mode_idx]
@@ -204,9 +214,10 @@ def main():
last_action_str = f"Wi-Fi Connected: {ip_addr}" last_action_str = f"Wi-Fi Connected: {ip_addr}"
force_dashboard_redraw = True force_dashboard_redraw = True
# Check touch interaction if available (Push-to-talk Voice Assistant) # Check voice assistant trigger (touch screen or physical key button)
if touch and touch.is_touched(): if is_talk_trigger_active():
print("Touch detected! Starting Hermes Voice Assistant...") print("Voice assistant trigger detected! Starting Hermes Voice Assistant...")
voice_assistant_active = True
def draw_status_bar(text): def draw_status_bar(text):
y_bar = display.height - 25 y_bar = display.height - 25
@@ -216,55 +227,65 @@ def main():
display.show() display.show()
def clear_status_bar(): def clear_status_bar():
display.fill_rect(0, 215, 320, 25, 0) y_bar = display.height - 25
display.fill_rect(0, y_bar, display.width, 25, 0)
display.show() display.show()
# State 1: Wait for user to release their finger from the initial touch draw_status_bar("PTT Voice: Initializing...")
draw_status_bar("Release finger...")
# State 1: Wait for user to release the initial trigger press
draw_status_bar("Release button/screen...")
release_start = time.ticks_ms() release_start = time.ticks_ms()
while touch.is_touched(): while is_talk_trigger_active():
if time.ticks_diff(time.ticks_ms(), release_start) > 2000: if time.ticks_diff(time.ticks_ms(), release_start) > 2000:
print("Release timeout occurred") print("Release timeout occurred")
break break
time.sleep_ms(30) time.sleep_ms(30)
# State 2: Wait for tap-to-start recording # State 2: Wait for tap-to-start recording
draw_status_bar("Ready. Tap screen to record...") draw_status_bar("Ready. Tap key/screen to record...")
tap_started = False tap_started = False
start_wait = time.ticks_ms() start_wait = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start_wait) < 10000: # 10s timeout to start recording while time.ticks_diff(time.ticks_ms(), start_wait) < 10000: # 10s timeout
if touch.is_touched(): if is_talk_trigger_active():
tap_started = True tap_started = True
break break
time.sleep_ms(30) time.sleep_ms(30)
if tap_started: if tap_started:
# User tapped the screen. Let's record! # User tapped. Let's record!
draw_status_bar("Recording: 15s (Tap to stop)") draw_status_bar("Recording: 15s (Tap to stop)")
# 1. Start MCLK PWM and configure ES8311 mic path # 1. Start MCLK PWM and configure mic path based on board config
mclk_pin = Pin(4, Pin.OUT) mclk_pwm = None
mclk_pwm = machine.PWM(mclk_pin) if board_config.audio_mclk_pin is not None:
mclk_pwm.freq(6144000) mclk_pin = Pin(board_config.audio_mclk_pin, Pin.OUT)
mclk_pwm.duty_u16(32768) mclk_pwm = machine.PWM(mclk_pin)
mclk_pwm.freq(board_config.audio_mclk_freq)
mclk_pwm.duty_u16(32768)
codec = ES8311(i2c) if board_config.audio_mic_codec == "ES7210":
if codec.init(sample_rate=16000): from audio_util import ES7210
codec.set_volume(80) # Set a balanced volume to prevent speaker saturation codec = ES7210(i2c)
try: codec.init(sample_rate=16000, bit_width=16)
codec._write(0x14, 0x1A) # Enable analog mic input & PGA else:
codec._write(0x16, 0x01) # Enable +6dB gain boost for mic recording from audio_util import ES8311
codec._write(0x17, 0xC8) # Set ADC digital volume codec = ES8311(i2c)
except Exception as e: if codec.init(sample_rate=16000):
print("Failed to set mic gain:", e) codec.set_volume(80)
try:
codec._write(0x14, 0x1A) # Enable analog mic input & PGA
codec._write(0x16, 0x01) # Enable +6dB gain boost
codec._write(0x17, 0xC8) # Set ADC digital volume
except Exception as e:
print("Failed to set mic gain:", e)
# 2. Configure I2S RX for recording (Mono 16kHz) # 2. Configure I2S RX for recording (Mono 16kHz)
i2s_rx = I2S(1, i2s_rx = I2S(1,
sck=Pin(5), sck=Pin(board_config.audio_i2s_sck),
ws=Pin(7), ws=Pin(board_config.audio_i2s_ws),
sd=Pin(6), sd=Pin(board_config.audio_i2s_rx_sd),
mode=I2S.RX, mode=I2S.RX,
ibuf=8000, ibuf=8000,
rate=16000, rate=16000,
@@ -280,7 +301,6 @@ def main():
rec_start_time = time.ticks_ms() rec_start_time = time.ticks_ms()
max_rec_duration_ms = 15000 # 15 seconds max duration 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_bytes = 480000 # 15 seconds of mono 16kHz 16-bit PCM (32KB/s)
last_sec_left = -1
try: try:
while True: while True:
@@ -290,20 +310,14 @@ def main():
print("Recording stopped: maximum duration/size reached") print("Recording stopped: maximum duration/size reached")
break break
# Update countdown
sec_left = max(0, 15 - (elapsed // 1000))
if sec_left != last_sec_left:
last_sec_left = sec_left
draw_status_bar(f"Recording: {sec_left}s (Tap to stop)")
# Read I2S chunk # Read I2S chunk
bytes_read = i2s_rx.readinto(buffer) bytes_read = i2s_rx.readinto(buffer)
if bytes_read > 0: if bytes_read > 0:
audio_chunks.append(bytes(buffer[:bytes_read])) audio_chunks.append(bytes(buffer[:bytes_read]))
total_data_bytes += bytes_read total_data_bytes += bytes_read
# Check touch state # Check release and stop-tap
touched = touch.is_touched() touched = is_talk_trigger_active()
if not start_tap_released: if not start_tap_released:
if not touched: if not touched:
start_tap_released = True start_tap_released = True
@@ -316,10 +330,11 @@ def main():
print("Error recording:", e) print("Error recording:", e)
finally: finally:
i2s_rx.deinit() i2s_rx.deinit()
try: if board_config.audio_mic_codec == "ES8311":
codec._write(0x16, 0x00) # Reset mic gain back to 0dB immediately to prevent playback saturation try:
except: codec._write(0x16, 0x00) # Reset mic gain
pass except:
pass
if total_data_bytes >= 1000: if total_data_bytes >= 1000:
draw_status_bar("Sending & processing...") draw_status_bar("Sending & processing...")
@@ -335,53 +350,60 @@ def main():
"X-Device-ID": "kitchen-button" "X-Device-ID": "kitchen-button"
} }
print(f"Posting WAV ({len(wav_data)} bytes) to Hermes API...")
try: try:
res = urequests.post("http://192.168.68.126:8642/api/esp32/voice", headers=headers, data=wav_data) import urequests
print(f"HTTP Status: {res.status_code}") res = urequests.post("https://hermes.adolforeyna.com/api/voice",
data=wav_data,
headers=headers,
timeout=30)
if res.status_code == 200: if res.status_code == 200:
response_data = res.content response_data = res.content
print(f"Received response: {len(response_data)} bytes")
if response_data[0:4] == b'RIFF' and response_data[8:12] == b'WAVE': # Parse response WAV header
idx = response_data.find(b'fmt ') if len(response_data) >= 44 and response_data[0:4] == b'RIFF' and response_data[8:12] == b'WAVE':
if idx != -1: draw_status_bar("Playing response...")
fmt_chunk = response_data[idx:idx+24]
audio_format, channels, sample_rate, byte_rate, block_align, bits = struct.unpack('<HHIIHH', fmt_chunk[8:24])
print(f"Playing response WAV: {sample_rate}Hz, {channels}ch, {bits}bit")
data_idx = response_data.find(b'data') # Parse channel count, sample rate, bits
if data_idx != -1: import struct
audio_start = data_idx + 8 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])
draw_status_bar("Speaking...") 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
# Play response if audio_start < len(response_data):
amp_pin.value(0) # Enable Amp (Active Low) # Play response using board_config parameters
i2s_format = I2S.MONO if channels == 1 else I2S.STEREO on_val = 0 if board_config.audio_amp_active_level == 0 else 1
i2s_tx = I2S(1, off_val = 1 if board_config.audio_amp_active_level == 0 else 0
sck=Pin(5), amp_pin.value(on_val) # Enable Amp
ws=Pin(7),
sd=Pin(8), i2s_format = I2S.MONO if channels == 1 else I2S.STEREO
mode=I2S.TX, i2s_tx = I2S(1,
ibuf=4096, sck=Pin(board_config.audio_i2s_sck),
rate=sample_rate, ws=Pin(board_config.audio_i2s_ws),
bits=bits, sd=Pin(board_config.audio_i2s_tx_sd),
format=i2s_format) mode=I2S.TX,
try: ibuf=4096,
pos = audio_start rate=sample_rate,
chunk_size = 2048 bits=bits,
while pos < len(response_data): format=i2s_format)
chunk = response_data[pos:pos+chunk_size] try:
i2s_tx.write(chunk) pos = audio_start
pos += chunk_size chunk_size = 2048
finally: while pos < len(response_data):
time.sleep_ms(150) chunk = response_data[pos:pos+chunk_size]
amp_pin.value(1) # Disable Amp i2s_tx.write(chunk)
i2s_tx.deinit() pos += chunk_size
finally:
time.sleep_ms(150)
amp_pin.value(off_val) # Disable Amp
i2s_tx.deinit()
else: else:
# MP3 warning message on display
draw_status_bar("Error: Gateway returned MP3") draw_status_bar("Error: Gateway returned MP3")
time.sleep(3) time.sleep(3)
else: else:
@@ -393,17 +415,22 @@ def main():
time.sleep(2) time.sleep(2)
# Deinit MCLK PWM # Deinit MCLK PWM
mclk_pwm.deinit() if mclk_pwm:
try:
mclk_pwm.deinit()
except:
pass
# Debounce touch release at the very end of wizard # Debounce release at the very end of wizard
clear_status_bar() clear_status_bar()
release_end = time.ticks_ms() release_end = time.ticks_ms()
while touch.is_touched(): while is_talk_trigger_active():
if time.ticks_diff(time.ticks_ms(), release_end) > 2000: if time.ticks_diff(time.ticks_ms(), release_end) > 2000:
break break
time.sleep_ms(30) time.sleep_ms(30)
time.sleep_ms(200) time.sleep_ms(200)
voice_assistant_active = False
last_action_str = "Voice query finished" last_action_str = "Voice query finished"
force_dashboard_redraw = True force_dashboard_redraw = True