This commit is contained in:
jeremygan2021
2026-03-02 22:43:04 +08:00
parent c0882a93a9
commit e0776a1839
18 changed files with 1331 additions and 82 deletions

421
main.py
View File

@@ -4,15 +4,195 @@ import math
import struct
import array
import gc
import network
import st7789py as st7789
from config import CURRENT_CONFIG
from audio import AudioPlayer, Microphone
from display import Display
from websocket_client import WebSocketClient
import uselect
# =============================================================================
# 网络配置
# =============================================================================
WIFI_SSID = "Tangledup-AI"
WIFI_PASS = "djt12345678"
# 请修改为你的电脑 IP 地址
SERVER_IP = "6.6.6.88"
SERVER_PORT = 8000
SERVER_URL = f"ws://{SERVER_IP}:{SERVER_PORT}/ws/audio"
def diagnose_wifi():
"""
诊断WiFi模块状态打印详细的调试信息
"""
print("\n" + "="*50)
print("WiFi DIAGNOSTIC INFORMATION")
print("="*50)
wlan = network.WLAN(network.STA_IF)
# 基本状态
print(f"WiFi Module Active: {wlan.active()}")
print(f"Connection Status: {wlan.isconnected()}")
if wlan.isconnected():
print(f"Network Config: {wlan.ifconfig()}")
print(f"Network SSID: {wlan.config('essid')}")
print(f"Signal Strength: {wlan.status('rssi')} dBm")
# 扫描可用网络
try:
print("\nScanning for available networks...")
wlan.active(True)
time.sleep(1)
networks = wlan.scan()
print(f"Found {len(networks)} networks:")
for net in networks:
ssid = net[0].decode('utf-8') if net[0] else "Hidden"
bssid = ':'.join(['%02x' % b for b in net[1]])
channel = net[2]
rssi = net[3]
security = net[4]
# 标记目标网络
marker = " [TARGET]" if ssid == WIFI_SSID else ""
print(f" {ssid}{marker}")
print(f" BSSID: {bssid}, Channel: {channel}, RSSI: {rssi}dBm")
# 信号强度解释
if rssi > -50:
signal_desc = "Excellent"
elif rssi > -60:
signal_desc = "Good"
elif rssi > -70:
signal_desc = "Fair"
else:
signal_desc = "Weak"
print(f" Signal: {signal_desc}")
print("")
except Exception as e:
print(f"Network scan failed: {e}")
print("="*50 + "\n")
def connect_wifi(max_retries=3):
"""
连接WiFi网络包含完整的错误处理和重试机制
Args:
max_retries: 最大重试次数默认为3次
Returns:
bool: 连接成功返回True失败返回False
"""
wlan = network.WLAN(network.STA_IF)
# 首先确保WiFi模块处于干净状态
try:
wlan.active(False) # 先关闭WiFi
time.sleep(1) # 等待1秒让模块完全关闭
wlan.active(True) # 重新激活WiFi
time.sleep(1) # 等待模块初始化完成
except Exception as e:
print(f"WiFi module initialization error: {e}")
return False
# 尝试连接,包含重试机制
for attempt in range(max_retries):
try:
print(f"WiFi connection attempt {attempt + 1}/{max_retries}")
# 检查是否已连接
if wlan.isconnected():
print('Already connected to WiFi')
print('Network config:', wlan.ifconfig())
return True
# 尝试连接
print(f'Connecting to WiFi {WIFI_SSID}...')
wlan.connect(WIFI_SSID, WIFI_PASS)
# 等待连接完成,设置超时
start_time = time.time()
while not wlan.isconnected():
if time.time() - start_time > 20: # 单次连接超时20秒
print("WiFi connection timeout!")
break
time.sleep(0.5)
print(".", end="")
print("") # 换行
# 检查连接结果
if wlan.isconnected():
print('WiFi connected successfully!')
print('Network config:', wlan.ifconfig())
return True
else:
print(f"Connection attempt {attempt + 1} failed")
# 在重试前进行清理
if attempt < max_retries - 1: # 如果不是最后一次尝试
print("Resetting WiFi module for retry...")
wlan.disconnect() # 断开连接
time.sleep(2) # 等待2秒
except OSError as e:
print(f"WiFi connection error on attempt {attempt + 1}: {e}")
if "Wifi Internal State Error" in str(e):
print("Detected internal state error, resetting WiFi module...")
try:
wlan.active(False)
time.sleep(2)
wlan.active(True)
time.sleep(1)
except:
pass
if attempt < max_retries - 1:
print(f"Retrying in 3 seconds...")
time.sleep(3)
except Exception as e:
print(f"Unexpected error on attempt {attempt + 1}: {e}")
if attempt < max_retries - 1:
time.sleep(2)
# 所有尝试都失败
print("All WiFi connection attempts failed!")
try:
wlan.active(False) # 关闭WiFi模块节省电力
except:
pass
return False
# =============================================================================
# 硬件引脚配置 (从 config.py 获取)
# =============================================================================
def print_nice_asr(text, display=None):
"""在终端美观地打印ASR结果并在屏幕显示"""
print("\n" + "*"*40)
print(" ASR RESULT:")
print(f" {text}")
print("*"*40 + "\n")
if display and display.tft:
# 清除之前的文本区域 (保留顶部的状态栏和底部的可视化条)
# 假设状态栏 30px底部 240-200=40px 用于可视化?
# init_ui 画了 0-30 的白条。
# update_audio_bar 在 240-bar_height 画条。
# 我们使用中间区域 40 - 200
display.fill_rect(0, 40, 240, 160, st7789.BLACK)
display.text(text, 0, 40, st7789.WHITE)
def main():
print("\n" + "="*40)
print("AUDIO & MIC DIAGNOSTIC V5 (Modular & Clean)")
@@ -35,7 +215,44 @@ def main():
speaker = AudioPlayer()
if speaker.i2s:
# 默认播放马里奥
speaker.play_mario()
# speaker.play_mario()
# 播放简单方波 (1kHz, 1秒)
# 直接在 main.py 中实现分块播放,避免因 audio.py 未同步导致的 MemoryError
print("Playing 1kHz square wave...")
try:
import struct
# 1. 参数设置
sr = 24000 # 默认采样率
if hasattr(speaker, 'config') and speaker.config:
sr = speaker.config.get('sample_rate', 24000)
freq = 1000
duration = 1000 # ms
vol = 10000 # 音量 (max 32767)
# 2. 准备缓冲区 (只生成一小段,循环播放)
# 1kHz @ 24kHz -> 24 samples/cycle
period = sr // freq
# 生成约 1000 字节的 buffer (包含整数个周期)
cycles_in_buf = 10
buf = bytearray(period * cycles_in_buf * 4) # 16bit stereo = 4 bytes/frame
# 3. 填充方波数据
for i in range(period * cycles_in_buf):
# 方波逻辑
sample = vol if (i % period) < (period // 2) else -vol
# 写入左右声道 (Little Endian, 16-bit signed)
struct.pack_into('<hh', buf, i*4, sample, sample)
# 4. 循环写入 I2S
t_end = time.ticks_add(time.ticks_ms(), duration)
while time.ticks_diff(t_end, time.ticks_ms()) > 0:
speaker.i2s.write(buf)
except Exception as e:
print(f"Tone error: {e}")
else:
print("!!! Speaker initialization failed")
@@ -57,7 +274,49 @@ def main():
# 录音状态变量
is_recording = False
recorded_chunks = []
# WebSocket 连接
ws = None
# 定义连接函数
def connect_ws():
nonlocal ws
# Reset existing connection object to ensure clean slate
try:
if ws:
ws.close()
except:
pass
ws = None
try:
print(f"Connecting to WebSocket Server: {SERVER_URL}")
ws = WebSocketClient(SERVER_URL)
print("WebSocket connected successfully!")
# Pass WebSocket to display for font loading
if display:
display.set_ws(ws)
return True
except Exception as e:
print(f"WebSocket connection failed: {e}")
return False
# 先运行WiFi诊断
print("Running WiFi diagnostics...")
diagnose_wifi()
# 尝试连接WiFi
print("Starting WiFi connection process...")
if connect_wifi(max_retries=3):
print("WiFi connected successfully!")
connect_ws()
else:
print("WiFi connection failed after all attempts!")
print("Continuing in offline mode without WebSocket functionality...")
print("You can still use the device for local audio recording and visualization.")
# 调试:打印一次 Boot 键状态
print(f"Boot Button Initial State: {boot_btn.value()}")
@@ -86,68 +345,151 @@ def main():
if not is_recording:
print("\n>>> Start Recording (Boot Pressed)...")
is_recording = True
recorded_chunks = []
if display.tft:
print(">>> Filling Screen WHITE")
display.fill(st7789.WHITE)
else:
print(">>> Display TFT is None!")
# 尝试重连 WS
if ws is None or not ws.is_connected():
print(">>> WS not connected, trying to reconnect...")
connect_ws()
# 发送开始录音指令
if ws and ws.is_connected():
try:
ws.send("START_RECORDING")
except Exception as e:
print(f"WS Send Error: {e}")
ws = None # Disconnect on error
else:
print(">>> Warning: No WebSocket connection! Audio will be discarded.")
# 录音
# 录音并流式传输
if mic.i2s:
num_read = mic.readinto(read_buf)
if num_read > 0:
try:
recorded_chunks.append(bytes(read_buf[:num_read]))
except MemoryError:
print("Memory Full!")
if ws and ws.is_connected():
try:
# 发送二进制数据
ws.send(read_buf[:num_read], opcode=2)
# 检查是否有回传的 ASR 结果 (非阻塞)
poller = uselect.poll()
poller.register(ws.sock, uselect.POLLIN)
events = poller.poll(0) # 0 = return immediately
if events:
msg = ws.recv()
if isinstance(msg, str) and msg.startswith("ASR:"):
print_nice_asr(msg[4:], display)
except Exception as e:
print(f"WS Send/Recv Error: {e}")
# 如果发送失败,视为断开
try:
ws.close()
except:
pass
ws = None
else:
# 如果没有 WS就不保存了避免内存溢出
pass
continue # 跳过可视化逻辑
# === 按键释放处理 ===
elif is_recording:
print(f"\n>>> Stop Recording. Captured {len(recorded_chunks)} chunks.")
print(f"\n>>> Stop Recording.")
is_recording = False
if display.tft:
display.init_ui()
# 播放录音
if speaker.i2s and len(recorded_chunks) > 0:
print(">>> Playing...")
# 停止录音并等待回放
if ws:
try:
cfg = speaker.config
# 重新初始化 Speaker (16kHz Mono 16-bit) 以匹配 Mic 数据
speaker.i2s.deinit()
speaker.i2s = machine.I2S(
0,
sck=machine.Pin(cfg['bck']),
ws=machine.Pin(cfg['ws']),
sd=machine.Pin(cfg['sd']),
mode=machine.I2S.TX,
bits=16,
format=machine.I2S.MONO,
rate=16000,
ibuf=20000,
)
print(">>> Sending STOP & Waiting for playback...")
ws.send("STOP_RECORDING")
# 播放数据
for chunk in recorded_chunks:
# 32-bit Mono -> 16-bit Mono (取高16位)
# chunk 是 bytes, 转为 array('h') 方便访问 16-bit word
# 32-bit 数据: LowWord, HighWord
# 我们需要 HighWord
arr = array.array('h', chunk)
samples = arr[1::2]
speaker.i2s.write(samples)
# 重新初始化 Speaker (16kHz Mono 16-bit)
if speaker.i2s:
cfg = speaker.config
speaker.i2s.deinit()
speaker.i2s = machine.I2S(
0,
sck=machine.Pin(cfg['bck']),
ws=machine.Pin(cfg['ws']),
sd=machine.Pin(cfg['sd']),
mode=machine.I2S.TX,
bits=16,
format=machine.I2S.MONO,
rate=16000,
ibuf=40000,
)
# 接收回放循环
playback_timeout = 5000 # 5秒无数据则退出
last_data_time = time.ticks_ms()
while True:
# Check for data with timeout
poller = uselect.poll()
poller.register(ws.sock, uselect.POLLIN)
events = poller.poll(100) # 100ms wait
if events:
msg = ws.recv()
last_data_time = time.ticks_ms()
if isinstance(msg, str):
if msg == "START_PLAYBACK":
print(">>> Server starting playback stream...")
continue
elif msg == "STOP_PLAYBACK":
print(">>> Server finished playback.")
break
elif msg.startswith("ASR:"):
print_nice_asr(msg[4:], display)
elif isinstance(msg, bytes):
# 播放接收到的音频数据
if speaker.i2s:
# 使用 try-except 防止 write 阻塞导致的问题
try:
speaker.i2s.write(msg)
except Exception as e:
print(f"I2S Write Error: {e}")
elif msg is None:
print("WS Connection closed or error (recv returned None)")
try:
ws.close()
except:
pass
ws = None
break
else:
# No data received in this poll window
if time.ticks_diff(time.ticks_ms(), last_data_time) > playback_timeout:
print("Playback timeout - no data received for 5 seconds")
break
# Feed watchdog or do other small tasks if needed
# time.sleep(0.01)
except Exception as e:
print(f"Playback error: {e}")
print(f"Playback loop error: {e}")
try:
ws.close()
except:
pass
ws = None
# 恢复 Speaker 原始配置
if speaker.i2s: speaker.i2s.deinit()
speaker._init_audio()
recorded_chunks = []
gc.collect()
# === 原有的可视化逻辑 ===
@@ -178,10 +520,7 @@ def main():
last_print = time.ticks_ms()
if display.tft:
# 调整缩放比例,让显示更敏感
# 你的日志显示安静时 Max ~2000-3000, 说话时 Max ~40000
# 我们可以把 Max 40000 映射到满格
# 调整缩放比例
bar_height = int((max_val / 40000) * 200)
if bar_height > 200: bar_height = 200
if bar_height < 0: bar_height = 0