Skip to content

Commit 231d4d7

Browse files
authored
Enhance note buffering and retry mechanism for Notecard communication. Introduced new functions to buffer notes for retry and flush buffered notes. Updated tank state structure to track valid readings and modified alarm handling to support SMS escalation based on configuration.
1 parent 64a10f2 commit 231d4d7

File tree

2 files changed

+159
-28
lines changed

2 files changed

+159
-28
lines changed

TankAlarm-112025-Client-BluesOpta/TankAlarm-112025-Client-BluesOpta.ino

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ static size_t strlcpy(char *dst, const char *src, size_t size) {
7575
#define CONFIG_OUTBOX_FILE "config.qo"
7676
#endif
7777

78+
#ifndef NOTE_BUFFER_PATH
79+
#define NOTE_BUFFER_PATH "/pending_notes.log"
80+
#endif
81+
82+
#ifndef NOTE_BUFFER_TEMP_PATH
83+
#define NOTE_BUFFER_TEMP_PATH "/pending_notes.tmp"
84+
#endif
85+
7886
#ifndef MAX_TANKS
7987
#define MAX_TANKS 8
8088
#endif
@@ -169,6 +177,7 @@ struct TankRuntime {
169177
uint8_t clearDebounceCount;
170178
// Sensor failure detection
171179
float lastValidReading;
180+
bool hasLastValidReading;
172181
uint8_t consecutiveFailures;
173182
uint8_t stuckReadingCount;
174183
bool sensorFailed;
@@ -225,6 +234,8 @@ static void sendTelemetry(uint8_t idx, const char *reason, bool syncNow);
225234
static void sendAlarm(uint8_t idx, const char *alarmType, float inches);
226235
static void sendDailyReport();
227236
static void publishNote(const char *fileName, const JsonDocument &doc, bool syncNow);
237+
static void bufferNoteForRetry(const char *fileName, const char *payload, bool syncNow);
238+
static void flushBufferedNotes();
228239
static void ensureTimeSync();
229240
static void updateDailyScheduleIfNeeded();
230241
static bool checkNotecardHealth();
@@ -272,6 +283,7 @@ void setup() {
272283
gTankState[i].lowAlarmDebounceCount = 0;
273284
gTankState[i].clearDebounceCount = 0;
274285
gTankState[i].lastValidReading = 0.0f;
286+
gTankState[i].hasLastValidReading = false;
275287
gTankState[i].consecutiveFailures = 0;
276288
gTankState[i].stuckReadingCount = 0;
277289
gTankState[i].sensorFailed = false;
@@ -591,6 +603,7 @@ static bool checkNotecardHealth() {
591603
gNotecardAvailable = true;
592604
gNotecardFailureCount = 0;
593605
gLastSuccessfulNotecardComm = millis();
606+
flushBufferedNotes();
594607
return true;
595608
}
596609

@@ -726,6 +739,7 @@ static void reinitializeHardware() {
726739
gTankState[i].stuckReadingCount = 0;
727740
gTankState[i].sensorFailed = false;
728741
gTankState[i].lastValidReading = 0.0f;
742+
gTankState[i].hasLastValidReading = false;
729743
}
730744

731745
Serial.println(F("Hardware reinitialized after config update"));
@@ -884,7 +898,7 @@ static bool validateSensorReading(uint8_t idx, float reading) {
884898
}
885899

886900
// Check for stuck sensor (same reading multiple times)
887-
if (state.lastValidReading > 0.0f && fabs(reading - state.lastValidReading) < 0.05f) {
901+
if (state.hasLastValidReading && fabs(reading - state.lastValidReading) < 0.05f) {
888902
state.stuckReadingCount++;
889903
if (state.stuckReadingCount >= SENSOR_STUCK_THRESHOLD) {
890904
if (!state.sensorFailed) {
@@ -928,6 +942,7 @@ static bool validateSensorReading(uint8_t idx, float reading) {
928942
publishNote(ALARM_FILE, doc, true);
929943
}
930944
state.lastValidReading = reading;
945+
state.hasLastValidReading = true;
931946
return true;
932947
}
933948

@@ -1206,9 +1221,7 @@ static void sendAlarm(uint8_t idx, const char *alarmType, float inches) {
12061221
}
12071222

12081223
const TankConfig &cfg = gConfig.tanks[idx];
1209-
if (!cfg.enableAlarmSms) {
1210-
return;
1211-
}
1224+
bool allowSmsEscalation = cfg.enableAlarmSms;
12121225

12131226
// Always activate local alarm regardless of rate limits
12141227
bool isAlarm = (strcmp(alarmType, "clear") != 0);
@@ -1233,6 +1246,7 @@ static void sendAlarm(uint8_t idx, const char *alarmType, float inches) {
12331246
doc["levelInches"] = inches;
12341247
doc["highThreshold"] = cfg.highAlarmInches;
12351248
doc["lowThreshold"] = cfg.lowAlarmInches;
1249+
doc["smsEnabled"] = allowSmsEscalation;
12361250
doc["smsPrimary"] = gConfig.smsPrimary;
12371251
doc["smsSecondary"] = gConfig.smsSecondary;
12381252
doc["time"] = currentEpoch();
@@ -1344,47 +1358,138 @@ static bool appendDailyTank(DynamicJsonDocument &doc, JsonArray &array, uint8_t
13441358
}
13451359

13461360
static void publishNote(const char *fileName, const JsonDocument &doc, bool syncNow) {
1347-
// Skip if notecard is offline - local alarms still work
1361+
// Build target file string and serialized payload once for both live send and buffering
1362+
char targetFile[80];
1363+
snprintf(targetFile, sizeof(targetFile), "fleet.%s:%s", gConfig.serverFleet, fileName);
1364+
1365+
char buffer[1024];
1366+
size_t len = serializeJson(doc, buffer, sizeof(buffer));
1367+
if (len == 0 || len >= sizeof(buffer)) {
1368+
return;
1369+
}
1370+
buffer[len] = '\0';
1371+
13481372
if (!gNotecardAvailable) {
1373+
bufferNoteForRetry(targetFile, buffer, syncNow);
13491374
return;
13501375
}
13511376

13521377
J *req = notecard.newRequest("note.add");
13531378
if (!req) {
13541379
gNotecardFailureCount++;
1380+
bufferNoteForRetry(targetFile, buffer, syncNow);
13551381
return;
13561382
}
13571383

1358-
// Use fleet-based targeting: send to server fleet's notefile
1359-
// Format: fleet.<fleetname>:<filename>
1360-
char targetFile[80];
1361-
snprintf(targetFile, sizeof(targetFile), "fleet.%s:%s", gConfig.serverFleet, fileName);
13621384
JAddStringToObject(req, "file", targetFile);
13631385
if (syncNow) {
13641386
JAddBoolToObject(req, "sync", true);
13651387
}
13661388

1367-
char buffer[1024];
1368-
size_t len = serializeJson(doc, buffer, sizeof(buffer));
1369-
if (len == 0 || len >= sizeof(buffer)) {
1370-
notecard.deleteRequest(req);
1371-
return;
1372-
}
1373-
1374-
buffer[len] = '\0';
13751389
J *body = JParse(buffer);
13761390
if (!body) {
13771391
notecard.deleteRequest(req);
1392+
bufferNoteForRetry(targetFile, buffer, syncNow);
13781393
return;
13791394
}
13801395

13811396
JAddItemToObject(req, "body", body);
1382-
13831397
bool success = notecard.sendRequest(req);
13841398
if (success) {
13851399
gLastSuccessfulNotecardComm = millis();
13861400
gNotecardFailureCount = 0;
1401+
flushBufferedNotes();
13871402
} else {
13881403
gNotecardFailureCount++;
1404+
bufferNoteForRetry(targetFile, buffer, syncNow);
1405+
}
1406+
}
1407+
1408+
static void bufferNoteForRetry(const char *fileName, const char *payload, bool syncNow) {
1409+
File file = LittleFS.open(NOTE_BUFFER_PATH, "a");
1410+
if (!file) {
1411+
Serial.println(F("Failed to open note buffer; dropping payload"));
1412+
return;
1413+
}
1414+
file.print(fileName);
1415+
file.print('\t');
1416+
file.print(syncNow ? '1' : '0');
1417+
file.print('\t');
1418+
file.println(payload);
1419+
file.close();
1420+
Serial.println(F("Note buffered for retry"));
1421+
}
1422+
1423+
static void flushBufferedNotes() {
1424+
if (!gNotecardAvailable) {
1425+
return;
1426+
}
1427+
if (!LittleFS.exists(NOTE_BUFFER_PATH)) {
1428+
return;
1429+
}
1430+
1431+
File src = LittleFS.open(NOTE_BUFFER_PATH, "r");
1432+
if (!src) {
1433+
return;
1434+
}
1435+
1436+
File tmp = LittleFS.open(NOTE_BUFFER_TEMP_PATH, "w");
1437+
if (!tmp) {
1438+
src.close();
1439+
return;
1440+
}
1441+
1442+
bool wroteFailures = false;
1443+
while (src.available()) {
1444+
String line = src.readStringUntil('\n');
1445+
line.trim();
1446+
if (line.length() == 0) {
1447+
continue;
1448+
}
1449+
1450+
int firstTab = line.indexOf('\t');
1451+
int secondTab = (firstTab >= 0) ? line.indexOf('\t', firstTab + 1) : -1;
1452+
if (firstTab < 0 || secondTab < 0) {
1453+
continue;
1454+
}
1455+
1456+
String fileName = line.substring(0, firstTab);
1457+
String syncToken = line.substring(firstTab + 1, secondTab);
1458+
bool syncNow = (syncToken == "1");
1459+
String payload = line.substring(secondTab + 1);
1460+
1461+
J *req = notecard.newRequest("note.add");
1462+
if (!req) {
1463+
wroteFailures = true;
1464+
tmp.println(line);
1465+
continue;
1466+
}
1467+
JAddStringToObject(req, "file", fileName.c_str());
1468+
if (syncNow) {
1469+
JAddBoolToObject(req, "sync", true);
1470+
}
1471+
1472+
J *body = JParse(payload.c_str());
1473+
if (!body) {
1474+
notecard.deleteRequest(req);
1475+
continue;
1476+
}
1477+
JAddItemToObject(req, "body", body);
1478+
1479+
if (!notecard.sendRequest(req)) {
1480+
wroteFailures = true;
1481+
tmp.println(line);
1482+
}
1483+
}
1484+
1485+
src.close();
1486+
tmp.close();
1487+
1488+
if (wroteFailures) {
1489+
LittleFS.remove(NOTE_BUFFER_PATH);
1490+
LittleFS.rename(NOTE_BUFFER_TEMP_PATH, NOTE_BUFFER_PATH);
1491+
} else {
1492+
LittleFS.remove(NOTE_BUFFER_PATH);
1493+
LittleFS.remove(NOTE_BUFFER_TEMP_PATH);
13891494
}
13901495
}

TankAlarm-112025-Server-BluesOpta/TankAlarm-112025-Server-BluesOpta.ino

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ struct TankRecord {
129129
float levelInches;
130130
float percent;
131131
bool alarmActive;
132-
char alarmType[8];
132+
char alarmType[24];
133133
double lastUpdateEpoch;
134134
// Rate limiting for SMS alerts
135135
double lastSmsAlertEpoch;
@@ -806,7 +806,12 @@ static void sendDashboard(EthernetClient &client);
806806
static void sendTankJson(EthernetClient &client);
807807
static void sendClientDataJson(EthernetClient &client);
808808
static void handleConfigPost(EthernetClient &client, const String &body);
809-
static void dispatchClientConfig(const char *clientUid, JsonVariantConst cfgObj);
809+
enum class ConfigDispatchStatus : uint8_t {
810+
Ok = 0,
811+
PayloadTooLarge,
812+
NotecardFailure
813+
};
814+
static ConfigDispatchStatus dispatchClientConfig(const char *clientUid, JsonVariantConst cfgObj);
810815
static void pollNotecard();
811816
static void processNotefile(const char *fileName, void (*handler)(JsonDocument &, double));
812817
static void handleTelemetry(JsonDocument &doc, double epoch);
@@ -1439,25 +1444,33 @@ static void handleConfigPost(EthernetClient &client, const String &body) {
14391444
if (doc.containsKey("client") && doc.containsKey("config")) {
14401445
const char *clientUid = doc["client"].as<const char *>();
14411446
if (clientUid && strlen(clientUid) > 0) {
1442-
dispatchClientConfig(clientUid, doc["config"]);
1447+
ConfigDispatchStatus status = dispatchClientConfig(clientUid, doc["config"]);
1448+
if (status == ConfigDispatchStatus::PayloadTooLarge) {
1449+
respondStatus(client, 413, F("Config payload too large"));
1450+
return;
1451+
}
1452+
if (status == ConfigDispatchStatus::NotecardFailure) {
1453+
respondStatus(client, 500, F("Failed to queue config"));
1454+
return;
1455+
}
14431456
}
14441457
}
14451458

14461459
respondStatus(client, 200, F("OK"));
14471460
}
14481461

1449-
static void dispatchClientConfig(const char *clientUid, JsonVariantConst cfgObj) {
1462+
static ConfigDispatchStatus dispatchClientConfig(const char *clientUid, JsonVariantConst cfgObj) {
14501463
char buffer[1536];
14511464
size_t len = serializeJson(cfgObj, buffer, sizeof(buffer));
14521465
if (len == 0 || len >= sizeof(buffer)) {
14531466
Serial.println(F("Client config payload too large"));
1454-
return;
1467+
return ConfigDispatchStatus::PayloadTooLarge;
14551468
}
14561469
buffer[len] = '\0';
14571470

14581471
J *req = notecard.newRequest("note.add");
14591472
if (!req) {
1460-
return;
1473+
return ConfigDispatchStatus::NotecardFailure;
14611474
}
14621475
// Use device-specific targeting: send directly to client's config.qi inbox
14631476
char targetFile[80];
@@ -1468,15 +1481,20 @@ static void dispatchClientConfig(const char *clientUid, JsonVariantConst cfgObj)
14681481
J *body = JParse(buffer);
14691482
if (!body) {
14701483
notecard.deleteRequest(req);
1471-
return;
1484+
return ConfigDispatchStatus::PayloadTooLarge;
14721485
}
14731486
JAddItemToObject(req, "body", body);
1474-
notecard.sendRequest(req);
1487+
bool queued = notecard.sendRequest(req);
1488+
if (!queued) {
1489+
return ConfigDispatchStatus::NotecardFailure;
1490+
}
14751491

14761492
cacheClientConfigFromBuffer(clientUid, buffer);
14771493

14781494
Serial.print(F("Queued config update for client " ));
14791495
Serial.println(clientUid);
1496+
1497+
return ConfigDispatchStatus::Ok;
14801498
}
14811499

14821500
static void pollNotecard() {
@@ -1547,10 +1565,17 @@ static void handleAlarm(JsonDocument &doc, double epoch) {
15471565

15481566
const char *type = doc["type"] | "";
15491567
float inches = doc["levelInches"].as<float>();
1568+
bool isDiagnostic = (strcmp(type, "sensor-fault") == 0) ||
1569+
(strcmp(type, "sensor-stuck") == 0) ||
1570+
(strcmp(type, "sensor-recovered") == 0);
1571+
bool isRecovery = (strcmp(type, "sensor-recovered") == 0);
15501572

1551-
if (strcmp(type, "clear") == 0) {
1573+
if (strcmp(type, "clear") == 0 || isRecovery) {
15521574
rec->alarmActive = false;
15531575
strlcpy(rec->alarmType, "clear", sizeof(rec->alarmType));
1576+
if (isRecovery) {
1577+
strlcpy(rec->alarmType, type, sizeof(rec->alarmType));
1578+
}
15541579
} else {
15551580
rec->alarmActive = true;
15561581
strlcpy(rec->alarmType, type, sizeof(rec->alarmType));
@@ -1567,7 +1592,8 @@ static void handleAlarm(JsonDocument &doc, double epoch) {
15671592
rec->lastUpdateEpoch = (epoch > 0.0) ? epoch : currentEpoch();
15681593

15691594
// Check rate limit before sending SMS
1570-
if (checkSmsRateLimit(rec)) {
1595+
bool smsEnabled = !doc.containsKey("smsEnabled") || doc["smsEnabled"].as<bool>();
1596+
if (!isDiagnostic && smsEnabled && checkSmsRateLimit(rec)) {
15711597
char message[160];
15721598
snprintf(message, sizeof(message), "%s #%d %s alarm %.1f in", rec->site, rec->tankNumber, rec->alarmType, inches);
15731599
sendSmsAlert(message);

0 commit comments

Comments
 (0)