Add SD logging and update docs

- Add optional microSD CSV logging per sender/day on receiver
- Wire logger into receiver packet handling
- Document new batch header fields, build envs, and SD logging
- Make sender links open in a new tab
This commit is contained in:
2026-02-02 00:22:35 +01:00
parent 5085b9ad3d
commit f3af5b3f1c
6 changed files with 149 additions and 4 deletions

114
src/sd_logger.cpp Normal file
View File

@@ -0,0 +1,114 @@
#include "sd_logger.h"
#include "config.h"
#include <SD.h>
#include <SPI.h>
#include <time.h>
static bool g_sd_ready = false;
static const char *fault_text(FaultType fault) {
switch (fault) {
case FaultType::MeterRead:
return "meter";
case FaultType::Decode:
return "decode";
case FaultType::LoraTx:
return "loratx";
default:
return "";
}
}
static bool ensure_dir(const String &path) {
if (SD.exists(path)) {
return true;
}
return SD.mkdir(path);
}
static String format_date_utc(uint32_t ts_utc) {
time_t t = static_cast<time_t>(ts_utc);
struct tm tm_utc;
gmtime_r(&t, &tm_utc);
char buf[16];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d",
tm_utc.tm_year + 1900,
tm_utc.tm_mon + 1,
tm_utc.tm_mday);
return String(buf);
}
void sd_logger_init() {
if (!ENABLE_SD_LOGGING) {
g_sd_ready = false;
return;
}
g_sd_ready = SD.begin(PIN_SD_CS);
}
bool sd_logger_is_ready() {
return g_sd_ready;
}
void sd_logger_log_sample(const MeterData &data, bool include_error_text) {
if (!g_sd_ready || data.ts_utc == 0) {
return;
}
String root_dir = "/dd3";
if (!ensure_dir(root_dir)) {
return;
}
String sender_dir = root_dir + "/" + String(data.device_id);
if (!ensure_dir(sender_dir)) {
return;
}
String filename = sender_dir + "/" + format_date_utc(data.ts_utc) + ".csv";
bool new_file = !SD.exists(filename);
File f = SD.open(filename, FILE_APPEND);
if (!f) {
return;
}
if (new_file) {
f.println("ts_utc,p_w,p1_w,p2_w,p3_w,e_kwh,bat_v,bat_pct,rssi,snr,err_m,err_d,err_tx,err_last");
}
f.print(data.ts_utc);
f.print(',');
f.print(data.total_power_w, 1);
f.print(',');
f.print(data.phase_power_w[0], 1);
f.print(',');
f.print(data.phase_power_w[1], 1);
f.print(',');
f.print(data.phase_power_w[2], 1);
f.print(',');
f.print(data.energy_total_kwh, 3);
f.print(',');
f.print(data.battery_voltage_v, 2);
f.print(',');
f.print(data.battery_percent);
f.print(',');
f.print(data.link_rssi_dbm);
f.print(',');
if (isnan(data.link_snr_db)) {
f.print("");
} else {
f.print(data.link_snr_db, 1);
}
f.print(',');
f.print(data.err_meter_read);
f.print(',');
f.print(data.err_decode);
f.print(',');
f.print(data.err_lora_tx);
f.print(',');
if (include_error_text && data.last_error != FaultType::None) {
f.print(fault_text(data.last_error));
}
f.println();
f.close();
}