optional RTC 3231 integration
This commit is contained in:
@@ -3,27 +3,71 @@
|
||||
#include <math.h>
|
||||
#include "power_manager.h"
|
||||
|
||||
static float round2(float value) {
|
||||
if (isnan(value)) {
|
||||
return value;
|
||||
}
|
||||
return roundf(value * 100.0f) / 100.0f;
|
||||
}
|
||||
|
||||
static const char *short_id_from_device_id(const char *device_id) {
|
||||
if (!device_id) {
|
||||
return "";
|
||||
}
|
||||
size_t len = strlen(device_id);
|
||||
if (len >= 4) {
|
||||
return device_id + (len - 4);
|
||||
}
|
||||
return device_id;
|
||||
}
|
||||
|
||||
static void format_float_2(char *buf, size_t buf_len, float value) {
|
||||
if (!buf || buf_len == 0) {
|
||||
return;
|
||||
}
|
||||
if (isnan(value)) {
|
||||
snprintf(buf, buf_len, "null");
|
||||
return;
|
||||
}
|
||||
snprintf(buf, buf_len, "%.2f", round2(value));
|
||||
}
|
||||
|
||||
bool meterDataToJson(const MeterData &data, String &out_json) {
|
||||
StaticJsonDocument<192> doc;
|
||||
doc["id"] = data.device_id;
|
||||
doc["id"] = short_id_from_device_id(data.device_id);
|
||||
doc["ts"] = data.ts_utc;
|
||||
doc["energy_kwh"] = data.energy_total_kwh;
|
||||
doc["p_total_w"] = data.total_power_w;
|
||||
doc["p1_w"] = data.phase_power_w[0];
|
||||
doc["p2_w"] = data.phase_power_w[1];
|
||||
doc["p3_w"] = data.phase_power_w[2];
|
||||
doc["v1_v"] = data.phase_voltage_v[0];
|
||||
doc["v2_v"] = data.phase_voltage_v[1];
|
||||
doc["v3_v"] = data.phase_voltage_v[2];
|
||||
char bat_buf[8];
|
||||
snprintf(bat_buf, sizeof(bat_buf), "%.2f", data.battery_voltage_v);
|
||||
doc["bat_v"] = serialized(bat_buf);
|
||||
char buf[16];
|
||||
format_float_2(buf, sizeof(buf), data.energy_total_kwh);
|
||||
doc["e_kwh"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.total_power_w);
|
||||
doc["p_w"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.phase_power_w[0]);
|
||||
doc["p1_w"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.phase_power_w[1]);
|
||||
doc["p2_w"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.phase_power_w[2]);
|
||||
doc["p3_w"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.phase_voltage_v[0]);
|
||||
doc["v1_v"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.phase_voltage_v[1]);
|
||||
doc["v2_v"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.phase_voltage_v[2]);
|
||||
doc["v3_v"] = serialized(buf);
|
||||
format_float_2(buf, sizeof(buf), data.battery_voltage_v);
|
||||
doc["bat_v"] = serialized(buf);
|
||||
|
||||
out_json = "";
|
||||
size_t len = serializeJson(doc, out_json);
|
||||
return len > 0 && len < 256;
|
||||
}
|
||||
|
||||
static float read_float_or_legacy(JsonDocument &doc, const char *key, const char *legacy_key) {
|
||||
if (doc[key].isNull()) {
|
||||
return doc[legacy_key] | NAN;
|
||||
}
|
||||
return doc[key] | NAN;
|
||||
}
|
||||
|
||||
bool jsonToMeterData(const String &json, MeterData &data) {
|
||||
StaticJsonDocument<192> doc;
|
||||
DeserializationError err = deserializeJson(doc, json);
|
||||
@@ -32,12 +76,16 @@ bool jsonToMeterData(const String &json, MeterData &data) {
|
||||
}
|
||||
|
||||
const char *id = doc["id"] | "";
|
||||
strncpy(data.device_id, id, sizeof(data.device_id));
|
||||
if (strlen(id) == 4) {
|
||||
snprintf(data.device_id, sizeof(data.device_id), "dd3-%s", id);
|
||||
} else {
|
||||
strncpy(data.device_id, id, sizeof(data.device_id));
|
||||
}
|
||||
data.device_id[sizeof(data.device_id) - 1] = '\0';
|
||||
|
||||
data.ts_utc = doc["ts"] | 0;
|
||||
data.energy_total_kwh = doc["energy_kwh"] | NAN;
|
||||
data.total_power_w = doc["p_total_w"] | NAN;
|
||||
data.energy_total_kwh = read_float_or_legacy(doc, "e_kwh", "energy_kwh");
|
||||
data.total_power_w = read_float_or_legacy(doc, "p_w", "p_total_w");
|
||||
data.phase_power_w[0] = doc["p1_w"] | NAN;
|
||||
data.phase_power_w[1] = doc["p2_w"] | NAN;
|
||||
data.phase_power_w[2] = doc["p3_w"] | NAN;
|
||||
@@ -66,21 +114,21 @@ bool meterBatchToJson(const MeterData *samples, size_t count, String &out_json)
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(8192);
|
||||
doc["id"] = samples[count - 1].device_id;
|
||||
doc["bat_v"] = samples[count - 1].battery_voltage_v;
|
||||
doc["id"] = short_id_from_device_id(samples[count - 1].device_id);
|
||||
doc["bat_v"] = round2(samples[count - 1].battery_voltage_v);
|
||||
doc["bat_pct"] = samples[count - 1].battery_percent;
|
||||
JsonArray arr = doc.createNestedArray("s");
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
JsonArray row = arr.createNestedArray();
|
||||
row.add(samples[i].ts_utc);
|
||||
row.add(samples[i].energy_total_kwh);
|
||||
row.add(samples[i].total_power_w);
|
||||
row.add(samples[i].phase_power_w[0]);
|
||||
row.add(samples[i].phase_power_w[1]);
|
||||
row.add(samples[i].phase_power_w[2]);
|
||||
row.add(samples[i].phase_voltage_v[0]);
|
||||
row.add(samples[i].phase_voltage_v[1]);
|
||||
row.add(samples[i].phase_voltage_v[2]);
|
||||
row.add(round2(samples[i].energy_total_kwh));
|
||||
row.add(round2(samples[i].total_power_w));
|
||||
row.add(round2(samples[i].phase_power_w[0]));
|
||||
row.add(round2(samples[i].phase_power_w[1]));
|
||||
row.add(round2(samples[i].phase_power_w[2]));
|
||||
row.add(round2(samples[i].phase_voltage_v[0]));
|
||||
row.add(round2(samples[i].phase_voltage_v[1]));
|
||||
row.add(round2(samples[i].phase_voltage_v[2]));
|
||||
row.add(samples[i].valid ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -117,7 +165,11 @@ bool jsonToMeterBatch(const String &json, MeterData *out_samples, size_t max_cou
|
||||
}
|
||||
MeterData &data = out_samples[idx];
|
||||
data = {};
|
||||
strncpy(data.device_id, id, sizeof(data.device_id));
|
||||
if (strlen(id) == 4) {
|
||||
snprintf(data.device_id, sizeof(data.device_id), "dd3-%s", id);
|
||||
} else {
|
||||
strncpy(data.device_id, id, sizeof(data.device_id));
|
||||
}
|
||||
data.device_id[sizeof(data.device_id) - 1] = '\0';
|
||||
data.ts_utc = row[0] | 0;
|
||||
data.energy_total_kwh = row[1] | NAN;
|
||||
|
||||
10
src/main.cpp
10
src/main.cpp
@@ -22,6 +22,7 @@ static bool g_ap_mode = false;
|
||||
static WifiMqttConfig g_cfg;
|
||||
static uint32_t g_last_timesync_ms = 0;
|
||||
static constexpr uint32_t TIME_SYNC_OFFSET_MS = 15000;
|
||||
static uint32_t g_boot_ms = 0;
|
||||
|
||||
static constexpr size_t BATCH_HEADER_SIZE = 6;
|
||||
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
|
||||
@@ -244,11 +245,14 @@ void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(200);
|
||||
|
||||
g_boot_ms = millis();
|
||||
g_role = detect_role();
|
||||
init_device_ids(g_short_id, g_device_id, sizeof(g_device_id));
|
||||
|
||||
lora_init();
|
||||
display_init();
|
||||
time_rtc_init();
|
||||
time_try_load_from_rtc();
|
||||
display_set_role(g_role);
|
||||
display_set_self_ids(g_short_id, g_device_id);
|
||||
|
||||
@@ -381,7 +385,11 @@ static void receiver_loop() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_ap_mode && millis() - g_last_timesync_ms > TIME_SYNC_INTERVAL_SEC * 1000UL) {
|
||||
uint32_t interval_sec = TIME_SYNC_INTERVAL_SEC;
|
||||
if (time_rtc_present() && millis() - g_boot_ms >= TIME_SYNC_FAST_WINDOW_MS) {
|
||||
interval_sec = TIME_SYNC_SLOW_INTERVAL_SEC;
|
||||
}
|
||||
if (!g_ap_mode && millis() - g_last_timesync_ms > interval_sec * 1000UL) {
|
||||
g_last_timesync_ms = millis();
|
||||
time_send_timesync(g_short_id);
|
||||
}
|
||||
|
||||
105
src/rtc_ds3231.cpp
Normal file
105
src/rtc_ds3231.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "rtc_ds3231.h"
|
||||
#include "config.h"
|
||||
#include <Wire.h>
|
||||
#include <time.h>
|
||||
|
||||
static constexpr uint8_t DS3231_ADDR = 0x68;
|
||||
|
||||
static uint8_t bcd_to_dec(uint8_t val) {
|
||||
return static_cast<uint8_t>((val >> 4) * 10 + (val & 0x0F));
|
||||
}
|
||||
|
||||
static uint8_t dec_to_bcd(uint8_t val) {
|
||||
return static_cast<uint8_t>(((val / 10) << 4) | (val % 10));
|
||||
}
|
||||
|
||||
static bool read_registers(uint8_t start_reg, uint8_t *out, size_t len) {
|
||||
if (!out || len == 0) {
|
||||
return false;
|
||||
}
|
||||
Wire.beginTransmission(DS3231_ADDR);
|
||||
Wire.write(start_reg);
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
return false;
|
||||
}
|
||||
size_t read = Wire.requestFrom(DS3231_ADDR, static_cast<uint8_t>(len));
|
||||
if (read != len) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
out[i] = Wire.read();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_registers(uint8_t start_reg, const uint8_t *data, size_t len) {
|
||||
if (!data || len == 0) {
|
||||
return false;
|
||||
}
|
||||
Wire.beginTransmission(DS3231_ADDR);
|
||||
Wire.write(start_reg);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
Wire.write(data[i]);
|
||||
}
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool rtc_ds3231_init() {
|
||||
Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL);
|
||||
Wire.setClock(100000);
|
||||
return rtc_ds3231_is_present();
|
||||
}
|
||||
|
||||
bool rtc_ds3231_is_present() {
|
||||
Wire.beginTransmission(DS3231_ADDR);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool rtc_ds3231_read_epoch(uint32_t &epoch_utc) {
|
||||
uint8_t regs[7] = {};
|
||||
if (!read_registers(0x00, regs, sizeof(regs))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t sec = bcd_to_dec(regs[0] & 0x7F);
|
||||
uint8_t min = bcd_to_dec(regs[1] & 0x7F);
|
||||
uint8_t hour = bcd_to_dec(regs[2] & 0x3F);
|
||||
uint8_t day = bcd_to_dec(regs[4] & 0x3F);
|
||||
uint8_t month = bcd_to_dec(regs[5] & 0x1F);
|
||||
uint16_t year = 2000 + bcd_to_dec(regs[6]);
|
||||
|
||||
struct tm tm_utc = {};
|
||||
tm_utc.tm_sec = sec;
|
||||
tm_utc.tm_min = min;
|
||||
tm_utc.tm_hour = hour;
|
||||
tm_utc.tm_mday = day;
|
||||
tm_utc.tm_mon = month - 1;
|
||||
tm_utc.tm_year = year - 1900;
|
||||
tm_utc.tm_isdst = 0;
|
||||
|
||||
time_t t = timegm(&tm_utc);
|
||||
if (t <= 0) {
|
||||
return false;
|
||||
}
|
||||
epoch_utc = static_cast<uint32_t>(t);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rtc_ds3231_set_epoch(uint32_t epoch_utc) {
|
||||
time_t t = static_cast<time_t>(epoch_utc);
|
||||
struct tm tm_utc = {};
|
||||
if (!gmtime_r(&t, &tm_utc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t regs[7] = {};
|
||||
regs[0] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_sec));
|
||||
regs[1] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_min));
|
||||
regs[2] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_hour));
|
||||
regs[3] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_wday + 1));
|
||||
regs[4] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_mday));
|
||||
regs[5] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_mon + 1));
|
||||
regs[6] = dec_to_bcd(static_cast<uint8_t>((tm_utc.tm_year + 1900) - 2000));
|
||||
|
||||
return write_registers(0x00, regs, sizeof(regs));
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "time_manager.h"
|
||||
#include "compressor.h"
|
||||
#include "config.h"
|
||||
#include "rtc_ds3231.h"
|
||||
#include <time.h>
|
||||
|
||||
static bool g_time_synced = false;
|
||||
static bool g_tz_set = false;
|
||||
static bool g_rtc_present = false;
|
||||
|
||||
void time_receiver_init(const char *ntp_server_1, const char *ntp_server_2) {
|
||||
const char *server1 = (ntp_server_1 && ntp_server_1[0] != '\0') ? ntp_server_1 : "pool.ntp.org";
|
||||
@@ -40,6 +42,10 @@ void time_set_utc(uint32_t epoch) {
|
||||
tv.tv_usec = 0;
|
||||
settimeofday(&tv, nullptr);
|
||||
g_time_synced = true;
|
||||
|
||||
if (g_rtc_present) {
|
||||
rtc_ds3231_set_epoch(epoch);
|
||||
}
|
||||
}
|
||||
|
||||
void time_send_timesync(uint16_t device_id_short) {
|
||||
@@ -101,3 +107,30 @@ void time_get_local_hhmm(char *out, size_t out_len) {
|
||||
localtime_r(&now, &timeinfo);
|
||||
snprintf(out, out_len, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
||||
}
|
||||
|
||||
void time_rtc_init() {
|
||||
if (!ENABLE_DS3231) {
|
||||
g_rtc_present = false;
|
||||
return;
|
||||
}
|
||||
g_rtc_present = rtc_ds3231_init();
|
||||
}
|
||||
|
||||
bool time_try_load_from_rtc() {
|
||||
if (!g_rtc_present) {
|
||||
return false;
|
||||
}
|
||||
if (time_is_synced()) {
|
||||
return true;
|
||||
}
|
||||
uint32_t epoch = 0;
|
||||
if (!rtc_ds3231_read_epoch(epoch) || epoch == 0) {
|
||||
return false;
|
||||
}
|
||||
time_set_utc(epoch);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool time_rtc_present() {
|
||||
return g_rtc_present;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user