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
Some checks failed
Build and Push EVS Bridge Image / docker (push) Has been cancelled
This commit is contained in:
562
src/main.cpp
562
src/main.cpp
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user