fixed race conditions

This commit is contained in:
Roman Lut 2025-05-24 23:45:10 +02:00
parent 53dea4f714
commit ad725ffc71
5 changed files with 104 additions and 62 deletions

View file

@ -17,7 +17,7 @@ TaskHandle_t micTaskHandle = NULL;
TaskHandle_t networkTaskHandle = NULL; TaskHandle_t networkTaskHandle = NULL;
// TIMING REGISTERS // TIMING REGISTERS
bool scheduleListeningRestart = false; volatile bool scheduleListeningRestart = false;
unsigned long scheduledTime = 0; unsigned long scheduledTime = 0;
unsigned long speakingStartTime = 0; unsigned long speakingStartTime = 0;
@ -31,18 +31,20 @@ class BufferPrint : public Print {
public: public:
BufferPrint(BufferRTOS<uint8_t>& buf) : _buffer(buf) {} BufferPrint(BufferRTOS<uint8_t>& buf) : _buffer(buf) {}
// networkTask -> webSocket.loop() -> webSocketEvent(WStype_BIN, ...) -> opusDecoder.write() -> bufferPrint.write()
virtual size_t write(uint8_t data) override { virtual size_t write(uint8_t data) override {
if (webSocket.isConnected() && deviceState == SPEAKING) { if (webSocket.isConnected() && deviceState == SPEAKING) {
return _buffer.writeArray(&data, 1); return _buffer.writeArray(&data, 1);
} }
return 0; return 1; //let opusDecoder write, otherwise thread will stuck
} }
// networkTask -> webSocket.loop() -> webSocketEvent(WStype_BIN, ...) -> opusDecoder.write() -> bufferPrint.write()
virtual size_t write(const uint8_t *buffer, size_t size) override { virtual size_t write(const uint8_t *buffer, size_t size) override {
if (webSocket.isConnected() && deviceState == SPEAKING) { if (webSocket.isConnected() && deviceState == SPEAKING) {
return _buffer.writeArray(buffer, size); return _buffer.writeArray(buffer, size);
} }
return 0; return size; //let opusDecoder write, otherwise thread will stuck
} }
private: private:
@ -50,13 +52,14 @@ private:
}; };
BufferPrint bufferPrint(audioBuffer); BufferPrint bufferPrint(audioBuffer);
OpusAudioDecoder opusDecoder; OpusAudioDecoder opusDecoder; //access guarded by wsmutex
BufferRTOS<uint8_t> audioBuffer(AUDIO_BUFFER_SIZE, AUDIO_CHUNK_SIZE); BufferRTOS<uint8_t> audioBuffer(AUDIO_BUFFER_SIZE, AUDIO_CHUNK_SIZE); //producer: networkTask, consumer: audioStreamTask. Thread safe in single producer->single consumer scenario.
I2SStream i2s; I2SStream i2s; //access from audioStreamTask only
VolumeStream volume(i2s); VolumeStream volume(i2s); //access from audioStreamTask only
QueueStream<uint8_t> queue(audioBuffer); QueueStream<uint8_t> queue(audioBuffer); //access from audioStreamTask only
StreamCopy copier(volume, queue); StreamCopy copier(volume, queue);
AudioInfo info(SAMPLE_RATE, CHANNELS, BITS_PER_SAMPLE); AudioInfo info(SAMPLE_RATE, CHANNELS, BITS_PER_SAMPLE);
volatile bool outputFlushScheduled = false;
unsigned long getSpeakingDuration() { unsigned long getSpeakingDuration() {
if (deviceState == SPEAKING && speakingStartTime > 0) { if (deviceState == SPEAKING && speakingStartTime > 0) {
@ -65,45 +68,39 @@ unsigned long getSpeakingDuration() {
return 0; return 0;
} }
void transitionToSpeaking() { // networkTask -> webSocket.loop() -> webSocketEvent(WStype_TEXT, ...) -> transitionToSpeaking()
void transitionToSpeaking() {
vTaskDelay(50); vTaskDelay(50);
i2sInput.flush(); i2sInputFlushScheduled = true;
if (xSemaphoreTake(wsMutex, pdMS_TO_TICKS(100)) == pdTRUE) { deviceState = SPEAKING;
deviceState = SPEAKING; digitalWrite(I2S_SD_OUT, HIGH);
digitalWrite(I2S_SD_OUT, HIGH); speakingStartTime = millis();
speakingStartTime = millis();
webSocket.enableHeartbeat(30000, 15000, 3);
webSocket.enableHeartbeat(30000, 15000, 3);
xSemaphoreGive(wsMutex);
}
Serial.println("Transitioned to speaking mode"); Serial.println("Transitioned to speaking mode");
} }
// networkTask -> transitionToListening()
// ( networkTask -> webSocket.loop() -> webSocketEvent(WStype_TEXT, ...) -> (sets scheduleListeningRestart) -> networkTask -> transitionToListening() )
void transitionToListening() { void transitionToListening() {
deviceState = PROCESSING; deviceState = PROCESSING;
scheduleListeningRestart = false; scheduleListeningRestart = false;
Serial.println("Transitioning to listening mode"); Serial.println("Transitioning to listening mode");
// These stream operations don't directly interact with the WebSocket i2sInputFlushScheduled = true;
i2s.flush(); outputFlushScheduled = true;
volume.flush();
queue.flush();
i2sInput.flush();
audioBuffer.reset();
Serial.println("Transitioned to listening mode"); Serial.println("Transitioned to listening mode");
if (xSemaphoreTake(wsMutex, pdMS_TO_TICKS(100)) == pdTRUE) { deviceState = LISTENING;
deviceState = LISTENING; digitalWrite(I2S_SD_OUT, LOW);
digitalWrite(I2S_SD_OUT, LOW); webSocket.disableHeartbeat();
webSocket.disableHeartbeat();
xSemaphoreGive(wsMutex);
}
} }
// audioStreamTask -> copier.copy() (conditional on webSocket.isConnected())
void audioStreamTask(void *parameter) { void audioStreamTask(void *parameter) {
Serial.println("Starting I2S stream pipeline..."); Serial.println("Starting I2S stream pipeline...");
@ -114,8 +111,13 @@ void audioStreamTask(void *parameter) {
cfg.channels = CHANNELS; cfg.channels = CHANNELS;
cfg.bits_per_sample = BITS_PER_SAMPLE; cfg.bits_per_sample = BITS_PER_SAMPLE;
cfg.max_buffer_size = 6144; cfg.max_buffer_size = 6144;
xSemaphoreTake(wsMutex, portMAX_DELAY);
opusDecoder.setOutput(bufferPrint); opusDecoder.setOutput(bufferPrint);
opusDecoder.begin(cfg); opusDecoder.begin(cfg);
xSemaphoreGive(wsMutex);
audioBuffer.setReadMaxWait(0);
queue.begin(); queue.begin();
@ -134,53 +136,60 @@ void audioStreamTask(void *parameter) {
auto vcfg = volume.defaultConfig(); auto vcfg = volume.defaultConfig();
vcfg.copyFrom(config); vcfg.copyFrom(config);
vcfg.allow_boost = true; vcfg.allow_boost = true;
volume.begin(vcfg); volume.begin(vcfg);
while (1) { while (1) {
if ( outputFlushScheduled) {
outputFlushScheduled = false;
i2s.flush();
volume.flush();
queue.flush();
//audioBuffer.reset(); todo: read untill empty
}
if (webSocket.isConnected() && deviceState == SPEAKING) { if (webSocket.isConnected() && deviceState == SPEAKING) {
copier.copy(); copier.copy();
}
else {
//we should always read from audioBuffer, otherwise writing thread can stuck
queue.read();
} }
vTaskDelay(1); vTaskDelay(1);
} }
} }
// AUDIO INPUT SETTINGS
class WebsocketStream : public Print { class WebsocketStream : public Print {
public: public:
// micTask -> micToWsCopier.copyBytes() -> wsStream.write()
virtual size_t write(uint8_t b) override { virtual size_t write(uint8_t b) override {
if (!webSocket.isConnected() || deviceState != LISTENING) { if (!webSocket.isConnected() || deviceState != LISTENING) {
return 1; return 1;
} }
if (xSemaphoreTake(wsMutex, pdMS_TO_TICKS(100)) == pdTRUE) { xSemaphoreTake(wsMutex, portMAX_DELAY);
webSocket.sendBIN(&b, 1); webSocket.sendBIN(&b, 1);
xSemaphoreGive(wsMutex); xSemaphoreGive(wsMutex);
return 1;
}
return 1; return 1;
} }
// micTask -> micToWsCopier.copyBytes() -> wsStream.write()
virtual size_t write(const uint8_t *buffer, size_t size) override { virtual size_t write(const uint8_t *buffer, size_t size) override {
if (size == 0 || !webSocket.isConnected() || deviceState != LISTENING) { if (size == 0 || !webSocket.isConnected() || deviceState != LISTENING) {
return size; return size;
} }
xSemaphoreTake(wsMutex, portMAX_DELAY);
if (xSemaphoreTake(wsMutex, pdMS_TO_TICKS(100)) == pdTRUE) { webSocket.sendBIN(buffer, size);
webSocket.sendBIN(buffer, size); xSemaphoreGive(wsMutex);
xSemaphoreGive(wsMutex);
return size;
}
return size; return size;
} }
}; };
WebsocketStream wsStream; WebsocketStream wsStream; //guard with wsMutex
I2SStream i2sInput; I2SStream i2sInput; //access from micTask only
StreamCopy micToWsCopier(wsStream, i2sInput); StreamCopy micToWsCopier(wsStream, i2sInput);
volatile bool i2sInputFlushScheduled = false;
const int MIC_COPY_SIZE = 64; const int MIC_COPY_SIZE = 64;
void micTask(void *parameter) { void micTask(void *parameter) {
@ -198,10 +207,12 @@ void micTask(void *parameter) {
i2sConfig.port_no = I2S_PORT_IN; i2sConfig.port_no = I2S_PORT_IN;
i2sInput.begin(i2sConfig); i2sInput.begin(i2sConfig);
micToWsCopier.setDelayOnNoData(0);
while (1) { while (1) {
// Check to see if a transition to listening mode is scheduled. if ( i2sInputFlushScheduled ) {
if (scheduleListeningRestart && millis() >= scheduledTime) { i2sInputFlushScheduled = false;
transitionToListening(); i2sInput.flush();
} }
if (deviceState == LISTENING && webSocket.isConnected()) { if (deviceState == LISTENING && webSocket.isConnected()) {
@ -217,6 +228,7 @@ void micTask(void *parameter) {
} }
// WEBSOCKET EVENTS // WEBSOCKET EVENTS
// networkTask -> webSocket.loop() -> webSocketEvent()
void webSocketEvent(WStype_t type, uint8_t *payload, size_t length) void webSocketEvent(WStype_t type, uint8_t *payload, size_t length)
{ {
switch (type) switch (type)
@ -276,7 +288,7 @@ void webSocketEvent(WStype_t type, uint8_t *payload, size_t length)
// Check if volume_control is included in the message // Check if volume_control is included in the message
if (doc.containsKey("volume_control")) { if (doc.containsKey("volume_control")) {
int newVolume = doc["volume_control"].as<int>(); int newVolume = doc["volume_control"].as<int>();
volume.setVolume(newVolume / 100.0f); volume.setVolume(newVolume / 100.0f * 1.4f);
} }
scheduleListeningRestart = true; scheduleListeningRestart = true;
@ -317,9 +329,13 @@ void webSocketEvent(WStype_t type, uint8_t *payload, size_t length)
} }
} }
// wifiTask -> WIFIMANAGER::loop() -> WIFIMANAGER::tryConnect() -> connectCb() -> websocketSetup()
void websocketSetup(String server_domain, int port, String path) void websocketSetup(String server_domain, int port, String path)
{ {
String headers = "Authorization: Bearer " + String(authTokenGlobal); String headers = "Authorization: Bearer " + String(authTokenGlobal);
xSemaphoreTake(wsMutex, portMAX_DELAY);
webSocket.setExtraHeaders(headers.c_str()); webSocket.setExtraHeaders(headers.c_str());
webSocket.onEvent(webSocketEvent); webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(1000); webSocket.setReconnectInterval(1000);
@ -331,12 +347,23 @@ void websocketSetup(String server_domain, int port, String path)
#else #else
webSocket.beginSslWithCA(server_domain.c_str(), port, path.c_str(), CA_cert); webSocket.beginSslWithCA(server_domain.c_str(), port, path.c_str(), CA_cert);
#endif #endif
xSemaphoreGive(wsMutex);
} }
// networkTask -> webSocket.loop()
void networkTask(void *parameter) { void networkTask(void *parameter) {
while (1) { while (1) {
xSemaphoreTake(wsMutex, portMAX_DELAY);
// Check to see if a transition to listening mode is scheduled.
if (scheduleListeningRestart && millis() >= scheduledTime) {
transitionToListening();
}
webSocket.loop(); webSocket.loop();
xSemaphoreGive(wsMutex);
vTaskDelay(1); vTaskDelay(1);
} }
} }

View file

@ -13,7 +13,7 @@ extern TaskHandle_t speakerTaskHandle;
extern TaskHandle_t micTaskHandle; extern TaskHandle_t micTaskHandle;
extern TaskHandle_t networkTaskHandle; extern TaskHandle_t networkTaskHandle;
extern bool scheduleListeningRestart; extern volatile bool scheduleListeningRestart;
extern unsigned long scheduledTime; extern unsigned long scheduledTime;
extern unsigned long speakingStartTime; extern unsigned long speakingStartTime;
@ -31,10 +31,12 @@ extern VolumeStream volume;
extern QueueStream<uint8_t> queue; extern QueueStream<uint8_t> queue;
extern StreamCopy copier; extern StreamCopy copier;
extern AudioInfo info; extern AudioInfo info;
extern volatile bool outputFlushScheduled;
// AUDIO INPUT // AUDIO INPUT
extern I2SStream i2sInput; extern I2SStream i2sInput;
extern StreamCopy micToWsCopier; extern StreamCopy micToWsCopier;
extern volatile bool i2sInputFlushScheduled;
// WEBSOCKET // WEBSOCKET
void webSocketEvent(WStype_t type, uint8_t *payload, size_t length); void webSocketEvent(WStype_t type, uint8_t *payload, size_t length);

View file

@ -59,7 +59,7 @@ const uint16_t backend_port = 3000;
#endif #endif
String authTokenGlobal; String authTokenGlobal;
DeviceState deviceState = IDLE; volatile DeviceState deviceState = IDLE;
// I2S and Audio parameters // I2S and Audio parameters
const uint32_t SAMPLE_RATE = 24000; const uint32_t SAMPLE_RATE = 24000;

View file

@ -44,10 +44,11 @@ enum DeviceState
PROCESSING, PROCESSING,
WAITING, WAITING,
OTA, OTA,
FACTORY_RESET FACTORY_RESET,
SLEEP
}; };
extern DeviceState deviceState; extern volatile DeviceState deviceState;
// WiFi credentials // WiFi credentials
extern const char *EAP_IDENTITY; extern const char *EAP_IDENTITY;

View file

@ -20,27 +20,38 @@ AsyncWebServer webServer(80);
WIFIMANAGER WifiManager; WIFIMANAGER WifiManager;
esp_err_t getErr = ESP_OK; esp_err_t getErr = ESP_OK;
// Main Thread -> onButtonLongPressUpEventCb -> enterSleep()
// Main Thread -> onButtonDoubleClickCb -> enterSleep()
// Touch Task -> touchTask -> enterSleep()
// Main Thread -> loop() (inactivity timeout) -> enterSleep()
void enterSleep() void enterSleep()
{ {
Serial.println("Going to sleep..."); Serial.println("Going to sleep...");
// First, change device state to prevent any new data processing // First, change device state to prevent any new data processing
deviceState = IDLE; deviceState = SLEEP;
scheduleListeningRestart = false;
outputFlushScheduled = true;
i2sInputFlushScheduled = true;
vTaskDelay(10); //let all tasks accept state
xSemaphoreTake(wsMutex, portMAX_DELAY);
// Stop audio tasks first // Stop audio tasks first
i2s_stop(I2S_PORT_IN); i2s_stop(I2S_PORT_IN);
i2s_stop(I2S_PORT_OUT); i2s_stop(I2S_PORT_OUT);
// Clear any remaining audio in buffer // Clear any remaining audio in buffer
audioBuffer.reset(); outputFlushScheduled = true;
// Properly disconnect WebSocket and wait for it to complete // Properly disconnect WebSocket and wait for it to complete
if (webSocket.isConnected()) { if (webSocket.isConnected()) {
webSocket.disconnect(); webSocket.disconnect();
// Give some time for the disconnect to process // Give some time for the disconnect to process
delay(100);
} }
xSemaphoreGive(wsMutex);
delay(100);
// Stop all tasks that might be using I2S or other peripherals // Stop all tasks that might be using I2S or other peripherals
i2s_driver_uninstall(I2S_PORT_IN); i2s_driver_uninstall(I2S_PORT_IN);
i2s_driver_uninstall(I2S_PORT_OUT); i2s_driver_uninstall(I2S_PORT_OUT);
@ -58,6 +69,7 @@ void enterSleep()
#endif #endif
esp_deep_sleep_start(); esp_deep_sleep_start();
delay(1000);
} }
void printOutESP32Error(esp_err_t err) void printOutESP32Error(esp_err_t err)