Fix I2C scan on Waveshare RLCD board using SoftI2C and implement button PTT trigger fallback when touch is missing
This commit is contained in:
@@ -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])
|
# Parse channel count, sample rate, bits
|
||||||
print(f"Playing response WAV: {sample_rate}Hz, {channels}ch, {bits}bit")
|
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
|
||||||
|
|
||||||
data_idx = response_data.find(b'data')
|
if audio_start < len(response_data):
|
||||||
if data_idx != -1:
|
# Play response using board_config parameters
|
||||||
audio_start = data_idx + 8
|
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
|
||||||
draw_status_bar("Speaking...")
|
amp_pin.value(on_val) # Enable Amp
|
||||||
|
|
||||||
# Play response
|
i2s_format = I2S.MONO if channels == 1 else I2S.STEREO
|
||||||
amp_pin.value(0) # Enable Amp (Active Low)
|
i2s_tx = I2S(1,
|
||||||
i2s_format = I2S.MONO if channels == 1 else I2S.STEREO
|
sck=Pin(board_config.audio_i2s_sck),
|
||||||
i2s_tx = I2S(1,
|
ws=Pin(board_config.audio_i2s_ws),
|
||||||
sck=Pin(5),
|
sd=Pin(board_config.audio_i2s_tx_sd),
|
||||||
ws=Pin(7),
|
mode=I2S.TX,
|
||||||
sd=Pin(8),
|
ibuf=4096,
|
||||||
mode=I2S.TX,
|
rate=sample_rate,
|
||||||
ibuf=4096,
|
bits=bits,
|
||||||
rate=sample_rate,
|
format=i2s_format)
|
||||||
bits=bits,
|
try:
|
||||||
format=i2s_format)
|
pos = audio_start
|
||||||
try:
|
chunk_size = 2048
|
||||||
pos = audio_start
|
while pos < len(response_data):
|
||||||
chunk_size = 2048
|
chunk = response_data[pos:pos+chunk_size]
|
||||||
while pos < len(response_data):
|
i2s_tx.write(chunk)
|
||||||
chunk = response_data[pos:pos+chunk_size]
|
pos += chunk_size
|
||||||
i2s_tx.write(chunk)
|
finally:
|
||||||
pos += chunk_size
|
time.sleep_ms(150)
|
||||||
finally:
|
amp_pin.value(off_val) # Disable Amp
|
||||||
time.sleep_ms(150)
|
i2s_tx.deinit()
|
||||||
amp_pin.value(1) # 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user