[SERIAL] Improve communication robustness (#2079)

-Add a semaphore to avoid msg overlap
-Clean partial buffer
-Enable erase and restart of the secondary module from the primary

Co-authored-by: Florian <1technophile@users.noreply.github.com>
This commit is contained in:
FlorianandFlorian authored and GitHub committed 2024-10-03 09:47:22 -05:00
1 parent fcc09df02c
commit 4e090eda3c
3 files changed
+110 -60

No files matched your search

+1 -1
View File
@@ -852,7 +852,7 @@ void setupBTTasksAndBLE() {
procBLETask, /* Function to implement the task */ procBLETask, /* Function to implement the task */
"procBLETask", /* Name of the task */ "procBLETask", /* Name of the task */
# if defined(USE_ESP_IDF) || defined(USE_BLUFI) # if defined(USE_ESP_IDF) || defined(USE_BLUFI)
12500, 13500,
# else # else
8500, /* Stack size in bytes */ 8500, /* Stack size in bytes */
# endif # endif
+95 -52
View File
@@ -34,6 +34,20 @@
SoftwareSerial SERIALSoftSerial(SERIAL_RX_GPIO, SERIAL_TX_GPIO); // RX, TX SoftwareSerial SERIALSoftSerial(SERIAL_RX_GPIO, SERIAL_TX_GPIO); // RX, TX
# endif # endif
# ifdef ESP32
SemaphoreHandle_t serialSemaphore = NULL;
const TickType_t semaphoreTimeout = pdMS_TO_TICKS(1000); // 1 second timeout
# undef SEMAPHORE_SERIAL
# undef SEMAPHORE_SERIAL_GIVE
# define SEMAPHORE_SERIAL xSemaphoreTake(serialSemaphore, semaphoreTimeout) == pdTRUE
# define SEMAPHORE_SERIAL_GIVE xSemaphoreGive(serialSemaphore)
# else
# undef SEMAPHORE_SERIAL
# undef SEMAPHORE_SERIAL_GIVE
# define SEMAPHORE_SERIAL true
# define SEMAPHORE_SERIAL_GIVE
# endif
// use pointer to stream class for serial communication to make code // use pointer to stream class for serial communication to make code
// compatible with both softwareSerial as hardwareSerial. // compatible with both softwareSerial as hardwareSerial.
Stream* SERIALStream = NULL; Stream* SERIALStream = NULL;
@@ -43,7 +57,9 @@ bool receiverReady = false;
unsigned long lastHeartbeatReceived = 0; unsigned long lastHeartbeatReceived = 0;
unsigned long lastHeartbeatAckReceived = 0; unsigned long lastHeartbeatAckReceived = 0;
unsigned long lastHeartbeatSent = 0; unsigned long lastHeartbeatSent = 0;
unsigned long lastHeartbeatAckReceivedCheck = 0;
const unsigned long heartbeatTimeout = 15000; // 15 seconds timeout for ack const unsigned long heartbeatTimeout = 15000; // 15 seconds timeout for ack
const unsigned long heartbeatAckCheckInterval = 5000; // Check for ack every 5 seconds
const unsigned long maxHeartbeatInterval = 60000; // Maximum interval of 1 minute const unsigned long maxHeartbeatInterval = 60000; // Maximum interval of 1 minute
unsigned long heartbeatInterval = 5000; // 5 seconds unsigned long heartbeatInterval = 5000; // 5 seconds
bool isOverflow = false; bool isOverflow = false;
@@ -102,6 +118,13 @@ void setupSERIAL() {
Log.notice(F("SERIAL_TX_GPIO: %d" CR), SERIAL_TX_GPIO); Log.notice(F("SERIAL_TX_GPIO: %d" CR), SERIAL_TX_GPIO);
# endif # endif
# ifdef ESP32
serialSemaphore = xSemaphoreCreateMutex();
if (serialSemaphore == NULL) {
Log.error(F("Failed to create serialSemaphore" CR));
}
# endif
// Flush all bytes in the "link" serial port buffer // Flush all bytes in the "link" serial port buffer
while (SERIALStream->available() > 0) while (SERIALStream->available() > 0)
SERIALStream->read(); SERIALStream->read();
@@ -137,42 +160,60 @@ void SERIALtoX() {
# elif SERIALtoMQTTmode == 1 // Convert received JSON data to one or multiple MQTT topics # elif SERIALtoMQTTmode == 1 // Convert received JSON data to one or multiple MQTT topics
void sendHeartbeat() { void sendHeartbeat() {
Log.trace(F("Sending Serial heartbeat" CR)); if (SEMAPHORE_SERIAL) {
SERIALStream->print(SERIALPre); SERIALStream->print(SERIALPre);
SERIALStream->print("{\"type\":\"heartbeat\"}"); SERIALStream->print("{\"type\":\"heartbeat\"}");
SERIALStream->print(SERIALPost); SERIALStream->print(SERIALPost);
SERIALStream->flush(); SERIALStream->flush();
Log.notice(F("Sent Serial heartbeat" CR));
SEMAPHORE_SERIAL_GIVE;
} else {
Log.error(F("Failed to take serialSemaphore" CR));
}
} }
void sendHeartbeatAck() { void sendHeartbeatAck() {
SERIALStream->print(SERIALPre); if (SEMAPHORE_SERIAL) {
SERIALStream->print("{\"type\":\"heartbeat_ack\"}"); SERIALStream->print(SERIALPre);
SERIALStream->print(SERIALPost); SERIALStream->print("{\"type\":\"heartbeat_ack\"}");
SERIALStream->flush(); SERIALStream->print(SERIALPost);
Log.trace(F("Sent heartbeat ack" CR)); SERIALStream->flush();
Log.notice(F("Sent heartbeat ack" CR));
SEMAPHORE_SERIAL_GIVE;
} else {
Log.error(F("Failed to take serialSemaphore" CR));
}
} }
void SERIALtoX() { void SERIALtoX() {
static String buffer = ""; // Static buffer to store incomplete messages static String buffer = ""; // Static buffer to store incomplete messages
unsigned long currentTime = millis(); unsigned long currentTime = millis();
# ifdef SENDER_SERIAL_HEARTBEAT
// Check if it's time to send a heartbeat and we're not in overflow // Check if it's time to send a heartbeat and we're not in overflow
if (!isOverflow && currentTime - lastHeartbeatSent > heartbeatInterval) { if (!isOverflow && currentTime - lastHeartbeatSent > heartbeatInterval) {
sendHeartbeat();
lastHeartbeatSent = currentTime;
}
if (currentTime - lastHeartbeatAckReceivedCheck > heartbeatAckCheckInterval) {
lastHeartbeatAckReceivedCheck = currentTime;
// Check if we received an ack for the last heartbeat // Check if we received an ack for the last heartbeat
if (currentTime - lastHeartbeatAckReceived > heartbeatTimeout) { if (currentTime - lastHeartbeatAckReceived > heartbeatTimeout) {
// No ack received, increase the interval (with a maximum limit) // No ack received, increase the interval (with a maximum limit)
heartbeatInterval = min(heartbeatInterval * 2, maxHeartbeatInterval); unsigned long newHeartbeatInterval = heartbeatInterval * 1.25;
heartbeatInterval = min(newHeartbeatInterval, maxHeartbeatInterval);
Log.warning(F("No heartbeat ack received. Increasing interval to %lu ms" CR), heartbeatInterval); Log.warning(F("No heartbeat ack received. Increasing interval to %lu ms" CR), heartbeatInterval);
receiverReady = false;
} else { } else {
// Ack received, reset the interval // Ack received, reset the interval
heartbeatInterval = 5000; heartbeatInterval = 5000;
} }
sendHeartbeat();
lastHeartbeatSent = currentTime;
} }
# else
receiverReady = true;
# endif
while (SERIALStream->available()) { while (SERIALStream->available()) {
unsigned long now = millis();
char c = SERIALStream->read(); char c = SERIALStream->read();
buffer += c; buffer += c;
@@ -194,7 +235,8 @@ void SERIALtoX() {
if (SERIALdata.containsKey("type") && strcmp(SERIALdata["type"], "heartbeat") == 0) { if (SERIALdata.containsKey("type") && strcmp(SERIALdata["type"], "heartbeat") == 0) {
handleHeartbeat(); handleHeartbeat();
} else if (SERIALdata.containsKey("type") && strcmp(SERIALdata["type"], "heartbeat_ack") == 0) { } else if (SERIALdata.containsKey("type") && strcmp(SERIALdata["type"], "heartbeat_ack") == 0) {
lastHeartbeatAckReceived = currentTime; lastHeartbeatAckReceived = now;
receiverReady = true;
Log.notice(F("Heartbeat ack received" CR)); Log.notice(F("Heartbeat ack received" CR));
} else { } else {
// Process normal messages // Process normal messages
@@ -205,7 +247,7 @@ void SERIALtoX() {
} else { } else {
// send as json // send as json
if (SERIALdata.containsKey("origin") || SERIALdata.containsKey("topic")) { if (SERIALdata.containsKey("origin") || SERIALdata.containsKey("topic")) {
# ifdef ZmqttDiscovery # ifdef SecondaryModule
// We need to assign the discovery message to the primary module instead of the secondary module // We need to assign the discovery message to the primary module instead of the secondary module
if (SERIALdata.containsKey("device") && SERIALdata["device"].containsKey("via_device")) { if (SERIALdata.containsKey("device") && SERIALdata["device"].containsKey("via_device")) {
SERIALdata["device"]["via_device"] = gateway_name; SERIALdata["device"]["via_device"] = gateway_name;
@@ -231,6 +273,10 @@ void SERIALtoX() {
// Clear the buffer for the next message // Clear the buffer for the next message
buffer = ""; buffer = "";
} else if (buffer.endsWith(SERIALPost)) {
// If the buffer ends with the postfix but does not start with the prefix, clear it
Log.error(F("Buffer error, clearing buffer. Partial content: %s" CR), buffer.c_str());
buffer = "";
} else if (buffer.length() > JSON_MSG_BUFFER) { } else if (buffer.length() > JSON_MSG_BUFFER) {
// If the buffer gets too large without finding a complete message, clear it // If the buffer gets too large without finding a complete message, clear it
Log.error(F("Buffer overflow, clearing buffer. Partial content: %s" CR), buffer.c_str()); Log.error(F("Buffer overflow, clearing buffer. Partial content: %s" CR), buffer.c_str());
@@ -280,40 +326,37 @@ void sendMQTTfromNestedJson(JsonVariant obj, char* topic, int level, int maxLeve
bool XtoSERIAL(const char* topicOri, JsonObject& SERIALdata) { bool XtoSERIAL(const char* topicOri, JsonObject& SERIALdata) {
bool res = false; bool res = false;
unsigned long currentTime = millis(); if (SEMAPHORE_SERIAL) {
if (receiverReady && (cmpToMainTopic(topicOri, subjectMQTTtoSERIAL) ||
(SYSConfig.serial && SERIALdata.containsKey("origin") && SERIALdata["origin"].is<const char*>()) ||
(SYSConfig.serial && SERIALdata.containsKey("topic") && SERIALdata["topic"].is<const char*>()))) {
Log.trace(F("XtoSERIAL" CR));
// Prepare the data string
std::string data;
if (SYSConfig.serial ||
(SERIALdata.containsKey("origin") && SERIALdata["origin"].is<const char*>()) || // Module like BT to SERIAL
(SERIALdata.containsKey("target") && SERIALdata["target"].is<const char*>())) { // Command to send to a specific target example MQTTtoBT through SERIAL
//SERIALdata["msgcount"] = msgCount++;
serializeJson(SERIALdata, data);
} else if (SERIALdata.containsKey("value")) {
data = SERIALdata["value"].as<std::string>();
}
// Check if receiver is still ready (heartbeat check) // Send the message
if (currentTime - lastHeartbeatReceived > heartbeatTimeout) { const char* prefix = SERIALdata["prefix"] | SERIALPre;
receiverReady = false; const char* postfix = SERIALdata["postfix"] | SERIALPost;
Log.error(F("Heartbeat timeout. Receiver is not ready." CR)); SERIALStream->print(prefix);
} SERIALStream->print(data.c_str());
SERIALStream->print(postfix);
SERIALStream->flush();
if (receiverReady && (cmpToMainTopic(topicOri, subjectMQTTtoSERIAL) || (SYSConfig.serial && SERIALdata.containsKey("origin") && SERIALdata["origin"].is<const char*>()) || (SYSConfig.serial && SERIALdata.containsKey("topic") && SERIALdata["topic"].is<const char*>()))) { Log.notice(F("[ OMG->SERIAL ] data sent: %s" CR), data.c_str());
Log.trace(F("XtoSERIAL" CR)); res = true;
delay(100);
// Prepare the data string
std::string data;
if (SYSConfig.serial ||
(SERIALdata.containsKey("origin") && SERIALdata["origin"].is<const char*>()) || // Module like BT to SERIAL
(SERIALdata.containsKey("target") && SERIALdata["target"].is<const char*>())) { // Command to send to a specific target example MQTTtoBT through SERIAL
//SERIALdata["msgcount"] = msgCount++;
serializeJson(SERIALdata, data);
} else if (SERIALdata.containsKey("value")) {
data = SERIALdata["value"].as<std::string>();
} }
SEMAPHORE_SERIAL_GIVE;
// Send the message } else {
const char* prefix = SERIALdata["prefix"] | SERIALPre; Log.error(F("Failed to take serialSemaphore" CR));
const char* postfix = SERIALdata["postfix"] | SERIALPost;
SERIALStream->print(prefix);
SERIALStream->print(data.c_str());
SERIALStream->print(postfix);
SERIALStream->flush();
Log.notice(F("[ OMG->SERIAL ] data sent: %s" CR), data.c_str());
res = true;
delay(100);
} }
return res; return res;
} }
@@ -322,11 +365,11 @@ bool isSerialReady() {
return receiverReady; return receiverReady;
} }
// This function should be called when a heartbeat is received from the receiver // This function should be called when a heartbeat is received from the emitter
void handleHeartbeat() { void handleHeartbeat() {
receiverReady = true;
lastHeartbeatReceived = millis(); lastHeartbeatReceived = millis();
Log.trace(F("Heartbeat received. Receiver is ready." CR)); if (gatewayState == GatewayState::BROKER_CONNECTED) {
sendHeartbeatAck(); sendHeartbeatAck();
}
} }
#endif #endif
+14 -7
View File
@@ -1766,6 +1766,13 @@ void setupTLS(int index) {
9 - SELFTEST end 9 - SELFTEST end
*/ */
void ESPRestart(byte reason) { void ESPRestart(byte reason) {
#ifdef SecondaryModule
// Erase the secondary module config
String restartCmdStr = "{\"cmd\":\"" + String(restartCmd) + "\"}";
Log.notice(F("Restarting secondary module : %s" CR), restartCmdStr.c_str());
receivingDATA(subjectMQTTtoSYSsetSecondaryModule, restartCmdStr.c_str());
delay(2000);
#endif
StaticJsonDocument<128> jsonBuffer; StaticJsonDocument<128> jsonBuffer;
JsonObject jsondata = jsonBuffer.to<JsonObject>(); JsonObject jsondata = jsonBuffer.to<JsonObject>();
jsondata["reason"] = reason; jsondata["reason"] = reason;
@@ -1873,13 +1880,6 @@ void blockingWaitForReset() {
} }
# endif # endif
ledManager.setMode(-1, -1, LEDManager::STATIC, LED_WAITING_ONBOARD_COLOR, -1); ledManager.setMode(-1, -1, LEDManager::STATIC, LED_WAITING_ONBOARD_COLOR, -1);
# ifdef SecondaryModule
// Erase the secondary module config
String eraseCmdStr = "{\"cmd\":\"" + String(eraseCmd) + "\"}";
Log.notice(F("Erasing secondary module config: %s" CR), eraseCmdStr.c_str());
receivingDATA(subjectMQTTtoSYSsetSecondaryModule, eraseCmdStr.c_str());
delay(5000);
# endif
// Checking if the flash has already been erased to identify if we erase it or go into failsafe mode // Checking if the flash has already been erased to identify if we erase it or go into failsafe mode
// going to failsafe mode is done by doing a long button press from a state where the flash has already been erased // going to failsafe mode is done by doing a long button press from a state where the flash has already been erased
if (SPIFFS.begin()) { if (SPIFFS.begin()) {
@@ -2737,6 +2737,13 @@ float intTemperatureRead() {
Erase flash and restart the ESP Erase flash and restart the ESP
*/ */
void erase(bool restart) { void erase(bool restart) {
#ifdef SecondaryModule
// Erase the secondary module config
String eraseCmdStr = "{\"cmd\":\"" + String(eraseCmd) + "\"}";
Log.notice(F("Erasing secondary module config: %s" CR), eraseCmdStr.c_str());
receivingDATA(subjectMQTTtoSYSsetSecondaryModule, eraseCmdStr.c_str());
delay(2000);
#endif
Log.trace(F("Formatting requested, result: %d" CR), SPIFFS.format()); Log.trace(F("Formatting requested, result: %d" CR), SPIFFS.format());
#if defined(ESP8266) #if defined(ESP8266)