Improve reliability and add data recovery tools

## Bug Fixes
- Fix integer overflow potential in history bin allocation (web_server.cpp)
  Using uint64_t for intermediate multiplication prevents overflow with different constants

- Prevent data loss during WiFi failures (main.cpp)
  Device now automatically attempts WiFi reconnection every 30 seconds when in AP mode
  Exits AP mode and resumes MQTT transmission as soon as WiFi becomes available
  Data collection and SD logging continue regardless of connectivity

## New Features
- Add standalone MQTT data republisher for lost data recovery
  - Command-line tool (republish_mqtt.py) with interactive and scripting modes
  - GUI tool (republish_mqtt_gui.py) for user-friendly recovery
  - Rate-limited publishing (5 msg/sec default, configurable 1-100)
  - Manual time range selection or auto-detect missing data via InfluxDB
  - Cross-platform support (Windows, macOS, Linux)
  - Converts SD card CSV exports back to MQTT format

## Documentation
- Add comprehensive code review (CODE_REVIEW.md)
  - 16 detailed security and quality assessments
  - Identifies critical HTTPS/auth gaps, medium-priority overflow issues
  - Confirms absence of buffer overflows and unsafe string functions
  - Grade: B+ with areas for improvement

- Add republisher documentation (REPUBLISH_README.md, REPUBLISH_GUI_README.md)
  - Installation and usage instructions
  - Example commands and scenarios
  - Troubleshooting guide
  - Performance characteristics

## Dependencies
- Add requirements_republish.txt
  - paho-mqtt>=1.6.1
  - influxdb-client>=1.18.0

## Impact
- Eliminates data loss scenario where unreliable WiFi leaves device stuck in AP mode
- Provides recovery mechanism for any historical data missed during outages
- Improves code safety with explicit overflow-resistant arithmetic
- Increases operational visibility with comprehensive code review
This commit is contained in:
2026-03-11 17:01:22 +01:00
parent ee849433c8
commit 32cd0652c9
8 changed files with 1982 additions and 2 deletions

View File

@@ -48,6 +48,10 @@ static uint32_t g_sender_last_error_remote_ms[NUM_SENDERS] = {};
static bool g_sender_discovery_sent[NUM_SENDERS] = {};
static bool g_receiver_discovery_sent = false;
// WiFi reconnection in AP mode: retry every 30 seconds
static uint32_t g_last_wifi_reconnect_attempt_ms = 0;
static constexpr uint32_t WIFI_RECONNECT_INTERVAL_MS = 30000;
static constexpr size_t BATCH_HEADER_SIZE = 6;
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
static constexpr size_t BATCH_MAX_COMPRESSED = 4096;
@@ -1122,6 +1126,7 @@ void setup() {
web_server_begin_sta(g_sender_statuses, NUM_SENDERS);
} else {
g_ap_mode = true;
g_last_wifi_reconnect_attempt_ms = millis();
char ap_ssid[32];
snprintf(ap_ssid, sizeof(ap_ssid), "%s%04X", AP_SSID_PREFIX, g_short_id);
wifi_start_ap(ap_ssid, AP_PASSWORD);
@@ -1548,6 +1553,26 @@ static void receiver_loop() {
}
receiver_loop_done:
// Attempt WiFi reconnection if in AP mode and timer has elapsed
if (g_ap_mode && g_cfg.valid) {
uint32_t now_ms = millis();
if (now_ms - g_last_wifi_reconnect_attempt_ms >= WIFI_RECONNECT_INTERVAL_MS) {
g_last_wifi_reconnect_attempt_ms = now_ms;
if (wifi_connect_sta(g_cfg)) {
// WiFi reconnected! Switch off AP mode and resume normal operation
g_ap_mode = false;
time_receiver_init(g_cfg.ntp_server_1.c_str(), g_cfg.ntp_server_2.c_str());
mqtt_init(g_cfg, g_device_id);
web_server_set_config(g_cfg);
web_server_set_sender_faults(g_sender_faults_remote, g_sender_last_error_remote);
web_server_begin_sta(g_sender_statuses, NUM_SENDERS);
if (SERIAL_DEBUG_MODE) {
serial_debug_printf("WiFi reconnected! Exiting AP mode.");
}
}
}
}
mqtt_loop();
web_server_loop();
if (ENABLE_HA_DISCOVERY && !g_receiver_discovery_sent) {

View File

@@ -737,12 +737,14 @@ static void handle_history_start() {
if (res_min < SD_HISTORY_MIN_RES_MIN) {
res_min = SD_HISTORY_MIN_RES_MIN;
}
uint32_t bins = (static_cast<uint32_t>(days) * 24UL * 60UL) / res_min;
if (bins == 0 || bins > SD_HISTORY_MAX_BINS) {
// Use uint64_t for intermediate calculation to prevent overflow
uint64_t bins_64 = (static_cast<uint64_t>(days) * 24UL * 60UL) / res_min;
if (bins_64 == 0 || bins_64 > SD_HISTORY_MAX_BINS) {
String resp = String("{\"ok\":false,\"error\":\"too_many_bins\",\"max_bins\":") + SD_HISTORY_MAX_BINS + "}";
server.send(200, "application/json", resp);
return;
}
uint32_t bins = static_cast<uint32_t>(bins_64);
history_reset();
g_history.active = true;