Fix OLED autosleep timing and battery sampling cadence

- Track last OLED activity to avoid double timeout; keep power gating on transitions
- Copy TZ before setenv() in timegm_fallback to avoid invalid pointer reuse
- Add BATTERY_SAMPLE_INTERVAL_MS and only refresh cache at batch start when due
- Keep battery sampling to a single ADC read (Arduino core lacks explicit ADC power gating)
This commit is contained in:
2026-02-02 23:01:55 +01:00
parent 90d830da6f
commit 2199627a35
6 changed files with 29 additions and 33 deletions

View File

@@ -250,7 +250,8 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C };
``` ```
## OLED Behavior ## OLED Behavior
- Sender: OLED stays **ON for 10 seconds** on each wake, then powers down for sleep. - Sender: OLED stays on for `OLED_AUTO_OFF_MS` after boot or last activity.
- Activity is detected while `PIN_OLED_CTRL` is held high, or on the high→low edge when the control is released.
- Receiver: OLED is always on (no auto-off). - Receiver: OLED is always on (no auto-off).
- Pages rotate every 4s. - Pages rotate every 4s.
@@ -260,6 +261,7 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C };
- 4.2 V = 100% - 4.2 V = 100%
- Uses deep sleep between cycles (`SENDER_WAKE_INTERVAL_SEC`). - Uses deep sleep between cycles (`SENDER_WAKE_INTERVAL_SEC`).
- Sender CPU is throttled to 80 MHz and LoRa RX is only enabled in short windows (ACK wait or time-sync). - Sender CPU is throttled to 80 MHz and LoRa RX is only enabled in short windows (ACK wait or time-sync).
- Battery sampling uses a single ADC read and updates at most once per `BATTERY_SAMPLE_INTERVAL_MS` (default 60s).
## Web UI ## Web UI
- AP SSID: `DD3-Bridge-<short_id>` (prefix configurable) - AP SSID: `DD3-Bridge-<short_id>` (prefix configurable)
@@ -313,6 +315,7 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C };
Key timing settings in `include/config.h`: Key timing settings in `include/config.h`:
- `METER_SAMPLE_INTERVAL_MS` - `METER_SAMPLE_INTERVAL_MS`
- `METER_SEND_INTERVAL_MS` - `METER_SEND_INTERVAL_MS`
- `BATTERY_SAMPLE_INTERVAL_MS`
- `BATCH_ACK_TIMEOUT_MS` - `BATCH_ACK_TIMEOUT_MS`
- `BATCH_MAX_RETRIES` - `BATCH_MAX_RETRIES`
- `BATCH_QUEUE_DEPTH` - `BATCH_QUEUE_DEPTH`

View File

@@ -69,6 +69,7 @@ constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL;
constexpr uint32_t SENDER_OLED_READ_MS = 10000; constexpr uint32_t SENDER_OLED_READ_MS = 10000;
constexpr uint32_t METER_SAMPLE_INTERVAL_MS = 1000; constexpr uint32_t METER_SAMPLE_INTERVAL_MS = 1000;
constexpr uint32_t METER_SEND_INTERVAL_MS = 30000; constexpr uint32_t METER_SEND_INTERVAL_MS = 30000;
constexpr uint32_t BATTERY_SAMPLE_INTERVAL_MS = 60000;
constexpr uint32_t BATCH_ACK_TIMEOUT_MS = 3000; constexpr uint32_t BATCH_ACK_TIMEOUT_MS = 3000;
constexpr uint8_t BATCH_MAX_RETRIES = 2; constexpr uint8_t BATCH_MAX_RETRIES = 2;
constexpr uint8_t METER_BATCH_MAX_SAMPLES = 30; constexpr uint8_t METER_BATCH_MAX_SAMPLES = 30;

View File

@@ -36,10 +36,9 @@ static bool g_mqtt_ok = false;
static bool g_oled_on = true; static bool g_oled_on = true;
static bool g_prev_ctrl_high = false; static bool g_prev_ctrl_high = false;
static uint32_t g_oled_off_start = 0;
static uint32_t g_last_page_ms = 0; static uint32_t g_last_page_ms = 0;
static uint8_t g_page = 0; static uint8_t g_page = 0;
static uint32_t g_boot_ms = 0; static uint32_t g_last_activity_ms = 0;
static bool g_display_ready = false; static bool g_display_ready = false;
static uint32_t g_last_init_attempt_ms = 0; static uint32_t g_last_init_attempt_ms = 0;
static bool g_last_oled_on = true; static bool g_last_oled_on = true;
@@ -83,7 +82,7 @@ void display_init() {
display.display(); display.display();
} }
g_last_init_attempt_ms = millis(); g_last_init_attempt_ms = millis();
g_boot_ms = millis(); g_last_activity_ms = millis();
} }
void display_set_role(DeviceRole role) { void display_set_role(DeviceRole role) {
@@ -380,27 +379,16 @@ void display_tick() {
ctrl_high = digitalRead(PIN_OLED_CTRL) == HIGH; ctrl_high = digitalRead(PIN_OLED_CTRL) == HIGH;
} }
bool in_boot_window = (millis() - g_boot_ms) < OLED_AUTO_OFF_MS; uint32_t now_ms = millis();
bool ctrl_falling_edge = g_prev_ctrl_high && !ctrl_high;
if (g_role == DeviceRole::Receiver) { if (g_role == DeviceRole::Receiver) {
g_oled_on = true; g_oled_on = true;
g_oled_off_start = 0; g_last_activity_ms = now_ms;
} else if (in_boot_window) {
g_oled_on = true;
} else { } else {
if (ctrl_high) { if (ctrl_high || ctrl_falling_edge) {
g_oled_on = true; g_last_activity_ms = now_ms;
g_oled_off_start = 0;
} else if (g_prev_ctrl_high && !ctrl_high) {
g_oled_off_start = millis();
} else if (!g_prev_ctrl_high && !ctrl_high && g_oled_off_start == 0) {
g_oled_off_start = millis();
} }
g_oled_on = (now_ms - g_last_activity_ms) < OLED_AUTO_OFF_MS;
if (!ctrl_high && g_oled_off_start > 0 && millis() - g_oled_off_start > OLED_AUTO_OFF_MS) {
g_oled_on = false;
}
// fall through to power gating below
} }
if (g_oled_on) { if (g_oled_on) {

View File

@@ -152,6 +152,10 @@ static void update_battery_cache() {
g_last_battery_ms = millis(); g_last_battery_ms = millis();
} }
static bool battery_sample_due(uint32_t now_ms) {
return g_last_battery_ms == 0 || now_ms - g_last_battery_ms >= BATTERY_SAMPLE_INTERVAL_MS;
}
static bool sender_timesync_window_due() { static bool sender_timesync_window_due() {
uint32_t interval_sec = SENDER_TIMESYNC_CHECK_SEC_FAST; uint32_t interval_sec = SENDER_TIMESYNC_CHECK_SEC_FAST;
if (time_is_synced() && time_rtc_present()) { if (time_is_synced() && time_rtc_present()) {
@@ -742,7 +746,7 @@ static void sender_loop() {
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead); note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead);
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms); display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
} }
if (g_build_count == 0) { if (g_build_count == 0 && battery_sample_due(now_ms)) {
update_battery_cache(); update_battery_cache();
} }
data.battery_voltage_v = g_last_battery_voltage_v; data.battery_voltage_v = g_last_battery_voltage_v;

View File

@@ -34,11 +34,8 @@ void power_configure_unused_pins_sender() {
} }
void read_battery(MeterData &data) { void read_battery(MeterData &data) {
uint32_t sum = 0; uint32_t raw = analogRead(PIN_BAT_ADC);
sum += analogRead(PIN_BAT_ADC); float v = (static_cast<float>(raw) / 4095.0f) * ADC_REF_V * BATTERY_DIVIDER * BATTERY_CAL;
sum += analogRead(PIN_BAT_ADC);
float avg = static_cast<float>(sum) / 2.0f;
float v = (avg / 4095.0f) * ADC_REF_V * BATTERY_DIVIDER * BATTERY_CAL;
data.battery_voltage_v = v; data.battery_voltage_v = v;
data.battery_percent = battery_percent_from_voltage(v); data.battery_percent = battery_percent_from_voltage(v);

View File

@@ -1,6 +1,7 @@
#include "rtc_ds3231.h" #include "rtc_ds3231.h"
#include "config.h" #include "config.h"
#include <Wire.h> #include <Wire.h>
#include <string>
#include <time.h> #include <time.h>
static constexpr uint8_t DS3231_ADDR = 0x68; static constexpr uint8_t DS3231_ADDR = 0x68;
@@ -17,12 +18,14 @@ static time_t timegm_fallback(struct tm *tm_utc) {
if (!tm_utc) { if (!tm_utc) {
return static_cast<time_t>(-1); return static_cast<time_t>(-1);
} }
char *old_tz = getenv("TZ"); const char *old_tz = getenv("TZ");
// getenv() may return a pointer into mutable storage that becomes invalid after setenv().
std::string old_tz_copy = old_tz ? old_tz : "";
setenv("TZ", "UTC0", 1); setenv("TZ", "UTC0", 1);
tzset(); tzset();
time_t t = mktime(tm_utc); time_t t = mktime(tm_utc);
if (old_tz) { if (!old_tz_copy.empty()) {
setenv("TZ", old_tz, 1); setenv("TZ", old_tz_copy.c_str(), 1);
} else { } else {
unsetenv("TZ"); unsetenv("TZ");
} }