-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserial_commands.cpp
More file actions
1352 lines (1163 loc) · 47.2 KB
/
Copy pathserial_commands.cpp
File metadata and controls
1352 lines (1163 loc) · 47.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// serial_commands.cpp
// Serial command parser for USB time setting and configuration
// Uses ESP32 internal RTC (gettimeofday/settimeofday)
#include "config.h"
#if ENABLE_SERIAL_COMMANDS
#include "serial_commands.h"
#include "storage.h"
#include "drinks.h"
#include "storage_drinks.h"
#include "weight.h"
#include "config.h"
#include <Preferences.h>
#include <sys/time.h>
#include <time.h>
#include <RTClib.h> // Adafruit RTClib for DS3231
// Command buffer for serial input
#define CMD_BUFFER_SIZE 128
static char cmdBuffer[CMD_BUFFER_SIZE];
static uint8_t cmdBufferPos = 0;
// Callback for time set events
static OnTimeSetCallback g_onTimeSetCallback = nullptr;
// External variables (managed in main.cpp)
extern int8_t g_timezone_offset;
extern bool g_rtc_ds3231_present;
extern RTC_DS3231 rtc;
// Runtime debug control variables (managed in main.cpp)
extern bool g_debug_enabled;
extern bool g_debug_water_level;
extern bool g_debug_accelerometer;
extern bool g_debug_display;
extern bool g_debug_drink_tracking;
extern bool g_debug_calibration;
extern bool g_debug_ble;
// Runtime display mode variable (managed in main.cpp)
extern uint8_t g_daily_intake_display_mode;
// Runtime sleep timeout variable (managed in main.cpp)
extern uint32_t g_sleep_timeout_ms;
// Low battery lockout (RTC variables managed in main.cpp)
extern bool rtc_low_battery_lockout;
extern uint8_t rtc_low_battery_threshold;
// Initialize serial command handler
void serialCommandsInit() {
cmdBufferPos = 0;
cmdBuffer[0] = '\0';
}
// Register callback for time set events
void serialCommandsSetTimeCallback(OnTimeSetCallback callback) {
g_onTimeSetCallback = callback;
}
// Parse integer from string
// Returns true if successful, false otherwise
static bool parseInt(const char* str, int& value) {
char* endptr;
long result = strtol(str, &endptr, 10);
if (endptr == str || *endptr != '\0') {
return false;
}
value = (int)result;
return true;
}
// Validate date components
static bool validateDate(int year, int month, int day) {
if (year < 2026 || year > 2099) {
Serial.println("ERROR: Year must be 2026-2099");
return false;
}
if (month < 1 || month > 12) {
Serial.println("ERROR: Month must be 1-12");
return false;
}
// Days in month (non-leap year, good enough for validation)
const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int maxDay = daysInMonth[month - 1];
// Leap year check
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
maxDay = 29;
}
if (day < 1 || day > maxDay) {
Serial.printf("ERROR: Day must be 1-%d for month %d\n", maxDay, month);
return false;
}
return true;
}
// Validate time components
static bool validateTime(int hour, int minute, int second) {
if (hour < 0 || hour > 23) {
Serial.println("ERROR: Hour must be 0-23");
return false;
}
if (minute < 0 || minute > 59) {
Serial.println("ERROR: Minute must be 0-59");
return false;
}
if (second < 0 || second > 59) {
Serial.println("ERROR: Second must be 0-59");
return false;
}
return true;
}
// Validate timezone offset
static bool validateTimezone(int offset) {
if (offset < -12 || offset > 14) {
Serial.println("ERROR: Timezone must be -12 to +14");
return false;
}
return true;
}
// Get timezone name for common offsets
static const char* getTimezoneName(int offset) {
switch (offset) {
case -8: return "PST";
case -7: return "MST";
case -6: return "CST";
case -5: return "EST";
case 0: return "UTC";
case 1: return "CET";
default: return "";
}
}
// Handle SET DATETIME command
// Format: SET DATETIME 2026-01-13 14:30:00 -5
static void handleSetDateTime(char* args) {
// Parse date and time
int year, month, day, hour, minute, second;
int timezone_offset = g_timezone_offset; // Default to current offset
// Try to parse date, time, and optional timezone
int parsed = sscanf(args, "%d-%d-%d %d:%d:%d %d",
&year, &month, &day, &hour, &minute, &second, &timezone_offset);
if (parsed < 6) {
Serial.println("ERROR: Invalid format");
Serial.println("Usage: SET DATETIME YYYY-MM-DD HH:MM:SS [timezone_offset]");
Serial.println("Example: SET DATETIME 2026-01-13 14:30:00 -5");
return;
}
// Validate components
if (!validateDate(year, month, day)) return;
if (!validateTime(hour, minute, second)) return;
if (!validateTimezone(timezone_offset)) return;
// Create tm structure with the user's LOCAL time
struct tm timeinfo = {};
timeinfo.tm_year = year - 1900; // tm_year is years since 1900
timeinfo.tm_mon = month - 1; // tm_mon is 0-11
timeinfo.tm_mday = day;
timeinfo.tm_hour = hour;
timeinfo.tm_min = minute;
timeinfo.tm_sec = second;
timeinfo.tm_isdst = -1; // Let mktime determine DST
// Convert to Unix timestamp
// mktime() uses system timezone, but we'll adjust for our custom offset
time_t timestamp = mktime(&timeinfo);
if (timestamp == -1) {
Serial.println("ERROR: Failed to convert time");
return;
}
// Adjust for timezone offset (mktime assumes system timezone)
timestamp -= timezone_offset * 3600;
// Set ESP32 internal RTC
struct timeval tv;
tv.tv_sec = timestamp;
tv.tv_usec = 0;
if (settimeofday(&tv, NULL) != 0) {
Serial.println("ERROR: Failed to set RTC");
return;
}
// Also update DS3231 if present
if (g_rtc_ds3231_present) {
rtc.adjust(DateTime(timestamp));
Serial.println("DS3231 RTC updated");
}
// Save timezone offset to NVS
if (!storageSaveTimezone(timezone_offset)) {
Serial.println("WARNING: Failed to save timezone to NVS");
}
// Save time_valid flag to NVS
if (!storageSaveTimeValid(true)) {
Serial.println("WARNING: Failed to save time_valid flag to NVS");
}
// Update global timezone offset
g_timezone_offset = timezone_offset;
// Save current timestamp to NVS for time persistence
storageSaveLastBootTime(timestamp);
// Format success message with local time
char timeStr[64];
const char* tzName = getTimezoneName(timezone_offset);
if (strlen(tzName) > 0) {
snprintf(timeStr, sizeof(timeStr), "Time set: %04d-%02d-%02d %02d:%02d:%02d %s (%+d)",
year, month, day,
hour + timezone_offset, minute, second,
tzName, timezone_offset);
} else {
snprintf(timeStr, sizeof(timeStr), "Time set: %04d-%02d-%02d %02d:%02d:%02d (UTC%+d)",
year, month, day,
hour + timezone_offset, minute, second,
timezone_offset);
}
Serial.println(timeStr);
Serial.println("Timezone and time_valid flag saved to NVS");
// FIX Bug #5: Initialize drink tracking when time becomes valid
extern bool g_time_valid; // Forward declaration
g_time_valid = true;
drinksInit();
// Call callback if registered
if (g_onTimeSetCallback != nullptr) {
g_onTimeSetCallback();
}
}
// Handle GET TIME command
static void handleGetTime() {
// Load time_valid flag
bool time_valid = storageLoadTimeValid();
if (!time_valid) {
Serial.println("WARNING: Time not set!");
Serial.println("Current RTC: 1970-01-01 00:00:00 (epoch)");
Serial.println("Use SET DATETIME command to set time");
Serial.println("Example: SET DATETIME 2026-01-13 14:30:00 -5");
return;
}
// Get current time from RTC
struct timeval tv;
gettimeofday(&tv, NULL);
time_t now = tv.tv_sec;
// Convert to local time using timezone offset
now += g_timezone_offset * 3600; // Add timezone offset in seconds
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
// Format time string
char timeStr[128];
const char* tzName = getTimezoneName(g_timezone_offset);
if (strlen(tzName) > 0) {
snprintf(timeStr, sizeof(timeStr), "Current time: %04d-%02d-%02d %02d:%02d:%02d %s (%+d)",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
tzName, g_timezone_offset);
} else {
snprintf(timeStr, sizeof(timeStr), "Current time: %04d-%02d-%02d %02d:%02d:%02d (UTC%+d)",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
g_timezone_offset);
}
Serial.println(timeStr);
Serial.println("Time valid: Yes");
// Show RTC source and accuracy
if (g_rtc_ds3231_present) {
Serial.println("RTC source: DS3231 external RTC (\u00b12-3min/year)");
Serial.println("Battery-backed: Yes (CR1220)");
} else {
Serial.println("RTC source: ESP32 internal RTC (\u00b12-10min/day)");
Serial.println("Resync recommended: Weekly via USB");
}
}
// Handle SET DATE command
// Format: SET DATE 2026-01-13
static void handleSetDate(char* args) {
int year, month, day;
// Parse date
int parsed = sscanf(args, "%d-%d-%d", &year, &month, &day);
if (parsed != 3) {
Serial.println("ERROR: Invalid format");
Serial.println("Usage: SET DATE YYYY-MM-DD");
Serial.println("Example: SET DATE 2026-01-13");
return;
}
// Validate date
if (!validateDate(year, month, day)) return;
// Get current time from RTC
struct timeval tv;
gettimeofday(&tv, NULL);
time_t now = tv.tv_sec;
// Convert current time to struct tm with timezone offset
now += g_timezone_offset * 3600;
struct tm current_time;
gmtime_r(&now, ¤t_time);
// Update only the date fields, keep existing time
current_time.tm_year = year - 1900;
current_time.tm_mon = month - 1;
current_time.tm_mday = day;
// Convert back to Unix timestamp
time_t new_timestamp = mktime(¤t_time);
if (new_timestamp == -1) {
Serial.println("ERROR: Failed to convert time");
return;
}
// Adjust for timezone offset
new_timestamp -= g_timezone_offset * 3600;
// Set ESP32 internal RTC
tv.tv_sec = new_timestamp;
tv.tv_usec = 0;
if (settimeofday(&tv, NULL) != 0) {
Serial.println("ERROR: Failed to set RTC");
return;
}
// Also update DS3231 if present
if (g_rtc_ds3231_present) {
rtc.adjust(DateTime(new_timestamp));
Serial.println("DS3231 RTC updated");
}
// Save current timestamp to NVS for time persistence
storageSaveLastBootTime(new_timestamp);
// Save time_valid flag if not already set
if (!storageLoadTimeValid()) {
if (!storageSaveTimeValid(true)) {
Serial.println("WARNING: Failed to save time_valid flag to NVS");
}
// Initialize drink tracking if time just became valid
extern bool g_time_valid;
g_time_valid = true;
drinksInit();
// Call callback if registered
if (g_onTimeSetCallback != nullptr) {
g_onTimeSetCallback();
}
}
Serial.printf("Date set: %04d-%02d-%02d (time preserved: %02d:%02d:%02d)\n",
year, month, day,
current_time.tm_hour, current_time.tm_min, current_time.tm_sec);
}
// Handle SET TIME command
// Format: SET TIME HH[:MM[:SS]]
static void handleSetTime(char* args) {
int hour, minute = 0, second = 0;
// Parse time - allow HH, HH:MM, or HH:MM:SS
int parsed = sscanf(args, "%d:%d:%d", &hour, &minute, &second);
if (parsed < 1) {
Serial.println("ERROR: Invalid format");
Serial.println("Usage: SET TIME HH[:MM[:SS]]");
Serial.println("Examples:");
Serial.println(" SET TIME 14 → 14:00:00");
Serial.println(" SET TIME 14:30 → 14:30:00");
Serial.println(" SET TIME 14:30:45 → 14:30:45");
return;
}
// Validate time
if (!validateTime(hour, minute, second)) return;
// Get current time from RTC
struct timeval tv;
gettimeofday(&tv, NULL);
time_t now = tv.tv_sec;
// Convert current time to struct tm with timezone offset
now += g_timezone_offset * 3600;
struct tm current_time;
gmtime_r(&now, ¤t_time);
// Update only the time fields, keep existing date
current_time.tm_hour = hour;
current_time.tm_min = minute;
current_time.tm_sec = second;
// Convert back to Unix timestamp
time_t new_timestamp = mktime(¤t_time);
if (new_timestamp == -1) {
Serial.println("ERROR: Failed to convert time");
return;
}
// Adjust for timezone offset
new_timestamp -= g_timezone_offset * 3600;
// Set ESP32 internal RTC
tv.tv_sec = new_timestamp;
tv.tv_usec = 0;
if (settimeofday(&tv, NULL) != 0) {
Serial.println("ERROR: Failed to set RTC");
return;
}
// Also update DS3231 if present
if (g_rtc_ds3231_present) {
rtc.adjust(DateTime(new_timestamp));
Serial.println("DS3231 RTC updated");
}
// Save current timestamp to NVS for time persistence
storageSaveLastBootTime(new_timestamp);
// Save time_valid flag if not already set
if (!storageLoadTimeValid()) {
if (!storageSaveTimeValid(true)) {
Serial.println("WARNING: Failed to save time_valid flag to NVS");
}
// Initialize drink tracking if time just became valid
extern bool g_time_valid;
g_time_valid = true;
drinksInit();
// Call callback if registered
if (g_onTimeSetCallback != nullptr) {
g_onTimeSetCallback();
}
}
Serial.printf("Time set: %02d:%02d:%02d (date preserved: %04d-%02d-%02d)\n",
hour, minute, second,
current_time.tm_year + 1900, current_time.tm_mon + 1, current_time.tm_mday);
}
// Handle SET TIMEZONE command (and SET TZ alias)
static void handleSetTimezone(char* args) {
int offset;
if (!parseInt(args, offset)) {
Serial.println("ERROR: Invalid timezone offset");
Serial.println("Usage: SET TIMEZONE offset (or SET TZ offset)");
Serial.println("Example: SET TIMEZONE -8");
return;
}
if (!validateTimezone(offset)) return;
// Save to NVS
if (!storageSaveTimezone(offset)) {
Serial.println("ERROR: Failed to save timezone to NVS");
return;
}
// Update global offset
g_timezone_offset = offset;
const char* tzName = getTimezoneName(offset);
if (strlen(tzName) > 0) {
Serial.printf("Timezone set: %+d hours (%s)\n", offset, tzName);
} else {
Serial.printf("Timezone set: UTC%+d\n", offset);
}
Serial.println("Saved to NVS");
}
// Handle GET DAILY STATE command - display current daily state
static void handleGetDailyState() {
DailyState state;
drinksGetState(state);
uint16_t daily_total = drinksGetDailyTotal();
uint16_t drink_count = drinksGetDrinkCount();
Serial.println("\n=== DAILY STATE ===");
Serial.printf("Daily total: %dml / %dml (%d%%)\n",
daily_total,
DRINK_DAILY_GOAL_DEFAULT_ML,
(daily_total * 100) / DRINK_DAILY_GOAL_DEFAULT_ML);
Serial.printf("Drink count: %d drinks today\n", drink_count);
Serial.printf("Last baseline ADC: %d\n", state.last_recorded_adc);
Serial.printf("Last displayed: %dml\n", state.last_displayed_total_ml);
Serial.println("==================\n");
}
// Handle GET LAST DRINK command - display most recent drink record
static void handleGetLastDrink() {
DrinkRecord record;
if (!storageLoadLastDrinkRecord(record)) {
Serial.println("No drink records found");
return;
}
const char* drink_type = (record.type == DRINK_TYPE_POUR) ? "POUR" : "GULP";
Serial.println("\n=== LAST DRINK RECORD ===");
// Format timestamp
time_t t = record.timestamp;
struct tm tm;
gmtime_r(&t, &tm);
Serial.printf("Time: %04d-%02d-%02d %02d:%02d:%02d\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
Serial.printf("Amount: %dml (%s)\n", record.amount_ml, drink_type);
Serial.printf("Bottle level: %dml\n", record.bottle_level_ml);
Serial.printf("Flags: 0x%02X (", record.flags);
if (record.flags & 0x01) Serial.print("synced ");
if (record.flags & 0x02) Serial.print("day_boundary");
if (record.flags == 0) Serial.print("not synced");
Serial.println(")");
Serial.println("=========================\n");
}
// Handle DUMP DRINKS command - display all drink records
static void handleDumpDrinks() {
CircularBufferMetadata meta;
if (!storageLoadBufferMetadata(meta)) {
Serial.println("No drink records in buffer");
return;
}
Serial.println("\n=== DRINK BUFFER METADATA ===");
Serial.printf("Write index: %d\n", meta.write_index);
Serial.printf("Record count: %d\n", meta.record_count);
Serial.printf("Total writes: %u\n", meta.total_writes);
Serial.println("=============================\n");
if (meta.record_count == 0) {
Serial.println("No drink records stored");
return;
}
Serial.printf("Showing %d most recent drinks:\n\n", meta.record_count);
// Calculate starting index (oldest record in buffer)
uint16_t start_index = (meta.record_count < DRINK_MAX_RECORDS)
? 0
: meta.write_index;
// Display records in chronological order
for (uint16_t i = 0; i < meta.record_count; i++) {
uint16_t index = (start_index + i) % DRINK_MAX_RECORDS;
// Load record directly from NVS
char key[16];
snprintf(key, sizeof(key), "drink_%03d", index);
DrinkRecord record;
Preferences prefs;
if (prefs.begin(NVS_NAMESPACE, true)) {
size_t read_size = prefs.getBytes(key, &record, sizeof(DrinkRecord));
prefs.end();
if (read_size == sizeof(DrinkRecord)) {
time_t t = record.timestamp;
struct tm tm;
gmtime_r(&t, &tm);
const char* drink_type = (record.type == DRINK_TYPE_POUR) ? "POUR" : "GULP";
Serial.printf("[%03d] %04d-%02d-%02d %02d:%02d:%02d | %+5dml (%s) | Level: %4dml | Flags: 0x%02X\n",
i,
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
record.amount_ml,
drink_type,
record.bottle_level_ml,
record.flags);
}
}
}
Serial.println();
}
// Handle RESET DAILY INTAKE command - reset daily counter
static void handleResetDailyIntake() {
drinksResetDaily();
Serial.println("OK: Daily intake reset");
}
// Note: SET DAILY INTAKE command removed - daily totals are now computed from records
// Handle CLEAR DRINKS command - clear all drink records
static void handleClearDrinks() {
drinksClearAll();
Serial.println("OK: All drink records cleared");
}
// Handle SET DISPLAY MODE command - switch between man and tumblers
static void handleSetDisplayMode(char* args) {
int mode;
if (!parseInt(args, mode)) {
Serial.println("ERROR: Invalid display mode");
Serial.println("Usage: SET DISPLAY MODE mode");
Serial.println(" 0 = Human figure (continuous fill)");
Serial.println(" 1 = Tumbler grid (10 glasses)");
return;
}
if (mode < 0 || mode > 1) {
Serial.println("ERROR: Display mode must be 0 or 1");
Serial.println(" 0 = Human figure (continuous fill)");
Serial.println(" 1 = Tumbler grid (10 glasses)");
return;
}
// Save to NVS
if (!storageSaveDisplayMode((uint8_t)mode)) {
Serial.println("ERROR: Failed to save display mode to NVS");
return;
}
// Update global variable
g_daily_intake_display_mode = (uint8_t)mode;
const char* mode_name = (mode == 0) ? "Human figure" : "Tumbler grid";
Serial.printf("Display mode set: %d (%s)\n", mode, mode_name);
Serial.println("Saved to NVS");
Serial.println("Display updated");
}
// Handle SET SLEEP TIMEOUT command
// Format: SET SLEEP TIMEOUT seconds (0 = disable sleep)
static void handleSetSleepTimeout(char* args) {
int seconds;
if (!parseInt(args, seconds)) {
Serial.println("ERROR: Invalid timeout");
Serial.println("Usage: SET SLEEP TIMEOUT seconds");
Serial.println(" 0 = Disable sleep (debug mode)");
Serial.println(" 1-300 = Sleep after N seconds");
Serial.println("Examples:");
Serial.println(" SET SLEEP TIMEOUT 30 \xE2\x86\x92 Sleep after 30 seconds (default)");
Serial.println(" SET SLEEP TIMEOUT 0 \xE2\x86\x92 Never sleep (for debugging)");
return;
}
if (seconds < 0 || seconds > 300) {
Serial.println("ERROR: Timeout must be 0-300 seconds");
return;
}
g_sleep_timeout_ms = (uint32_t)seconds * 1000;
// Save to NVS for persistence
if (storageSaveSleepTimeout((uint32_t)seconds)) {
if (seconds == 0) {
Serial.println("Sleep DISABLED (debug mode)");
Serial.println("Device will never enter deep sleep");
Serial.println("Setting saved to NVS - persists across reboots");
} else {
Serial.printf("Sleep timeout set: %d seconds\n", seconds);
Serial.println("Setting saved to NVS - persists across reboots");
}
} else {
Serial.println("WARNING: Failed to save to NVS - will reset to default on reboot");
}
}
// Handle SET EXTENDED SLEEP TIMER command
// Format: SET EXTENDED SLEEP TIMER seconds
static void handleSetExtendedSleepTimer(char* args) {
int seconds;
if (!parseInt(args, seconds)) {
Serial.println("ERROR: Invalid timer duration");
Serial.println("Usage: SET EXTENDED SLEEP TIMER seconds");
Serial.println(" 1-3600 = Timer wake interval in extended mode");
Serial.println("Examples:");
Serial.println(" SET EXTENDED SLEEP TIMER 60 - 1 minute timer wake (default)");
Serial.println(" SET EXTENDED SLEEP TIMER 120 - 2 minute timer wake");
return;
}
if (seconds < 1 || seconds > 3600) {
Serial.println("ERROR: Timer duration must be 1-3600 seconds");
return;
}
extern uint32_t g_extended_sleep_timer_sec;
g_extended_sleep_timer_sec = (uint32_t)seconds;
// Save to NVS for persistence
if (storageSaveExtendedSleepTimer((uint32_t)seconds)) {
Serial.printf("Extended sleep timer set: %d seconds\n", seconds);
Serial.println("Setting saved to NVS - persists across reboots");
} else {
Serial.println("WARNING: Failed to save to NVS - will reset to default on reboot");
}
}
// Handle SET EXTENDED SLEEP THRESHOLD command
// Format: SET EXTENDED SLEEP THRESHOLD seconds
static void handleSetExtendedSleepThreshold(char* args) {
int seconds;
if (!parseInt(args, seconds)) {
Serial.println("ERROR: Invalid threshold");
Serial.println("Usage: SET EXTENDED SLEEP THRESHOLD seconds");
Serial.println(" 30-600 = Continuous awake threshold before extended mode");
Serial.println("Examples:");
Serial.println(" SET EXTENDED SLEEP THRESHOLD 120 - 2 minutes (default)");
Serial.println(" SET EXTENDED SLEEP THRESHOLD 60 - 1 minute");
return;
}
if (seconds < 30 || seconds > 600) {
Serial.println("ERROR: Threshold must be 30-600 seconds");
return;
}
extern uint32_t g_time_since_stable_threshold_sec;
g_time_since_stable_threshold_sec = (uint32_t)seconds;
// Save to NVS for persistence
if (storageSaveExtendedSleepThreshold((uint32_t)seconds)) {
Serial.printf("Extended sleep threshold set: %d seconds\n", seconds);
Serial.println("Setting saved to NVS - persists across reboots");
} else {
Serial.println("WARNING: Failed to save to NVS - will reset to default on reboot");
}
}
// Handle TARE command - zero the scale at current weight
static void handleTare() {
// Check if NAU7802 is ready
if (!weightIsReady()) {
Serial.println("ERROR: NAU7802 not ready");
return;
}
Serial.println("Taking tare reading...");
// Take a quick weight measurement (2 seconds)
WeightConfig quick_config = weightGetDefaultConfig();
quick_config.duration_seconds = 2;
WeightMeasurement tare_measurement = weightMeasureStable(quick_config);
if (!tare_measurement.valid) {
Serial.println("ERROR: Failed to get stable tare reading");
return;
}
// Save as tare offset
CalibrationData cal;
if (storageLoadCalibration(cal)) {
// Update the empty bottle reading to current reading (tare)
cal.empty_bottle_adc = tare_measurement.raw_adc;
// Recalculate scale factor (if we have a valid full bottle reading)
if (cal.calibration_valid && cal.full_bottle_adc != cal.empty_bottle_adc) {
cal.scale_factor = (float)(cal.full_bottle_adc - cal.empty_bottle_adc) / 830.0f;
// Validate the new scale factor - prevent creating invalid calibration (Issue #84)
if (cal.scale_factor < CALIBRATION_SCALE_FACTOR_MIN ||
cal.scale_factor > CALIBRATION_SCALE_FACTOR_MAX) {
Serial.printf("WARNING: Tare would create invalid scale_factor (%.2f)\n", cal.scale_factor);
Serial.printf("Valid range: %.0f - %.0f counts/g\n",
CALIBRATION_SCALE_FACTOR_MIN, CALIBRATION_SCALE_FACTOR_MAX);
Serial.println("Full recalibration required - run standalone calibration");
cal.calibration_valid = 0;
}
}
// Save updated calibration
if (storageSaveCalibration(cal)) {
Serial.println("OK: Tare set successfully");
Serial.printf("New tare ADC: %d\n", cal.empty_bottle_adc);
if (cal.calibration_valid) {
Serial.printf("Updated scale factor: %.2f counts/g\n", cal.scale_factor);
}
// Force display refresh to show updated water level
extern void forceDisplayRefresh();
forceDisplayRefresh();
} else {
Serial.println("ERROR: Failed to save tare offset");
}
} else {
// No existing calibration, create a new one with just tare
cal = storageGetEmptyCalibration();
cal.empty_bottle_adc = tare_measurement.raw_adc;
cal.calibration_valid = 0; // Not fully calibrated yet
if (storageSaveCalibration(cal)) {
Serial.println("OK: Tare set successfully");
Serial.printf("Tare ADC: %d\n", cal.empty_bottle_adc);
Serial.println("Note: Full calibration still required (SET FULL BOTTLE)");
// Force display refresh to show updated water level
extern void forceDisplayRefresh();
forceDisplayRefresh();
} else {
Serial.println("ERROR: Failed to save tare offset");
}
}
}
// Handle SET LOW BATTERY LOCKOUT command
// Format: SET LOW BATTERY LOCKOUT percent
static void handleSetLowBatteryLockout(char* args) {
int percent;
if (!parseInt(args, percent)) {
Serial.println("ERROR: Invalid percentage");
Serial.println("Usage: SET LOW BATTERY LOCKOUT percent");
Serial.println(" 5-95 = Lockout threshold (default=20)");
Serial.println("Examples:");
Serial.println(" SET LOW BATTERY LOCKOUT 20 - Production (default)");
Serial.println(" SET LOW BATTERY LOCKOUT 85 - Testing (locks out immediately)");
return;
}
if (percent < 5 || percent > 95) {
Serial.println("ERROR: Threshold must be 5-95%%");
return;
}
// Save to NVS and update RTC variable
if (storageSaveLowBatteryThreshold((uint8_t)percent)) {
rtc_low_battery_threshold = (uint8_t)percent;
Serial.printf("Low battery lockout threshold set: %d%%\n", percent);
Serial.printf("Recovery threshold: %d%%\n", percent + LOW_BATTERY_RECOVERY_OFFSET);
Serial.println("Setting saved to NVS - persists across reboots");
} else {
Serial.println("WARNING: Failed to save to NVS");
}
}
// Handle GET LOW BATTERY command
static void handleGetLowBattery() {
Serial.println("\n=== LOW BATTERY STATUS ===");
Serial.printf("Lockout threshold: %d%%\n", rtc_low_battery_threshold);
Serial.printf("Recovery threshold: %d%%\n", rtc_low_battery_threshold + LOW_BATTERY_RECOVERY_OFFSET);
Serial.printf("Check interval: %d seconds\n", LOW_BATTERY_CHECK_INTERVAL_SEC);
Serial.printf("Lockout active: %s\n", rtc_low_battery_lockout ? "YES" : "NO");
Serial.println("==========================\n");
}
// Handle GET STATUS command - show all system status
static void handleGetStatus() {
extern bool g_calibrated;
extern int8_t g_timezone_offset;
extern bool g_time_valid;
extern uint8_t g_daily_intake_display_mode;
extern uint32_t g_sleep_timeout_ms;
extern uint32_t g_extended_sleep_timer_sec;
extern uint32_t g_time_since_stable_threshold_sec;
extern bool g_in_extended_sleep_mode;
extern unsigned long g_time_since_stable_start;
Serial.println("\n=== SYSTEM STATUS ===");
// Calibration status
Serial.print("Calibration: ");
Serial.println(g_calibrated ? "VALID" : "NOT CALIBRATED");
// Time configuration
Serial.print("Time valid: ");
Serial.println(g_time_valid ? "YES" : "NO");
if (g_time_valid) {
Serial.print("Timezone offset: ");
Serial.println(g_timezone_offset);
// Show current time
struct timeval tv;
gettimeofday(&tv, NULL);
time_t now = tv.tv_sec + (g_timezone_offset * 3600);
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
Serial.printf("Current time: %04d-%02d-%02d %02d:%02d:%02d\n",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
}
// Display mode
Serial.print("Display mode: ");
Serial.print(g_daily_intake_display_mode);
Serial.print(" (");
Serial.print((g_daily_intake_display_mode == 0) ? "Human figure" : "Tumbler grid");
Serial.println(")");
// Sleep settings
Serial.print("Normal sleep timeout: ");
if (g_sleep_timeout_ms == 0) {
Serial.println("DISABLED");
} else {
Serial.print(g_sleep_timeout_ms / 1000);
Serial.println(" seconds");
}
Serial.print("Extended sleep timer: ");
Serial.print(g_extended_sleep_timer_sec);
Serial.println(" seconds");
Serial.print("Time-since-stable threshold: ");
Serial.print(g_time_since_stable_threshold_sec);
Serial.println(" seconds");
Serial.print("Extended sleep mode: ");
Serial.println(g_in_extended_sleep_mode ? "ACTIVE" : "INACTIVE");
if (g_time_since_stable_start > 0) {
unsigned long time_since_stable = (millis() - g_time_since_stable_start) / 1000;
Serial.print("Time since stable: ");
Serial.print(time_since_stable);
Serial.println(" seconds");
}
// Low battery lockout
Serial.printf("Low battery lockout: %s (threshold: %d%%, recovery: %d%%)\n",
rtc_low_battery_lockout ? "ACTIVE" : "inactive",
rtc_low_battery_threshold,
rtc_low_battery_threshold + LOW_BATTERY_RECOVERY_OFFSET);
Serial.println("=====================\n");
}
// Handle debug level change (single character '0'-'4')
static void handleDebugLevel(char level) {
switch (level) {
case '0': // Level 0: All OFF
g_debug_enabled = false;
g_debug_water_level = false;
g_debug_accelerometer = false;
g_debug_display = false;
g_debug_drink_tracking = false;
g_debug_calibration = false;
g_debug_ble = false;
Serial.println("Debug Level 0: All debug output OFF");
break;
case '1': // Level 1: Events only (no gesture details)
g_debug_enabled = true;
g_debug_water_level = false;
g_debug_accelerometer = false;
g_debug_display = true;
g_debug_drink_tracking = true;
g_debug_calibration = false; // Gestures are level 2
g_debug_ble = false;
Serial.println("Debug Level 1: Events (drinks, refills, display)");
break;
case '2': // Level 2: + Gestures (uses g_debug_calibration for gesture output)
g_debug_enabled = true;
g_debug_water_level = false;
g_debug_accelerometer = false;
g_debug_display = true;
g_debug_drink_tracking = true;
g_debug_calibration = true; // Used for gesture detection output
g_debug_ble = false;
Serial.println("Debug Level 2: + Gestures (gesture detection, state changes)");
break;
case '3': // Level 3: + Weight readings
g_debug_enabled = true;
g_debug_water_level = true;
g_debug_accelerometer = false;
g_debug_display = true;
g_debug_drink_tracking = true;
g_debug_calibration = true;
g_debug_ble = false;
Serial.println("Debug Level 3: + Weight (load cell ADC, water levels)");
break;
case '4': // Level 4: + Accelerometer
g_debug_enabled = true;
g_debug_water_level = true;
g_debug_accelerometer = true;
g_debug_display = true;
g_debug_drink_tracking = true;
g_debug_calibration = true;
g_debug_ble = false;