M5 Update and Web Voice Agent using Terp AI.
This commit is contained in:
@@ -1,177 +0,0 @@
|
||||
# --- FACE ASSETS ---
|
||||
C = {
|
||||
'0': 0x222222,
|
||||
'Y': 0xFFFF00,
|
||||
'R': 0xFF0000,
|
||||
'W': 0xFFFFFF,
|
||||
'B': 0x000000,
|
||||
'P': 0xFF8888,
|
||||
'D': 0x555555
|
||||
}
|
||||
|
||||
f_s_o = [
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000WWW0000WWW000",
|
||||
"00WWBW0000WWBW00",
|
||||
"00WWBW0000WWBW00",
|
||||
"000WWW0000WWW000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"YY000000000000YY",
|
||||
"0YY0000000000YY0",
|
||||
"00YY00000000YY00",
|
||||
"000YYYYYYYYYY000",
|
||||
"00000YYYYYY00000",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_s_h = [
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000WWW0000WWW000",
|
||||
"00WWBW0000WWBW00",
|
||||
"000WWW0000WWW000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"YY000000000000YY",
|
||||
"0YY0000000000YY0",
|
||||
"00YY00000000YY00",
|
||||
"000YYYYYYYYYY000",
|
||||
"00000YYYYYY00000",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_s_c = [
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"00WWWW0000WWWW00",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"YY000000000000YY",
|
||||
"0YY0000000000YY0",
|
||||
"00YY00000000YY00",
|
||||
"000YYYYYYYYYY000",
|
||||
"00000YYYYYY00000",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_a_o = [
|
||||
"0000000000000000",
|
||||
"0DDDD000000DDDD0",
|
||||
"00DDDD0000DDDD00",
|
||||
"000DDDD00DDDD000",
|
||||
"000WWW0000WWW000",
|
||||
"00WWBB0000BBWW00",
|
||||
"000WWW0000WWW000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000000RRRR000000",
|
||||
"0000RRRRRRRR0000",
|
||||
"00RRRR0000RRRR00",
|
||||
"0RRR00000000RRR0",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_a_h = [
|
||||
"0000000000000000",
|
||||
"0DDDD000000DDDD0",
|
||||
"00DDDD0000DDDD00",
|
||||
"000DDDD00DDDD000",
|
||||
"0000000000000000",
|
||||
"000WWW0000WWW000",
|
||||
"00WWBB0000BBWW00",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000000RRRR000000",
|
||||
"0000RRRRRRRR0000",
|
||||
"00RRRR0000RRRR00",
|
||||
"0RRR00000000RRR0",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_a_c = [
|
||||
"0000000000000000",
|
||||
"0DDDD000000DDDD0",
|
||||
"00DDDD0000DDDD00",
|
||||
"000DDDD00DDDD000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000WW000000WW000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000000RRRR000000",
|
||||
"0000RRRRRRRR0000",
|
||||
"00RRRR0000RRRR00",
|
||||
"0RRR00000000RRR0",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_t_1 = [
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000WWW0000WWW000",
|
||||
"00WWWW0000WWWW00",
|
||||
"000WWW0000WWW000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"000YYYYYYYYYY000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
f_t_2 = [
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"00DD00000000DD00",
|
||||
"000DD000000DD000",
|
||||
"0000000000000000",
|
||||
"000WWW0000WWW000",
|
||||
"00WWBW0000WWBW00",
|
||||
"000WWW0000WWW000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000RRRRRRRR0000",
|
||||
"000RR000000RR000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000"
|
||||
]
|
||||
|
||||
def d_s(lcd, f, s_x, s_y, p_s):
|
||||
for r in range(16):
|
||||
c = 0
|
||||
while c < 16:
|
||||
s_c = c
|
||||
v = f[r][c]
|
||||
while c < 16 and f[r][c] == v:
|
||||
c += 1
|
||||
w = c - s_c
|
||||
|
||||
x_p = s_x + (s_c * p_s)
|
||||
y_p = s_y + (r * p_s)
|
||||
|
||||
lcd.fillRect(x_p, y_p, w * p_s, p_s, C[v])
|
||||
+515
-267
@@ -3,73 +3,277 @@ import time
|
||||
import machine
|
||||
import json
|
||||
import math
|
||||
import _thread
|
||||
import websocket
|
||||
import usocket
|
||||
import ubinascii
|
||||
import os
|
||||
import gc
|
||||
from m5stack import *
|
||||
from m5ui import *
|
||||
from uiflow import *
|
||||
|
||||
C = { '0': 0x222222, 'Y': 0xFFFF00, 'R': 0xFF0000, 'W': 0xFFFFFF, 'B': 0x000000, 'D': 0x555555 }
|
||||
f_s_o = "00000000000000000000000000000000000WWW0000WWW00000WWBW0000WWBW0000WWBW0000WWBW00000WWW0000WWW000000000000000000000000000000000000000000000000000YY000000000000YY0YY0000000000YY000YY00000000YY00000YYYYYYYYYY00000000YYYYYY0000000000000000000000000000000000000"
|
||||
f_s_c = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000WWWW0000WWWW00000000000000000000000000000000000000000000000000YY000000000000YY0YY0000000000YY000YY00000000YY00000YYYYYYYYYY00000000YYYYYY0000000000000000000000000000000000000"
|
||||
f_t_1 = "0000000000000000000000000000000000000000000000000000000000000000000WWW0000WWW00000WWWW0000WWWW00000WWW0000WWW00000000000000000000000000000000000000000000000000000000000000000000000000000000000000YYYYYYYYYY000000000000000000000000000000000000000000000000000"
|
||||
f_t_2 = "0000000000000000000000000000000000DD00000000DD00000DD000000DD0000000000000000000000WWW0000WWW00000WWBW0000WWBW00000WWW0000WWW0000000000000000000000000000000000000000000000000000000RRRRRRRR0000000RR000000RR000000000000000000000000000000000000000000000000000"
|
||||
f_listen = "0000000000000000000000000000000000WWWW0000WWWW0000WBBW0000WBBW0000WBBW0000WBBW0000WWWW0000WWWW0000000000000000000000000000000000000000000000000000000YYYYYY000000000YY0000YY00000000YY0000YY000000000YYYYYY00000000000000000000000000000000000000000000000000000"
|
||||
f_speak = "00000000000000000000000000000000000WWW0000WWW00000WWBW0000WWBW0000WWBW0000WWBW00000WWW0000WWW0000000000000000000000000000000000000000000000000000000YYYYYYYY0000000YY000000YY000000YY000000YY0000000YYYYYYYY0000000000000000000000000000000000000000000000000000"
|
||||
f_angry = "00000000000000000DDDD000000DDDD000DDDD0000DDDD00000DDDD00DDDD000000WWW0000WWW00000WWBB0000BBWW00000WWW0000WWW000000000000000000000000000000000000000000000000000000000RRRR0000000000RRRRRRRR000000RRRR0000RRRR000RRR00000000RRR000000000000000000000000000000000"
|
||||
|
||||
def d_s(lcd, f, s_x, s_y, p_s):
|
||||
for r in range(16):
|
||||
c = 0
|
||||
i = r * 16
|
||||
while c < 16:
|
||||
s_c = c
|
||||
v = f[i + c]
|
||||
while c < 16 and f[i + c] == v:
|
||||
c += 1
|
||||
w = c - s_c
|
||||
lcd.fillRect(s_x + s_c * p_s, s_y + r * p_s, w * p_s, p_s, C[v])
|
||||
|
||||
setScreenColor(0x222222)
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
# ==========================================
|
||||
# CONFIGURATION
|
||||
# ==========================================
|
||||
|
||||
WIFI_SSID = "Blobby"
|
||||
WIFI_PASS = "73556088"
|
||||
# Update this to the IP address of your backend server
|
||||
WS_URL = "ws://192.168.137.1:8000/ws/voice"
|
||||
# ---------------------
|
||||
WS_URL = "ws://192.168.137.1:8000/ws/voice"
|
||||
|
||||
def draw_status(status, color):
|
||||
# Location Settings for Noise Monitoring
|
||||
CURRENT_ROOM_ID = "mckeldin"
|
||||
CURRENT_LAT = 38.986021
|
||||
CURRENT_LNG = -76.944949
|
||||
|
||||
# Audio Settings
|
||||
TARGET_SAMPLE_RATE = 8000 # Voice recording sample rate
|
||||
AUDIO_CHUNK_SIZE = 2048
|
||||
|
||||
# ==========================================
|
||||
# WEBSOCKET CLIENT
|
||||
# ==========================================
|
||||
|
||||
class WSClient:
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
|
||||
def send(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
opcode = 0x1
|
||||
else:
|
||||
opcode = 0x2
|
||||
length = len(data)
|
||||
mask_key = os.urandom(4)
|
||||
header = bytearray()
|
||||
header.append(0x80 | opcode)
|
||||
if length < 126:
|
||||
header.append(0x80 | length)
|
||||
elif length < 65536:
|
||||
header.append(0x80 | 126)
|
||||
header.append((length >> 8) & 0xFF)
|
||||
header.append(length & 0xFF)
|
||||
else:
|
||||
header.append(0x80 | 127)
|
||||
for i in range(7, -1, -1):
|
||||
header.append((length >> (8 * i)) & 0xFF)
|
||||
header.extend(mask_key)
|
||||
masked = bytearray(data)
|
||||
for i in range(length):
|
||||
masked[i] ^= mask_key[i % 4]
|
||||
self._sock.send(header + masked)
|
||||
|
||||
def recv(self):
|
||||
hdr = self._recv_exact(2)
|
||||
if not hdr or len(hdr) < 2:
|
||||
return None
|
||||
opcode = hdr[0] & 0x0F
|
||||
is_masked = (hdr[1] & 0x80) != 0
|
||||
length = hdr[1] & 0x7F
|
||||
if length == 126:
|
||||
ext = self._recv_exact(2)
|
||||
length = (ext[0] << 8) | ext[1]
|
||||
elif length == 127:
|
||||
ext = self._recv_exact(8)
|
||||
length = 0
|
||||
for b in ext:
|
||||
length = (length << 8) | b
|
||||
mask_key = self._recv_exact(4) if is_masked else None
|
||||
payload = self._recv_exact(length) if length > 0 else b""
|
||||
if is_masked and mask_key and payload:
|
||||
payload = bytearray(payload)
|
||||
for i in range(len(payload)):
|
||||
payload[i] ^= mask_key[i % 4]
|
||||
payload = bytes(payload)
|
||||
if opcode == 0x8:
|
||||
return None
|
||||
if opcode == 0x9:
|
||||
self._send_pong(payload)
|
||||
return self.recv()
|
||||
if opcode == 0x1:
|
||||
return payload.decode() if payload else ""
|
||||
return payload
|
||||
|
||||
def _send_pong(self, data):
|
||||
mask_key = os.urandom(4)
|
||||
length = len(data) if data else 0
|
||||
header = bytearray([0x8A, 0x80 | length])
|
||||
header.extend(mask_key)
|
||||
if data:
|
||||
masked = bytearray(data)
|
||||
for i in range(length):
|
||||
masked[i] ^= mask_key[i % 4]
|
||||
self._sock.send(header + masked)
|
||||
else:
|
||||
self._sock.send(header)
|
||||
|
||||
def _recv_exact(self, n):
|
||||
buf = bytearray(n)
|
||||
pos = 0
|
||||
while pos < n:
|
||||
chunk = self._sock.recv(n - pos)
|
||||
if not chunk:
|
||||
return None
|
||||
buf[pos:pos + len(chunk)] = chunk
|
||||
pos += len(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._sock.send(bytearray([0x88, 0x80, 0, 0, 0, 0]))
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self._sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def ws_connect(url):
|
||||
if url.startswith("ws://"):
|
||||
rest = url[5:]
|
||||
else:
|
||||
raise ValueError("Only ws:// supported")
|
||||
if "/" in rest:
|
||||
host_port = rest.split("/", 1)[0]
|
||||
path = "/" + rest.split("/", 1)[1]
|
||||
else:
|
||||
host_port = rest
|
||||
path = "/"
|
||||
if ":" in host_port:
|
||||
host = host_port.split(":")[0]
|
||||
port = int(host_port.split(":")[1])
|
||||
else:
|
||||
host = host_port
|
||||
port = 80
|
||||
|
||||
addr = usocket.getaddrinfo(host, port)[0][-1]
|
||||
sock = usocket.socket()
|
||||
sock.connect(addr)
|
||||
sock.settimeout(15)
|
||||
|
||||
key = ubinascii.b2a_base64(os.urandom(16)).strip().decode()
|
||||
req = (
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"Host: %s\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: %s\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n"
|
||||
) % (path, host_port, key)
|
||||
|
||||
sock.send(req.encode())
|
||||
resp = b""
|
||||
while b"\r\n\r\n" not in resp:
|
||||
b = sock.recv(1)
|
||||
if not b:
|
||||
sock.close()
|
||||
raise Exception("Closed during handshake")
|
||||
resp += b
|
||||
|
||||
status_line = resp.split(b"\r\n")[0]
|
||||
if b"101" not in status_line:
|
||||
sock.close()
|
||||
raise Exception("Upgrade failed: " + status_line.decode())
|
||||
|
||||
return WSClient(sock)
|
||||
|
||||
# ==========================================
|
||||
# UI & UTILITY FUNCTIONS
|
||||
# ==========================================
|
||||
|
||||
_cur_face = None
|
||||
def set_face(face):
|
||||
global _cur_face
|
||||
if face and face != _cur_face:
|
||||
d_s(lcd, face, 80, 25, 10)
|
||||
_cur_face = face
|
||||
|
||||
def draw_status(status, color, face=None):
|
||||
lcd.fillRect(0, 220, 320, 20, 0x222222)
|
||||
lcd.print(status, int((320 - len(status) * 8) / 2), 220, color)
|
||||
if face:
|
||||
set_face(face)
|
||||
|
||||
lcd.print("Connecting to WiFi...", 0, 0, 0xFFFFFF)
|
||||
wlan = network.WLAN(network.STA_IF)
|
||||
wlan.active(True)
|
||||
wlan.connect(WIFI_SSID, WIFI_PASS)
|
||||
def connect_wifi():
|
||||
lcd.clear()
|
||||
lcd.print("Connecting to WiFi...", 0, 0, 0xFFFFFF)
|
||||
wlan = network.WLAN(network.STA_IF)
|
||||
wlan.active(True)
|
||||
if not wlan.isconnected():
|
||||
wlan.connect(WIFI_SSID, WIFI_PASS)
|
||||
|
||||
attempts = 0
|
||||
while not wlan.isconnected() and attempts < 20:
|
||||
time.sleep(0.5)
|
||||
attempts += 1
|
||||
|
||||
if wlan.isconnected():
|
||||
lcd.clear()
|
||||
lcd.print("WiFi Connected!", 0, 0, 0x00FF00)
|
||||
lcd.print(wlan.ifconfig()[0], 0, 20, 0x00FF00)
|
||||
time.sleep(1)
|
||||
lcd.clear()
|
||||
else:
|
||||
lcd.clear()
|
||||
lcd.print("WiFi Failed", 0, 0, 0xFF0000)
|
||||
time.sleep(2)
|
||||
lcd.clear()
|
||||
return wlan.isconnected()
|
||||
|
||||
# Simple connection loop
|
||||
attempts = 0
|
||||
while not wlan.isconnected() and attempts < 20:
|
||||
time.sleep(0.5)
|
||||
attempts += 1
|
||||
# ==========================================
|
||||
# AUDIO I/O
|
||||
# ==========================================
|
||||
|
||||
if wlan.isconnected():
|
||||
lcd.clear()
|
||||
lcd.print("WiFi Connected!", 0, 0, 0x00FF00)
|
||||
lcd.print(wlan.ifconfig()[0], 0, 20, 0x00FF00)
|
||||
time.sleep(1)
|
||||
lcd.clear()
|
||||
else:
|
||||
lcd.clear()
|
||||
lcd.print("WiFi Failed", 0, 0, 0xFF0000)
|
||||
time.sleep(2)
|
||||
lcd.clear()
|
||||
|
||||
try:
|
||||
adc = machine.ADC(34)
|
||||
adc.atten(machine.ADC.ATTN_11DB)
|
||||
except:
|
||||
def get_adc():
|
||||
try:
|
||||
adc = machine.ADC(machine.Pin(34))
|
||||
adc.atten(machine.ADC.ATTN_11DB)
|
||||
a = machine.ADC(34)
|
||||
a.atten(machine.ADC.ATTN_11DB)
|
||||
return a
|
||||
except:
|
||||
adc = None
|
||||
try:
|
||||
a = machine.ADC(machine.Pin(34))
|
||||
a.atten(machine.ADC.ATTN_11DB)
|
||||
return a
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_db():
|
||||
if not adc: return 30
|
||||
def get_db(adc_obj):
|
||||
if not adc_obj: return 30
|
||||
sum_v = 0
|
||||
sum_sq = 0
|
||||
count = 0
|
||||
end_t = time.ticks_ms() + 40
|
||||
while time.ticks_ms() < end_t:
|
||||
try:
|
||||
v = adc.read()
|
||||
v = adc_obj.read()
|
||||
sum_v += v
|
||||
sum_sq += v * v
|
||||
count += 1
|
||||
except:
|
||||
pass
|
||||
|
||||
if count == 0: return 30
|
||||
|
||||
mean = sum_v / count
|
||||
@@ -79,249 +283,293 @@ def get_db():
|
||||
amp = math.sqrt(variance)
|
||||
if amp <= 1: return 30
|
||||
|
||||
# Use your specific calculation formula to properly scale to human DB
|
||||
db = 20 * math.log10(amp) + 25
|
||||
return db
|
||||
# Scale to human DB
|
||||
return 20 * math.log10(amp) + 25
|
||||
|
||||
def init_mic():
|
||||
def init_manual_spk():
|
||||
try:
|
||||
if hasattr(machine.I2S, "RX"):
|
||||
audio_in = machine.I2S(
|
||||
0,
|
||||
sck=machine.Pin(0),
|
||||
ws=machine.Pin(0),
|
||||
sd=machine.Pin(34),
|
||||
mode=machine.I2S.RX,
|
||||
bits=16,
|
||||
format=machine.I2S.MONO,
|
||||
rate=16000,
|
||||
ibuf=4096
|
||||
)
|
||||
return audio_in
|
||||
else:
|
||||
# Fallback for old Micropython (e.g. M5Stack UIFlow)
|
||||
mode = getattr(machine.I2S, "MODE_MASTER", 1) | getattr(machine.I2S, "MODE_RX", 2)
|
||||
if hasattr(machine.I2S, "MODE_PDM"):
|
||||
mode |= getattr(machine.I2S, "MODE_PDM", 0)
|
||||
|
||||
cfmt = getattr(machine.I2S, "CHANNEL_FMT_ALL_LEFT", 1)
|
||||
dfmt = getattr(machine.I2S, "FORMAT_I2S", 1)
|
||||
|
||||
try_args = [
|
||||
([getattr(machine.I2S, "NUM0", 0), mode, 16000, 16, cfmt, dfmt], {}),
|
||||
([getattr(machine.I2S, "NUM0", 0), mode, 16000, 16], {}),
|
||||
([getattr(machine.I2S, "NUM0", 0)], {"mode": mode, "sample_rate": 16000, "bits": 16, "channel_format": cfmt, "data_format": dfmt}),
|
||||
([getattr(machine.I2S, "NUM0", 0)], {"mode": mode, "sample_rate": 16000, "bits": 16}),
|
||||
([getattr(machine.I2S, "NUM0", 0)], {"mode": mode, "bck": 0, "ws": 0, "sd": 34, "sample_rate": 16000, "bits": 16}),
|
||||
([], {"mode": mode, "sample_rate": 16000, "bits": 16}),
|
||||
([getattr(machine.I2S, "NUM0", 0)], {"mode": mode}),
|
||||
([], {"mode": mode}),
|
||||
]
|
||||
|
||||
audio_in = None
|
||||
last_e = None
|
||||
for args, kwargs in try_args:
|
||||
try:
|
||||
audio_in = machine.I2S(*args, **kwargs)
|
||||
break
|
||||
except Exception as e:
|
||||
last_e = e
|
||||
if audio_in is None:
|
||||
raise Exception("Mic fallback failed: " + str(last_e))
|
||||
return audio_in
|
||||
except Exception as e:
|
||||
print("Mic I2S Error:", repr(e))
|
||||
lcd.print("Mic Err: " + str(e)[:20], 0, 40, 0xFF0000)
|
||||
return None
|
||||
|
||||
def init_spk():
|
||||
try:
|
||||
if hasattr(machine.I2S, "TX"):
|
||||
audio_out = machine.I2S(
|
||||
1,
|
||||
sck=machine.Pin(12),
|
||||
ws=machine.Pin(0),
|
||||
sd=machine.Pin(25), # M5Stack Core/GO speaker is typically on Pin 25
|
||||
mode=machine.I2S.TX,
|
||||
bits=16,
|
||||
format=machine.I2S.MONO,
|
||||
rate=16000,
|
||||
ibuf=8192
|
||||
)
|
||||
return audio_out
|
||||
else:
|
||||
mode = getattr(machine.I2S, "MODE_MASTER", 1) | getattr(machine.I2S, "MODE_TX", 2)
|
||||
if hasattr(machine.I2S, "MODE_DAC_BUILT_IN"):
|
||||
mode |= machine.I2S.MODE_DAC_BUILT_IN
|
||||
|
||||
# M5GO Core speaker sits on DAC 1 (Pin 25)
|
||||
# Therefore we MUST strictly use DAC_BUILT_IN. Digital I2S on this pin is not supported!
|
||||
if hasattr(machine.I2S, "MODE_DAC_BUILT_IN"):
|
||||
mode = getattr(machine.I2S, "MODE_MASTER", 1) | getattr(machine.I2S, "MODE_TX", 2) | getattr(machine.I2S, "MODE_DAC_BUILT_IN", 0)
|
||||
cfmt = getattr(machine.I2S, "CHANNEL_FMT_RIGHT_LEFT", 1)
|
||||
dfmt = getattr(machine.I2S, "FORMAT_I2S_MSB", 1)
|
||||
|
||||
try_args = [
|
||||
([getattr(machine.I2S, "NUM1", 1), mode, 16000, 16, cfmt, dfmt], {}),
|
||||
([getattr(machine.I2S, "NUM1", 1), mode, 16000, 16], {}),
|
||||
([getattr(machine.I2S, "NUM1", 1)], {"mode": mode, "sample_rate": 16000, "bits": 16, "channel_format": cfmt, "data_format": dfmt}),
|
||||
([getattr(machine.I2S, "NUM1", 1)], {"mode": mode, "sample_rate": 16000, "bits": 16}),
|
||||
([getattr(machine.I2S, "NUM1", 1)], {"mode": mode, "bck": 12, "ws": 0, "sd": 25, "sample_rate": 16000, "bits": 16}),
|
||||
i2s_id = getattr(machine.I2S, "NUM0", 0)
|
||||
|
||||
# Different MicroPython versions vary wildly on kwarg vs positional I2S init structure
|
||||
try_sigs = [
|
||||
([i2s_id], {"mode": mode, "rate": 16000, "bits": 16, "format": cfmt, "ibuf": 2048}),
|
||||
([i2s_id, mode, 16000, 16, cfmt, dfmt], {}),
|
||||
([i2s_id, mode, 16000, 16], {}),
|
||||
([i2s_id], {"mode": mode, "sample_rate": 16000, "bits": 16, "channel_format": cfmt, "data_format": dfmt}),
|
||||
([i2s_id], {"mode": mode, "sample_rate": 16000, "bits": 16}),
|
||||
([], {"mode": mode, "sample_rate": 16000, "bits": 16}),
|
||||
([getattr(machine.I2S, "NUM1", 1)], {"mode": mode}),
|
||||
([], {"mode": mode}),
|
||||
([i2s_id], {"mode": mode, "rate": 16000, "bits": 16})
|
||||
]
|
||||
|
||||
audio_out = None
|
||||
last_e = None
|
||||
for args, kwargs in try_args:
|
||||
last_err = None
|
||||
for args, kwargs in try_sigs:
|
||||
try:
|
||||
audio_out = machine.I2S(*args, **kwargs)
|
||||
break
|
||||
return machine.I2S(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_e = e
|
||||
if audio_out is None:
|
||||
raise Exception("Spk fallback failed: " + str(last_e))
|
||||
return audio_out
|
||||
last_err = e
|
||||
|
||||
print("I2S Fallback err (exhausted):", last_err)
|
||||
return None
|
||||
else:
|
||||
print("ERR: DAC_BUILT_IN missing on this firmware")
|
||||
return None
|
||||
except Exception as e:
|
||||
print("Spk I2S Error:", repr(e))
|
||||
lcd.print("Spk Err: " + str(e)[:20], 0, 60, 0xFF0000)
|
||||
print("I2S Init err:", e)
|
||||
return None
|
||||
|
||||
def deinit_i2s(i2s_obj):
|
||||
if i2s_obj:
|
||||
try:
|
||||
if hasattr(i2s_obj, 'deinit'):
|
||||
i2s_obj.deinit()
|
||||
except Exception as e:
|
||||
print("I2S Deinit Error:", e)
|
||||
|
||||
ws = None
|
||||
|
||||
def connect_ws():
|
||||
global ws
|
||||
try:
|
||||
ws = websocket.WebSocket()
|
||||
ws.connect(WS_URL)
|
||||
return True
|
||||
except Exception as e:
|
||||
draw_status("WS Connection Error", 0xFF0000)
|
||||
def stream_http_audio(url):
|
||||
print("Streaming I2S direct from HTTP...")
|
||||
audio_out = init_manual_spk()
|
||||
if not audio_out:
|
||||
print("Speaker init failed, cannot play audio via DAC")
|
||||
return False
|
||||
|
||||
draw_status("Hold Button A to Talk", 0xFFFFFF)
|
||||
|
||||
buf = bytearray(1024)
|
||||
|
||||
# Noise monitoring variables
|
||||
l_db_s = ""
|
||||
s_db = 30.0
|
||||
|
||||
# Location settings - change this depending on where the M5GO is placed
|
||||
CURRENT_ROOM_ID = "mckeldin"
|
||||
CURRENT_LAT = 38.986021
|
||||
CURRENT_LNG = -76.944949
|
||||
|
||||
last_db_post_time = 0
|
||||
|
||||
while True:
|
||||
# m5stack core button check
|
||||
if btnA.isPressed():
|
||||
if not ws:
|
||||
draw_status("Connecting...", 0xFFFF00)
|
||||
if not connect_ws():
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
draw_status("Listening...", 0x0000FF)
|
||||
|
||||
# Read and send audio while button is held
|
||||
audio_in = init_mic()
|
||||
while btnA.isPressed():
|
||||
try:
|
||||
if audio_in:
|
||||
num_read = audio_in.readinto(buf)
|
||||
if num_read and num_read > 0 and ws:
|
||||
ws.send(buf[:num_read])
|
||||
except Exception as e:
|
||||
pass
|
||||
deinit_i2s(audio_in)
|
||||
audio_in = None
|
||||
|
||||
# Button released
|
||||
draw_status("Thinking...", 0xFFFF00)
|
||||
try:
|
||||
if ws:
|
||||
ws.send(json.dumps({"event": "stop_listening"}))
|
||||
|
||||
# Wait for response audio
|
||||
draw_status("Speaking...", 0x00FF00)
|
||||
audio_out = init_spk()
|
||||
while True:
|
||||
resp = ws.recv()
|
||||
if resp and isinstance(resp, bytes):
|
||||
if len(resp) == 0:
|
||||
break # End of audio transmission
|
||||
|
||||
# Briefly pause face animation updates while speaker is playing
|
||||
# to prevent dropping packets or stuttering
|
||||
if audio_out:
|
||||
audio_out.write(resp)
|
||||
else:
|
||||
break # Empty or non-bytes response means end
|
||||
deinit_i2s(audio_out)
|
||||
audio_out = None
|
||||
except Exception as e:
|
||||
draw_status("Error during playback", 0xFF0000)
|
||||
ws = None # force reconnect next time
|
||||
|
||||
draw_status("Hold Button A to Talk", 0xFFFFFF)
|
||||
|
||||
# ---------------------------------------------
|
||||
# NOISE MONITORING LOOP
|
||||
# ---------------------------------------------
|
||||
|
||||
r_db = get_db()
|
||||
s_db = (s_db * 0.8) + (r_db * 0.2)
|
||||
db = int(s_db)
|
||||
|
||||
if db < 40:
|
||||
i_c = 0x89b4fa
|
||||
elif db < 55:
|
||||
i_c = 0x94e2d5
|
||||
elif db < 65:
|
||||
i_c = 0xf9e2af
|
||||
elif db < 80:
|
||||
i_c = 0xfab387
|
||||
if url.startswith("http://"):
|
||||
rest = url[7:]
|
||||
else:
|
||||
i_c = 0xf38ba8
|
||||
|
||||
lcd.fillRect(0, 0, 320, 4, i_c)
|
||||
db_s = "Noise: %d dB" % db
|
||||
if db_s != l_db_s:
|
||||
lcd.fillRect(0, 4, 150, 15, 0x222222)
|
||||
lcd.print(db_s, 5, 4, i_c)
|
||||
l_db_s = db_s
|
||||
raise ValueError("Only http:// supported")
|
||||
|
||||
# ---------------------------------------------
|
||||
# POST DB DATA PERIODICALLY
|
||||
# ---------------------------------------------
|
||||
try:
|
||||
now_ms = time.ticks_ms()
|
||||
if time.ticks_diff(now_ms, last_db_post_time) > 15000: # Every 15 seconds
|
||||
last_db_post_time = now_ms
|
||||
try:
|
||||
payload = {
|
||||
"room_id": CURRENT_ROOM_ID,
|
||||
"location": {
|
||||
"type": "Point",
|
||||
"coordinates": [CURRENT_LNG, CURRENT_LAT]
|
||||
},
|
||||
"db": float(db)
|
||||
}
|
||||
# Convert ws url from ws://ip:port/ws/voice to http://ip:port/api/study-rooms
|
||||
http_url = WS_URL.replace("ws://", "http://").replace("/ws/voice", "/api/study-rooms")
|
||||
import urequests
|
||||
res = urequests.post(http_url, json=payload)
|
||||
res.close()
|
||||
except Exception as e:
|
||||
print("Failed to post db data:", e)
|
||||
except Exception as main_e:
|
||||
pass
|
||||
if "/" in rest:
|
||||
host_port = rest.split("/", 1)[0]
|
||||
path = "/" + rest.split("/", 1)[1]
|
||||
else:
|
||||
host_port = rest
|
||||
path = "/"
|
||||
|
||||
time.sleep(0.02)
|
||||
if ":" in host_port:
|
||||
host = host_port.split(":")[0]
|
||||
port = int(host_port.split(":")[1])
|
||||
else:
|
||||
host = host_port
|
||||
port = 80
|
||||
|
||||
addr = usocket.getaddrinfo(host, port)[0][-1]
|
||||
sock = usocket.socket()
|
||||
sock.connect(addr)
|
||||
sock.settimeout(30)
|
||||
|
||||
req = "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" % (path, host_port)
|
||||
sock.send(req.encode())
|
||||
|
||||
hdr = b""
|
||||
while b"\r\n\r\n" not in hdr:
|
||||
b = sock.recv(1)
|
||||
if not b:
|
||||
sock.close()
|
||||
return False
|
||||
hdr += b
|
||||
|
||||
# Read WAV header
|
||||
header_left = 44
|
||||
while header_left > 0:
|
||||
chunk = sock.recv(header_left)
|
||||
if not chunk: break
|
||||
header_left -= len(chunk)
|
||||
|
||||
in_buf = bytearray(1024)
|
||||
out_buf = bytearray(2048)
|
||||
|
||||
while True:
|
||||
n = 0
|
||||
while n < 1024:
|
||||
chunk = sock.recv(1024 - n)
|
||||
if not chunk: break
|
||||
in_buf[n:n+len(chunk)] = chunk
|
||||
n += len(chunk)
|
||||
|
||||
if n == 0: break
|
||||
|
||||
samples = n // 2
|
||||
for j in range(samples):
|
||||
idx = j * 2
|
||||
# Read 16-bit Signed LE
|
||||
s = in_buf[idx] | (in_buf[idx + 1] << 8)
|
||||
if s >= 32768: s -= 65536
|
||||
|
||||
# Convert to Unsigned + Center Offset for DAC
|
||||
u = (s + 32768) & 0xFFFF
|
||||
u_lo = u & 0xFF
|
||||
u_hi = u >> 8
|
||||
|
||||
# Map Stereo for built-in MSB
|
||||
o_idx = j * 4
|
||||
out_buf[o_idx] = u_lo
|
||||
out_buf[o_idx + 1] = u_hi
|
||||
out_buf[o_idx + 2] = u_lo
|
||||
out_buf[o_idx + 3] = u_hi
|
||||
|
||||
try:
|
||||
audio_out.write(out_buf[:samples * 4])
|
||||
except Exception as e:
|
||||
print("Write err:", e)
|
||||
break
|
||||
|
||||
sock.close()
|
||||
if hasattr(audio_out, 'deinit'): audio_out.deinit()
|
||||
return True
|
||||
|
||||
# ==========================================
|
||||
# MAIN LOOP
|
||||
# ==========================================
|
||||
|
||||
def run_main():
|
||||
if not connect_wifi():
|
||||
return
|
||||
|
||||
adc = get_adc()
|
||||
ws = None
|
||||
l_db_s = ""
|
||||
s_db = 30.0
|
||||
last_db_post_time = time.ticks_ms()
|
||||
|
||||
draw_status("Hold Button A to Talk", 0xFFFFFF, f_s_o)
|
||||
|
||||
def maintain_ws():
|
||||
nonlocal ws
|
||||
if not ws:
|
||||
draw_status("Connecting...", 0xFFFF00, f_t_2)
|
||||
try:
|
||||
ws = ws_connect(WS_URL)
|
||||
draw_status("Hold Button A to Talk", 0xFFFFFF, f_s_o)
|
||||
except Exception as e:
|
||||
print("WS Err:", e)
|
||||
ws = None
|
||||
return ws is not None
|
||||
|
||||
while True:
|
||||
gc.collect()
|
||||
# --- Voice Interaction ---
|
||||
if btnA.isPressed():
|
||||
if maintain_ws():
|
||||
draw_status("Listening...", 0x0000FF, f_listen)
|
||||
|
||||
send_buf = bytearray(AUDIO_CHUNK_SIZE)
|
||||
buf_pos = 0
|
||||
total_samples = 0
|
||||
|
||||
rec_start_us = time.ticks_us()
|
||||
|
||||
# Fastest possible analog capture loop
|
||||
while btnA.isPressed():
|
||||
try:
|
||||
raw = adc.read() if adc else 2048
|
||||
sample = (raw - 2048) * 16
|
||||
# Fast clamp
|
||||
if sample > 32767: sample = 32767
|
||||
elif sample < -32768: sample = -32768
|
||||
|
||||
send_buf[buf_pos] = sample & 0xFF
|
||||
send_buf[buf_pos + 1] = (sample >> 8) & 0xFF
|
||||
buf_pos += 2
|
||||
total_samples += 1
|
||||
|
||||
if buf_pos >= AUDIO_CHUNK_SIZE:
|
||||
if ws: ws.send(bytes(send_buf))
|
||||
buf_pos = 0
|
||||
except Exception as e:
|
||||
print("Rec Err:", e)
|
||||
break
|
||||
|
||||
if buf_pos > 0 and ws:
|
||||
try:
|
||||
ws.send(bytes(send_buf[:buf_pos]))
|
||||
except:
|
||||
pass
|
||||
|
||||
draw_status("Thinking...", 0xFFFF00, f_t_1)
|
||||
actual_rate = (total_samples * 1000000) // time.ticks_diff(time.ticks_us(), rec_start_us)
|
||||
print("Captured at", actual_rate, "Hz")
|
||||
|
||||
try:
|
||||
ws.send(json.dumps({"event": "stop_listening", "sample_rate": actual_rate}))
|
||||
|
||||
# Wait for TTS ready
|
||||
resp = ws.recv()
|
||||
if resp and isinstance(resp, str):
|
||||
msg = json.loads(resp)
|
||||
if msg.get("event") == "tts_ready":
|
||||
# Start direct TCP stream immediately
|
||||
draw_status("Speaking...", 0x00FF00, f_speak)
|
||||
http_base = WS_URL.replace("ws://", "http://").replace("/ws/voice", "")
|
||||
audio_url = http_base + "/api/tts-audio"
|
||||
|
||||
try:
|
||||
stream_http_audio(audio_url)
|
||||
except Exception as e:
|
||||
print("Stream Err:", e)
|
||||
draw_status("Play Failed", 0xFF0000, f_s_c)
|
||||
time.sleep(1)
|
||||
|
||||
elif msg.get("event") == "error":
|
||||
draw_status("Error: " + msg.get("msg", "")[:10], 0xFF0000, f_s_c)
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
print("Comm Err:", e)
|
||||
try: ws.close()
|
||||
except: pass
|
||||
ws = None
|
||||
|
||||
draw_status("Hold Button A to Talk", 0xFFFFFF, f_s_o)
|
||||
|
||||
# --- Noise Monitoring ---
|
||||
r_db = get_db(adc)
|
||||
s_db = (s_db * 0.8) + (r_db * 0.2)
|
||||
db_val = int(s_db)
|
||||
|
||||
i_c = 0x89b4fa if db_val < 40 else (0x94e2d5 if db_val < 55 else (0xf9e2af if db_val < 65 else (0xfab387 if db_val < 80 else 0xf38ba8)))
|
||||
lcd.fillRect(0, 0, 320, 4, i_c)
|
||||
db_s = "Noise: %d dB" % db_val
|
||||
if db_s != l_db_s:
|
||||
lcd.fillRect(0, 4, 150, 15, 0x222222)
|
||||
lcd.print(db_s, 5, 4, i_c)
|
||||
l_db_s = db_s
|
||||
|
||||
# Periodic DB Posting
|
||||
try:
|
||||
now_ms = time.ticks_ms()
|
||||
if time.ticks_diff(now_ms, last_db_post_time) > 15000:
|
||||
last_db_post_time = now_ms
|
||||
payload_str = '{"room_id":"%s","location":{"type":"Point","coordinates":[%s,%s]},"db":%s}' % (CURRENT_ROOM_ID, CURRENT_LNG, CURRENT_LAT, db_val)
|
||||
http_url = WS_URL.replace("ws://", "http://").replace("/ws/voice", "/api/study-rooms")
|
||||
|
||||
# Raw socket post
|
||||
h_p = http_url.split("://")[1].split("/")[0]
|
||||
p_th = "/" + http_url.split("://")[1].split("/", 1)[1] if "/" in http_url.split("://")[1] else "/"
|
||||
h_b = h_p.split(":")[0]
|
||||
p_r = int(h_p.split(":")[1]) if ":" in h_p else 80
|
||||
addr = usocket.getaddrinfo(h_b, p_r)[0][-1]
|
||||
s = usocket.socket()
|
||||
s.settimeout(5)
|
||||
s.connect(addr)
|
||||
req = "POST %s HTTP/1.0\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s" % (p_th, h_p, len(payload_str), payload_str)
|
||||
s.send(req.encode())
|
||||
s.close()
|
||||
del s, req, payload_str
|
||||
except:
|
||||
pass
|
||||
|
||||
# Idle Face Blinking logic
|
||||
if db_val > 67:
|
||||
set_face(f_angry)
|
||||
else:
|
||||
t = time.ticks_ms() % 5000
|
||||
if t < 200:
|
||||
set_face(f_s_c)
|
||||
else:
|
||||
set_face(f_s_o)
|
||||
|
||||
time.sleep(0.02)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run_main()
|
||||
except Exception as e:
|
||||
print("Fatal error:", e)
|
||||
lcd.print("Error: " + str(e), 0, 100, 0xFF0000)
|
||||
|
||||
Reference in New Issue
Block a user