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:
@@ -13,6 +13,7 @@
|
||||
#include "web_server.h"
|
||||
#include "display_ui.h"
|
||||
#include "test_mode.h"
|
||||
#include "sd_logger.h"
|
||||
#include <stdarg.h>
|
||||
#include <math.h>
|
||||
#ifdef ARDUINO_ARCH_ESP32
|
||||
@@ -625,6 +626,7 @@ void setup() {
|
||||
update_battery_cache();
|
||||
} else {
|
||||
power_receiver_init();
|
||||
sd_logger_init();
|
||||
wifi_manager_init();
|
||||
init_sender_statuses();
|
||||
display_set_sender_statuses(g_sender_statuses, NUM_SENDERS);
|
||||
@@ -806,6 +808,7 @@ static void receiver_loop() {
|
||||
data.link_valid = true;
|
||||
data.link_rssi_dbm = pkt.rssi_dbm;
|
||||
data.link_snr_db = pkt.snr_db;
|
||||
sd_logger_log_sample(data, data.last_error != FaultType::None);
|
||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
||||
if (pkt.device_id_short == EXPECTED_SENDER_IDS[i]) {
|
||||
data.short_id = pkt.device_id_short;
|
||||
@@ -897,6 +900,7 @@ static void receiver_loop() {
|
||||
data.err_decode = batch.err_d;
|
||||
data.err_lora_tx = batch.err_tx;
|
||||
data.last_error = static_cast<FaultType>(batch.err_last);
|
||||
sd_logger_log_sample(data, (s + 1 == count) && data.last_error != FaultType::None);
|
||||
}
|
||||
|
||||
if (sender_idx >= 0) {
|
||||
|
||||
114
src/sd_logger.cpp
Normal file
114
src/sd_logger.cpp
Normal 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();
|
||||
}
|
||||
7
src/sd_logger.h
Normal file
7
src/sd_logger.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "data_model.h"
|
||||
|
||||
void sd_logger_init();
|
||||
bool sd_logger_is_ready();
|
||||
void sd_logger_log_sample(const MeterData &data, bool include_error_text);
|
||||
@@ -52,7 +52,8 @@ static String render_sender_block(const SenderStatus &status) {
|
||||
}
|
||||
}
|
||||
}
|
||||
s += "<strong>" + String(status.last_data.device_id) + "</strong>";
|
||||
String device_id = status.last_data.device_id;
|
||||
s += "<strong><a href='/sender/" + device_id + "' target='_blank' rel='noopener noreferrer'>" + device_id + "</a></strong>";
|
||||
if (status.has_data && status.last_data.link_valid) {
|
||||
s += " RSSI:" + String(status.last_data.link_rssi_dbm) + " SNR:" + String(status.last_data.link_snr_db, 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user