@@ -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);
225234static void sendAlarm (uint8_t idx, const char *alarmType, float inches);
226235static void sendDailyReport ();
227236static 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 ();
228239static void ensureTimeSync ();
229240static void updateDailyScheduleIfNeeded ();
230241static 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
13461360static 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}
0 commit comments