Initial EVS client + bridge setup
This commit is contained in:
327
src/main.cpp
Normal file
327
src/main.cpp
Normal file
@@ -0,0 +1,327 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoWebsockets.h>
|
||||
#include "driver/i2s.h"
|
||||
#include "secrets.h"
|
||||
using namespace websockets;
|
||||
|
||||
// ---------------------------
|
||||
// Project config
|
||||
// ---------------------------
|
||||
static constexpr i2s_port_t MIC_I2S_PORT = I2S_NUM_0;
|
||||
static constexpr uint32_t MIC_SAMPLE_RATE = 16000;
|
||||
static constexpr i2s_bits_per_sample_t MIC_BITS = I2S_BITS_PER_SAMPLE_32BIT;
|
||||
static constexpr size_t MIC_FRAME_SAMPLES = 256;
|
||||
|
||||
// INMP441 -> ESP32
|
||||
static constexpr int PIN_I2S_WS = 25;
|
||||
static constexpr int PIN_I2S_SCK = 26;
|
||||
static constexpr int PIN_I2S_SD = 33;
|
||||
|
||||
// ESP32 PWM -> PAM8403 IN (L+ or R+)
|
||||
static constexpr int PIN_AUDIO_OUT = 27;
|
||||
static constexpr int PWM_CHANNEL = 0;
|
||||
static constexpr uint32_t PWM_FREQ = 22050;
|
||||
static constexpr uint8_t PWM_RES_BITS = 8;
|
||||
static constexpr uint32_t SPEAKER_SAMPLE_RATE = 16000;
|
||||
|
||||
// WiFi / WebSocket
|
||||
|
||||
enum class DeviceMode : uint8_t {
|
||||
Idle,
|
||||
StreamToServer, // Placeholder: ship PCM to remote STT/LLM/TTS service
|
||||
LocalLoopback, // Debug mode: mic directly to speaker
|
||||
};
|
||||
|
||||
static DeviceMode g_mode = DeviceMode::Idle;
|
||||
static int32_t g_micBuffer[MIC_FRAME_SAMPLES];
|
||||
static WebsocketsClient g_ws;
|
||||
static bool g_wsConnected = false;
|
||||
static uint32_t g_lastConnectTryMs = 0;
|
||||
static uint32_t g_nextOutUs = 0;
|
||||
static bool g_streamingActive = false;
|
||||
|
||||
static constexpr size_t RX_SAMPLES_CAP = 16000;
|
||||
static int16_t g_rxSamples[RX_SAMPLES_CAP];
|
||||
static size_t g_rxHead = 0;
|
||||
static size_t g_rxTail = 0;
|
||||
static size_t g_rxCount = 0;
|
||||
|
||||
static bool initMicI2s() {
|
||||
const i2s_config_t i2sConfig = {
|
||||
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
|
||||
.sample_rate = MIC_SAMPLE_RATE,
|
||||
.bits_per_sample = MIC_BITS,
|
||||
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = 0,
|
||||
.dma_buf_count = 8,
|
||||
.dma_buf_len = 256,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = false,
|
||||
.fixed_mclk = 0,
|
||||
};
|
||||
|
||||
const i2s_pin_config_t pinConfig = {
|
||||
.bck_io_num = PIN_I2S_SCK,
|
||||
.ws_io_num = PIN_I2S_WS,
|
||||
.data_out_num = I2S_PIN_NO_CHANGE,
|
||||
.data_in_num = PIN_I2S_SD,
|
||||
};
|
||||
|
||||
if (i2s_driver_install(MIC_I2S_PORT, &i2sConfig, 0, nullptr) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
if (i2s_set_pin(MIC_I2S_PORT, &pinConfig) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool initPwmOut() {
|
||||
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
||||
return ledcAttach(PIN_AUDIO_OUT, PWM_FREQ, PWM_RES_BITS);
|
||||
#else
|
||||
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RES_BITS);
|
||||
ledcAttachPin(PIN_AUDIO_OUT, PWM_CHANNEL);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void pwmWrite(uint8_t value) {
|
||||
#if ESP_ARDUINO_VERSION_MAJOR >= 3
|
||||
ledcWrite(PIN_AUDIO_OUT, value);
|
||||
#else
|
||||
ledcWrite(PWM_CHANNEL, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Convert signed 16-bit PCM to unsigned 8-bit PWM domain.
|
||||
static inline uint8_t pcm16ToPwm8(int16_t s) {
|
||||
return (uint16_t)(s + 32768) >> 8;
|
||||
}
|
||||
|
||||
static bool enqueuePcmSample(int16_t s) {
|
||||
if (g_rxCount >= RX_SAMPLES_CAP) {
|
||||
return false;
|
||||
}
|
||||
g_rxSamples[g_rxTail] = s;
|
||||
g_rxTail = (g_rxTail + 1) % RX_SAMPLES_CAP;
|
||||
++g_rxCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool dequeuePcmSample(int16_t& out) {
|
||||
if (g_rxCount == 0) {
|
||||
return false;
|
||||
}
|
||||
out = g_rxSamples[g_rxHead];
|
||||
g_rxHead = (g_rxHead + 1) % RX_SAMPLES_CAP;
|
||||
--g_rxCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void enqueuePcmFrame(const int16_t* frame, size_t count) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (!enqueuePcmSample(frame[i])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onWsMessageCallback(WebsocketsMessage message) {
|
||||
if (!message.isBinary()) {
|
||||
return;
|
||||
}
|
||||
const String payload = message.data();
|
||||
const size_t n = payload.length();
|
||||
if (n < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(payload.c_str());
|
||||
const size_t sampleCount = n / 2;
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
const uint8_t lo = bytes[2 * i];
|
||||
const uint8_t hi = bytes[2 * i + 1];
|
||||
const int16_t s = (int16_t)((uint16_t)hi << 8 | lo);
|
||||
if (!enqueuePcmSample(s)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onWsEventCallback(WebsocketsEvent event, String) {
|
||||
if (event == WebsocketsEvent::ConnectionOpened) {
|
||||
g_wsConnected = true;
|
||||
if (g_mode == DeviceMode::StreamToServer && !g_streamingActive) {
|
||||
g_ws.send("{\"type\":\"start\"}");
|
||||
g_streamingActive = true;
|
||||
}
|
||||
Serial.println("WS connected");
|
||||
} else if (event == WebsocketsEvent::ConnectionClosed) {
|
||||
g_wsConnected = false;
|
||||
g_streamingActive = false;
|
||||
Serial.println("WS disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
static String makeWsUrl() {
|
||||
String url = "ws://";
|
||||
url += EVS_BRIDGE_HOST;
|
||||
url += ":";
|
||||
url += String(EVS_WS_PORT);
|
||||
url += EVS_WS_PATH;
|
||||
url += "?device_id=";
|
||||
url += EVS_DEVICE_ID;
|
||||
return url;
|
||||
}
|
||||
|
||||
static void ensureConnectivity() {
|
||||
const uint32_t now = millis();
|
||||
if ((now - g_lastConnectTryMs) < EVS_RECONNECT_MS) {
|
||||
return;
|
||||
}
|
||||
g_lastConnectTryMs = now;
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("WiFi connecting...");
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_wsConnected) {
|
||||
const String url = makeWsUrl();
|
||||
Serial.print("WS connecting: ");
|
||||
Serial.println(url);
|
||||
g_ws.connect(url);
|
||||
}
|
||||
}
|
||||
|
||||
static void setMode(DeviceMode mode) {
|
||||
if (g_mode == mode) {
|
||||
return;
|
||||
}
|
||||
if (g_mode == DeviceMode::StreamToServer && g_wsConnected && g_streamingActive) {
|
||||
g_ws.send("{\"type\":\"stop\"}");
|
||||
g_streamingActive = false;
|
||||
}
|
||||
g_mode = mode;
|
||||
if (g_mode == DeviceMode::StreamToServer && g_wsConnected && !g_streamingActive) {
|
||||
g_ws.send("{\"type\":\"start\"}");
|
||||
g_streamingActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Send PCM16 mono frame to server.
|
||||
static void handleFrameForServer(const int16_t* frame, size_t count) {
|
||||
if (!g_wsConnected || count == 0) {
|
||||
return;
|
||||
}
|
||||
g_ws.sendBinary(reinterpret_cast<const char*>(frame), count * sizeof(int16_t));
|
||||
}
|
||||
|
||||
static void serviceSpeaker() {
|
||||
const uint32_t periodUs = 1000000UL / SPEAKER_SAMPLE_RATE;
|
||||
const uint32_t now = micros();
|
||||
if ((int32_t)(now - g_nextOutUs) < 0) {
|
||||
return;
|
||||
}
|
||||
g_nextOutUs += periodUs;
|
||||
|
||||
int16_t s = 0;
|
||||
if (dequeuePcmSample(s)) {
|
||||
pwmWrite(pcm16ToPwm8(s));
|
||||
} else {
|
||||
pwmWrite(128);
|
||||
}
|
||||
}
|
||||
|
||||
static void printHelp() {
|
||||
Serial.println();
|
||||
Serial.println("Commands:");
|
||||
Serial.println(" i = idle");
|
||||
Serial.println(" s = stream mode (stub)");
|
||||
Serial.println(" l = local loopback mode");
|
||||
Serial.println(" p = print network status");
|
||||
Serial.println(" h = help");
|
||||
}
|
||||
|
||||
static void handleSerialCommands() {
|
||||
while (Serial.available()) {
|
||||
const char c = (char)Serial.read();
|
||||
if (c == 'i') {
|
||||
setMode(DeviceMode::Idle);
|
||||
Serial.println("Mode -> Idle");
|
||||
} else if (c == 's') {
|
||||
setMode(DeviceMode::StreamToServer);
|
||||
Serial.println("Mode -> StreamToServer");
|
||||
} else if (c == 'l') {
|
||||
setMode(DeviceMode::LocalLoopback);
|
||||
Serial.println("Mode -> LocalLoopback");
|
||||
} else if (c == 'p') {
|
||||
Serial.print("WiFi: ");
|
||||
Serial.print((WiFi.status() == WL_CONNECTED) ? "connected " : "disconnected ");
|
||||
Serial.print("IP=");
|
||||
Serial.println(WiFi.localIP());
|
||||
Serial.print("WS: ");
|
||||
Serial.println(g_wsConnected ? "connected" : "disconnected");
|
||||
} else if (c == 'h') {
|
||||
printHelp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
Serial.println("EVS Client boot");
|
||||
|
||||
if (!initMicI2s()) {
|
||||
Serial.println("ERROR: I2S init failed");
|
||||
while (true) delay(1000);
|
||||
}
|
||||
if (!initPwmOut()) {
|
||||
Serial.println("ERROR: PWM init failed");
|
||||
while (true) delay(1000);
|
||||
}
|
||||
|
||||
g_ws.onMessage(onWsMessageCallback);
|
||||
g_ws.onEvent(onWsEventCallback);
|
||||
g_nextOutUs = micros();
|
||||
|
||||
setMode(DeviceMode::LocalLoopback);
|
||||
Serial.println("Audio init ok");
|
||||
Serial.println("Set local environment values in include/secrets.h");
|
||||
printHelp();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
handleSerialCommands();
|
||||
ensureConnectivity();
|
||||
g_ws.poll();
|
||||
serviceSpeaker();
|
||||
|
||||
size_t bytesRead = 0;
|
||||
const esp_err_t res = i2s_read(
|
||||
MIC_I2S_PORT, g_micBuffer, sizeof(g_micBuffer), &bytesRead, 0);
|
||||
if (res != ESP_OK || bytesRead == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t sampleCount = bytesRead / sizeof(int32_t);
|
||||
static int16_t pcm16[MIC_FRAME_SAMPLES];
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
// INMP441 delivers meaningful data in the high bits for 32-bit slot formats.
|
||||
pcm16[i] = (int16_t)(g_micBuffer[i] >> 14);
|
||||
}
|
||||
|
||||
if (g_mode == DeviceMode::StreamToServer) {
|
||||
handleFrameForServer(pcm16, sampleCount);
|
||||
} else if (g_mode == DeviceMode::LocalLoopback) {
|
||||
enqueuePcmFrame(pcm16, sampleCount);
|
||||
} else {
|
||||
// idle
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user