Add EVS control portal, io_mode switching, and DAC-only speaker path
Some checks failed
Build and Push EVS Bridge Image / docker (push) Has been cancelled

This commit is contained in:
Kai
2026-02-15 13:16:41 +01:00
parent 179440858b
commit 04c59c3b25
13 changed files with 1257 additions and 89 deletions

View File

@@ -1,15 +1,36 @@
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ArduinoWebsockets.h>
#include <PubSubClient.h>
#include <math.h>
#include "driver/i2s.h"
#include "secrets.h"
using namespace websockets;
#ifndef EVS_ENABLE_MIC
#define EVS_ENABLE_MIC 1
#endif
#ifndef EVS_ENABLE_SPEAKER
#define EVS_ENABLE_SPEAKER 1
#endif
#ifndef EVS_DEFAULT_IO_MODE
#define EVS_DEFAULT_IO_MODE "mic"
#endif
#ifndef EVS_SPK_DAC_PIN
#define EVS_SPK_DAC_PIN 25
#endif
static constexpr bool kSerialCommandEcho = EVS_SERIAL_COMMAND_ECHO;
static constexpr bool kMicUseRightChannel = EVS_MIC_USE_RIGHT_CHANNEL;
static constexpr int kMicS24ToS16Shift = EVS_MIC_S24_TO_S16_SHIFT;
static constexpr float kMicGain = EVS_MIC_GAIN;
static constexpr bool kMicHardwareAvailable = (EVS_ENABLE_MIC != 0);
static constexpr bool kSpeakerHardwareAvailable = (EVS_ENABLE_SPEAKER != 0);
static constexpr float MIC_GAIN_MIN = 0.1f;
static constexpr float MIC_GAIN_MAX = 8.0f;
// ---------------------------
// Project config
@@ -24,14 +45,14 @@ 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;
// ESP32 DAC -> amplifier IN (L+ or R+)
static constexpr uint32_t SPEAKER_SAMPLE_RATE = 16000;
static constexpr uint32_t MIC_TELEMETRY_INTERVAL_MS = 1000;
static constexpr float PI_F = 3.14159265358979323846f;
static constexpr uint32_t MQTT_RECONNECT_MS = 5000;
static constexpr uint16_t MQTT_BUFFER_SIZE = 512;
static constexpr uint16_t UDP_MAX_PACKET_BYTES = MIC_FRAME_SAMPLES * sizeof(int16_t);
static constexpr uint32_t UDP_STATUS_INTERVAL_MS = 2000;
// WiFi / WebSocket
@@ -40,7 +61,13 @@ enum class DeviceMode : uint8_t {
StreamToServer, // Ship PCM to remote STT/LLM/TTS service
};
enum class IoMode : uint8_t {
Mic,
Speaker,
};
static DeviceMode g_mode = DeviceMode::Idle;
static IoMode g_ioMode = IoMode::Mic;
static int32_t g_micBuffer[MIC_FRAME_SAMPLES];
static WebsocketsClient g_ws;
static bool g_wsConnected = false;
@@ -48,6 +75,22 @@ static uint32_t g_lastConnectTryMs = 0;
static uint32_t g_nextOutUs = 0;
static bool g_streamingActive = false;
static uint32_t g_lastMicTelemetryMs = 0;
static WiFiClient g_mqttNet;
static PubSubClient g_mqtt(g_mqttNet);
static bool g_mqttConnected = false;
static uint32_t g_lastMqttConnectTryMs = 0;
static WiFiUDP g_udp;
static bool g_udpEnabled = false;
static String g_udpHost;
static uint16_t g_udpPort = 0;
static IPAddress g_udpIp;
static bool g_udpIpValid = false;
static uint32_t g_udpPacketsSent = 0;
static float g_micGain = EVS_MIC_GAIN;
static String g_mqttCmdTopic;
static String g_mqttStatusTopic;
static String g_mqttUdpStatusTopic;
static uint32_t g_lastUdpStatusMs = 0;
static constexpr size_t RX_SAMPLES_CAP = 16000;
static int16_t g_rxSamples[RX_SAMPLES_CAP];
@@ -56,8 +99,22 @@ static size_t g_rxTail = 0;
static size_t g_rxCount = 0;
static void setMode(DeviceMode mode);
static void setIoMode(IoMode mode);
static void publishClientStatus();
static void publishUdpStatus(bool force = false);
static void handleUdpCommand(const String& msg);
static bool parseJsonStringField(const String& msg, const char* key, String& out);
static bool parseJsonIntField(const String& msg, const char* key, int& out);
static bool parseJsonBoolField(const String& msg, const char* key, bool& out);
static bool parseJsonFloatField(const String& msg, const char* key, float& out);
static bool isMicModeActive() { return kMicHardwareAvailable && g_ioMode == IoMode::Mic; }
static bool isSpeakerModeActive() { return kSpeakerHardwareAvailable && g_ioMode == IoMode::Speaker; }
static bool isDacPinValid(int pin) { return pin == 25 || pin == 26; }
static bool initMicI2s() {
if (!kMicHardwareAvailable) {
return true;
}
const i2s_config_t i2sConfig = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = MIC_SAMPLE_RATE,
@@ -88,26 +145,25 @@ static bool initMicI2s() {
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 bool initSpeakerOut() {
if (!kSpeakerHardwareAvailable) {
return true;
}
return isDacPinValid(EVS_SPK_DAC_PIN);
}
static inline void pwmWrite(uint8_t value) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcWrite(PIN_AUDIO_OUT, value);
#else
ledcWrite(PWM_CHANNEL, value);
#endif
static inline void speakerWriteU8(uint8_t value) {
if (!kSpeakerHardwareAvailable) {
return;
}
if (!isDacPinValid(EVS_SPK_DAC_PIN)) {
return;
}
dacWrite(EVS_SPK_DAC_PIN, value);
}
// Convert signed 16-bit PCM to unsigned 8-bit PWM domain.
static inline uint8_t pcm16ToPwm8(int16_t s) {
// Convert signed 16-bit PCM to unsigned 8-bit DAC domain.
static inline uint8_t pcm16ToU8(int16_t s) {
return (uint16_t)(s + 32768) >> 8;
}
@@ -132,6 +188,9 @@ static bool dequeuePcmSample(int16_t& out) {
}
static void enqueuePcmFrame(const int16_t* frame, size_t count) {
if (!kSpeakerHardwareAvailable) {
return;
}
for (size_t i = 0; i < count; ++i) {
if (!enqueuePcmSample(frame[i])) {
break;
@@ -140,6 +199,9 @@ static void enqueuePcmFrame(const int16_t* frame, size_t count) {
}
static void enqueueTone(uint16_t freqHz, uint16_t durationMs, int16_t amplitude) {
if (!kSpeakerHardwareAvailable) {
return;
}
if (freqHz == 0 || durationMs == 0) {
return;
}
@@ -160,6 +222,9 @@ static void enqueueTone(uint16_t freqHz, uint16_t durationMs, int16_t amplitude)
}
static void enqueueSilenceMs(uint16_t durationMs) {
if (!kSpeakerHardwareAvailable) {
return;
}
const uint32_t sampleCount = (uint32_t)SPEAKER_SAMPLE_RATE * durationMs / 1000U;
for (uint32_t i = 0; i < sampleCount; ++i) {
if (!enqueuePcmSample(0)) {
@@ -183,6 +248,9 @@ static void playStopTone() {
}
static void onWsMessageCallback(WebsocketsMessage message) {
if (!isSpeakerModeActive()) {
return;
}
if (!message.isBinary()) {
return;
}
@@ -207,9 +275,14 @@ static void onWsMessageCallback(WebsocketsMessage message) {
static void onWsEventCallback(WebsocketsEvent event, String) {
if (event == WebsocketsEvent::ConnectionOpened) {
g_wsConnected = true;
// Connection-driven mode: always stream on connect.
setMode(DeviceMode::StreamToServer);
// Mic-mode clients stream immediately on connect.
if (isMicModeActive()) {
setMode(DeviceMode::StreamToServer);
} else {
setMode(DeviceMode::Idle);
}
Serial.println("WS connected");
publishClientStatus();
} else if (event == WebsocketsEvent::ConnectionClosed) {
const bool wasStreaming = g_streamingActive;
g_wsConnected = false;
@@ -220,6 +293,7 @@ static void onWsEventCallback(WebsocketsEvent event, String) {
playStopTone();
}
Serial.println("WS disconnected");
publishClientStatus();
}
}
@@ -256,20 +330,382 @@ static void ensureConnectivity() {
}
}
static bool resolveUdpHost() {
if (g_udpHost.length() == 0 || g_udpPort == 0) {
g_udpIpValid = false;
return false;
}
// Fast path for numeric IPv4 literals to avoid DNS dependency.
if (g_udpIp.fromString(g_udpHost)) {
g_udpIpValid = true;
return true;
}
if (WiFi.hostByName(g_udpHost.c_str(), g_udpIp)) {
g_udpIpValid = true;
return true;
}
g_udpIpValid = false;
return false;
}
static String mqttClientId() {
String id = "evs-client-";
id += EVS_DEVICE_ID;
return id;
}
static void mqttMessageCallback(char* topic, uint8_t* payload, unsigned int length) {
String msg;
msg.reserve(length);
for (unsigned int i = 0; i < length; ++i) msg += (char)payload[i];
Serial.print("MQTT cmd [");
Serial.print(topic);
Serial.print("]: ");
Serial.println(msg);
String cmd;
if (!parseJsonStringField(msg, "cmd", cmd)) {
return;
}
cmd.toLowerCase();
if (cmd == "status") {
publishClientStatus();
return;
}
if (cmd == "mode") {
String value;
if (parseJsonStringField(msg, "value", value)) {
value.toLowerCase();
if (value == "idle") {
setMode(DeviceMode::Idle);
} else if (value == "stream" || value == "streamtoserver") {
setMode(DeviceMode::StreamToServer);
}
publishClientStatus();
}
return;
}
if (cmd == "io_mode") {
String value;
if (parseJsonStringField(msg, "value", value)) {
value.toLowerCase();
if (value == "mic") {
setIoMode(IoMode::Mic);
} else if (value == "spk" || value == "speaker") {
setIoMode(IoMode::Speaker);
}
publishClientStatus();
publishUdpStatus(true);
}
return;
}
if (cmd == "udp_stream") {
if (!isMicModeActive()) {
g_udpEnabled = false;
g_udpIpValid = false;
Serial.println("UDP stream ignored: io_mode is not mic");
publishUdpStatus(true);
publishClientStatus();
return;
}
handleUdpCommand(msg);
publishClientStatus();
return;
}
if (cmd == "mic_gain") {
if (!isMicModeActive()) {
Serial.println("MIC gain command ignored: io_mode is not mic");
return;
}
float value = 0.0f;
bool changed = false;
if (parseJsonFloatField(msg, "value", value)) {
g_micGain = value;
changed = true;
}
float delta = 0.0f;
if (parseJsonFloatField(msg, "delta", delta)) {
g_micGain += delta;
changed = true;
}
String action;
if (parseJsonStringField(msg, "action", action)) {
action.toLowerCase();
float step = 0.1f;
parseJsonFloatField(msg, "step", step);
if (step < 0.0f) step = -step;
if (action == "up" || action == "inc" || action == "increase") {
g_micGain += step;
changed = true;
} else if (action == "down" || action == "dec" || action == "decrease") {
g_micGain -= step;
changed = true;
}
}
if (g_micGain < MIC_GAIN_MIN) g_micGain = MIC_GAIN_MIN;
if (g_micGain > MIC_GAIN_MAX) g_micGain = MIC_GAIN_MAX;
if (changed) {
Serial.print("MIC gain set to ");
Serial.println(g_micGain, 3);
}
publishClientStatus();
return;
}
}
static void ensureMqttConnectivity() {
if (WiFi.status() != WL_CONNECTED) return;
if (g_mqttConnected && g_mqtt.connected()) return;
const uint32_t now = millis();
if ((now - g_lastMqttConnectTryMs) < MQTT_RECONNECT_MS) return;
g_lastMqttConnectTryMs = now;
g_mqtt.setServer(EVS_MQTT_HOST, EVS_MQTT_PORT);
g_mqtt.setCallback(mqttMessageCallback);
String clientId = mqttClientId();
bool ok = false;
if (strlen(EVS_MQTT_USER) > 0) {
ok = g_mqtt.connect(clientId.c_str(), EVS_MQTT_USER, EVS_MQTT_PASSWORD);
} else {
ok = g_mqtt.connect(clientId.c_str());
}
g_mqttConnected = ok;
if (!ok) {
Serial.print("MQTT connect failed, state=");
Serial.println(g_mqtt.state());
return;
}
Serial.println("MQTT connected");
g_mqtt.subscribe(g_mqttCmdTopic.c_str());
publishClientStatus();
}
static void setMode(DeviceMode mode) {
if (g_mode == mode) {
return;
}
if (g_mode == DeviceMode::StreamToServer && g_wsConnected && g_streamingActive) {
if (isMicModeActive() && g_mode == DeviceMode::StreamToServer && g_wsConnected && g_streamingActive) {
g_ws.send("{\"type\":\"stop\"}");
g_streamingActive = false;
playStopTone();
if (isSpeakerModeActive()) playStopTone();
}
g_mode = mode;
if (g_mode == DeviceMode::StreamToServer && g_wsConnected && !g_streamingActive) {
if (isMicModeActive() && g_mode == DeviceMode::StreamToServer && g_wsConnected && !g_streamingActive) {
g_ws.send("{\"type\":\"start\"}");
g_streamingActive = true;
playStartTone();
if (isSpeakerModeActive()) playStartTone();
}
}
static void setIoMode(IoMode mode) {
if (mode == g_ioMode) {
return;
}
if (mode == IoMode::Mic && !kMicHardwareAvailable) {
Serial.println("io_mode mic rejected: mic hardware disabled");
return;
}
if (mode == IoMode::Speaker && !kSpeakerHardwareAvailable) {
Serial.println("io_mode spk rejected: speaker hardware disabled");
return;
}
if (g_streamingActive && g_wsConnected) {
g_ws.send("{\"type\":\"stop\"}");
g_streamingActive = false;
}
g_udpEnabled = false;
g_udpIpValid = false;
g_udpPacketsSent = 0;
g_ioMode = mode;
g_rxHead = g_rxTail = g_rxCount = 0;
if (isMicModeActive() && g_wsConnected) {
setMode(DeviceMode::StreamToServer);
} else {
setMode(DeviceMode::Idle);
}
Serial.print("io_mode set to ");
Serial.println(g_ioMode == IoMode::Mic ? "mic" : "spk");
}
static bool parseJsonStringField(const String& msg, const char* key, String& out) {
String pattern = "\"";
pattern += key;
pattern += "\"";
int k = msg.indexOf(pattern);
if (k < 0) return false;
int colon = msg.indexOf(':', k + pattern.length());
if (colon < 0) return false;
int q1 = msg.indexOf('"', colon + 1);
if (q1 < 0) return false;
int q2 = msg.indexOf('"', q1 + 1);
if (q2 < 0) return false;
out = msg.substring(q1 + 1, q2);
return true;
}
static bool parseJsonIntField(const String& msg, const char* key, int& out) {
String pattern = "\"";
pattern += key;
pattern += "\"";
int k = msg.indexOf(pattern);
if (k < 0) return false;
int colon = msg.indexOf(':', k + pattern.length());
if (colon < 0) return false;
int start = colon + 1;
while (start < (int)msg.length() && (msg[start] == ' ' || msg[start] == '\t')) start++;
int end = start;
while (end < (int)msg.length() && isDigit(msg[end])) end++;
if (end <= start) return false;
out = msg.substring(start, end).toInt();
return true;
}
static bool parseJsonBoolField(const String& msg, const char* key, bool& out) {
String pattern = "\"";
pattern += key;
pattern += "\"";
int k = msg.indexOf(pattern);
if (k < 0) return false;
int colon = msg.indexOf(':', k + pattern.length());
if (colon < 0) return false;
int start = colon + 1;
while (start < (int)msg.length() && (msg[start] == ' ' || msg[start] == '\t')) start++;
if (msg.startsWith("true", start)) {
out = true;
return true;
}
if (msg.startsWith("false", start)) {
out = false;
return true;
}
return false;
}
static bool parseJsonFloatField(const String& msg, const char* key, float& out) {
String pattern = "\"";
pattern += key;
pattern += "\"";
int k = msg.indexOf(pattern);
if (k < 0) return false;
int colon = msg.indexOf(':', k + pattern.length());
if (colon < 0) return false;
int start = colon + 1;
while (start < (int)msg.length() && (msg[start] == ' ' || msg[start] == '\t')) start++;
int end = start;
while (end < (int)msg.length()) {
const char c = msg[end];
if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') {
end++;
} else {
break;
}
}
if (end <= start) return false;
out = msg.substring(start, end).toFloat();
return true;
}
static void handleUdpCommand(const String& msg) {
bool enabled = g_udpEnabled;
const bool hasEnabled = parseJsonBoolField(msg, "enabled", enabled);
String host;
const bool hasHost = parseJsonStringField(msg, "target_host", host);
if (hasHost) {
g_udpHost = host;
}
int p = 0;
const bool hasPort = parseJsonIntField(msg, "target_port", p);
if (hasPort && p > 0 && p <= 65535) {
g_udpPort = (uint16_t)p;
}
Serial.print("UDP cmd parsed enabled=");
Serial.print(hasEnabled ? (enabled ? "true" : "false") : "<missing>");
Serial.print(" host=");
Serial.print(hasHost ? host : "<missing>");
Serial.print(" port=");
Serial.println(hasPort ? String(p) : "<missing>");
if (enabled) {
if (!resolveUdpHost()) {
Serial.println("UDP target resolve failed");
g_udpEnabled = false;
return;
}
g_udpEnabled = true;
Serial.print("UDP target resolved: ");
Serial.print(g_udpIp);
Serial.print(":");
Serial.println(g_udpPort);
} else {
g_udpEnabled = false;
Serial.println("UDP stream disabled");
}
publishUdpStatus(true);
}
static void publishClientStatus() {
if (!g_mqttConnected || !g_mqtt.connected()) return;
String msg = "{\"type\":\"client_status\",\"device_id\":\"";
msg += EVS_DEVICE_ID;
msg += "\",\"mic_enabled\":";
msg += isMicModeActive() ? "true" : "false";
msg += ",\"speaker_enabled\":";
msg += isSpeakerModeActive() ? "true" : "false";
msg += ",\"io_mode\":\"";
msg += (g_ioMode == IoMode::Mic) ? "mic" : "spk";
msg += "\",\"mode\":\"";
msg += (g_mode == DeviceMode::StreamToServer) ? "stream" : "idle";
msg += "\",\"ws_connected\":";
msg += g_wsConnected ? "true" : "false";
msg += ",\"mqtt_connected\":";
msg += (g_mqttConnected && g_mqtt.connected()) ? "true" : "false";
msg += "}";
const bool ok = g_mqtt.publish(g_mqttStatusTopic.c_str(), msg.c_str(), true);
if (!ok) {
Serial.print("MQTT status publish failed, len=");
Serial.print(msg.length());
Serial.print(" state=");
Serial.println(g_mqtt.state());
}
}
static void publishUdpStatus(bool force) {
if (!g_mqttConnected || !g_mqtt.connected()) return;
const uint32_t now = millis();
if (!force && (now - g_lastUdpStatusMs) < UDP_STATUS_INTERVAL_MS) return;
g_lastUdpStatusMs = now;
String msg = "{\"type\":\"udp_status\",\"device_id\":\"";
msg += EVS_DEVICE_ID;
msg += "\",\"enabled\":";
msg += g_udpEnabled ? "true" : "false";
msg += ",\"target_resolved\":";
msg += g_udpIpValid ? "true" : "false";
msg += ",\"target_host\":\"";
msg += g_udpHost;
msg += "\",\"target_port\":";
msg += String(g_udpPort);
msg += ",\"packets_sent\":";
msg += String(g_udpPacketsSent);
msg += "}";
const bool ok = g_mqtt.publish(g_mqttUdpStatusTopic.c_str(), msg.c_str(), true);
if (!ok) {
Serial.print("MQTT udp status publish failed, len=");
Serial.print(msg.length());
Serial.print(" state=");
Serial.println(g_mqtt.state());
}
}
@@ -282,6 +718,9 @@ static void handleFrameForServer(const int16_t* frame, size_t count) {
}
static void publishMicTelemetryIfDue(const int16_t* frame, size_t count) {
if (!isMicModeActive()) {
return;
}
if (count == 0) {
return;
}
@@ -306,6 +745,8 @@ static void publishMicTelemetryIfDue(const int16_t* frame, size_t count) {
Serial.print(peak);
Serial.print(" avg_abs=");
Serial.print(avg_abs);
Serial.print(" gain=");
Serial.print(g_micGain, 3);
Serial.print(" ws=");
Serial.println(g_wsConnected ? "connected" : "disconnected");
@@ -316,22 +757,30 @@ static void publishMicTelemetryIfDue(const int16_t* frame, size_t count) {
msg += String(avg_abs);
msg += ",\"samples\":";
msg += String((unsigned)count);
msg += ",\"mic_gain\":";
msg += String(g_micGain, 3);
msg += "}";
g_ws.send(msg);
}
}
static inline int16_t convertMicSampleToPcm16(int32_t raw32) {
if (!isMicModeActive()) {
return 0;
}
// INMP441 uses 24-bit signed samples packed into 32-bit I2S slots.
int32_t s24 = raw32 >> 8;
int32_t s16 = s24 >> kMicS24ToS16Shift;
float scaled = (float)s16 * kMicGain;
float scaled = (float)s16 * g_micGain;
if (scaled > 32767.0f) scaled = 32767.0f;
if (scaled < -32768.0f) scaled = -32768.0f;
return (int16_t)scaled;
}
static void serviceSpeaker() {
if (!isSpeakerModeActive()) {
return;
}
const uint32_t periodUs = 1000000UL / SPEAKER_SAMPLE_RATE;
uint32_t now = micros();
int processed = 0;
@@ -340,9 +789,9 @@ static void serviceSpeaker() {
g_nextOutUs += periodUs;
int16_t s = 0;
if (dequeuePcmSample(s)) {
pwmWrite(pcm16ToPwm8(s));
speakerWriteU8(pcm16ToU8(s));
} else {
pwmWrite(128);
speakerWriteU8(128);
}
now = micros();
++processed;
@@ -354,6 +803,8 @@ static void printHelp() {
Serial.println("Commands:");
Serial.println(" p = print network status");
Serial.println(" h = help");
Serial.println("MQTT cmd:");
Serial.println(" {\"cmd\":\"io_mode\",\"value\":\"mic|spk\"}");
Serial.println("Connection policy:");
Serial.println(" connect -> StreamToServer (start)");
Serial.println(" disconnect -> Idle");
@@ -392,18 +843,38 @@ void setup() {
Serial.println("ERROR: I2S init failed");
while (true) delay(1000);
}
if (!initPwmOut()) {
Serial.println("ERROR: PWM init failed");
if (!initSpeakerOut()) {
Serial.println("ERROR: speaker DAC init failed (EVS_SPK_DAC_PIN must be 25 or 26)");
while (true) delay(1000);
}
g_ws.onMessage(onWsMessageCallback);
g_ws.onEvent(onWsEventCallback);
g_mqtt.setBufferSize(MQTT_BUFFER_SIZE);
g_mqttCmdTopic = String(EVS_MQTT_BASE_TOPIC) + "/" + EVS_DEVICE_ID + "/command";
g_mqttStatusTopic = String(EVS_MQTT_BASE_TOPIC) + "/" + EVS_DEVICE_ID + "/status";
g_mqttUdpStatusTopic = String(EVS_MQTT_BASE_TOPIC) + "/" + EVS_DEVICE_ID + "/udp_status";
g_nextOutUs = micros();
String defaultMode = EVS_DEFAULT_IO_MODE;
defaultMode.toLowerCase();
if (defaultMode == "spk" || defaultMode == "speaker") {
g_ioMode = IoMode::Speaker;
} else {
g_ioMode = IoMode::Mic;
}
// Wait in idle until WS connect event switches to StreamToServer.
setMode(DeviceMode::Idle);
Serial.println("Audio init ok");
Serial.print("Hardware: mic=");
Serial.print(kMicHardwareAvailable ? "yes" : "no");
Serial.print(" speaker=");
Serial.println(kSpeakerHardwareAvailable ? "yes" : "no");
Serial.print("Active io_mode: ");
Serial.println(g_ioMode == IoMode::Mic ? "mic" : "spk");
Serial.print("Speaker DAC pin: ");
Serial.println(EVS_SPK_DAC_PIN);
Serial.println("Set local environment values in include/secrets.h");
printHelp();
}
@@ -411,9 +882,22 @@ void setup() {
void loop() {
handleSerialCommands();
ensureConnectivity();
ensureMqttConnectivity();
g_ws.poll();
if (g_mqtt.connected()) {
g_mqtt.loop();
g_mqttConnected = true;
} else {
g_mqttConnected = false;
}
publishUdpStatus(false);
serviceSpeaker();
if (!isMicModeActive()) {
delay(5);
return;
}
size_t bytesRead = 0;
const esp_err_t res = i2s_read(
MIC_I2S_PORT, g_micBuffer, sizeof(g_micBuffer), &bytesRead, 0);
@@ -430,7 +914,15 @@ void loop() {
if (g_mode == DeviceMode::StreamToServer) {
handleFrameForServer(pcm16, sampleCount);
publishMicTelemetryIfDue(pcm16, sampleCount);
} else {
// idle
}
if (g_udpEnabled && g_udpIpValid && g_udpPort > 0) {
const size_t nBytes = sampleCount * sizeof(int16_t);
if (nBytes <= UDP_MAX_PACKET_BYTES) {
g_udp.beginPacket(g_udpIp, g_udpPort);
g_udp.write(reinterpret_cast<const uint8_t*>(pcm16), nBytes);
g_udp.endPacket();
++g_udpPacketsSent;
}
}
}