#include "sd_logger.h" #include "config.h" #include #include #include static bool g_sd_ready = false; static SPIClass *g_sd_spi = nullptr; 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(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; } if (!g_sd_spi) { g_sd_spi = new SPIClass(HSPI); } g_sd_spi->begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS); g_sd_ready = SD.begin(PIN_SD_CS, *g_sd_spi); if (SERIAL_DEBUG_MODE) { if (g_sd_ready) { uint8_t type = SD.cardType(); uint64_t size = SD.cardSize(); Serial.printf("sd: ok type=%u size=%llu\n", static_cast(type), static_cast(size)); } else { Serial.println("sd: init failed"); } } } 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(); }