#include "display_ui.h" #include "config.h" #include "time_manager.h" #include #include #include #include #include #include static Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1); static DeviceRole g_role = DeviceRole::Sender; static uint16_t g_short_id = 0; static char g_device_id[16] = ""; static MeterData g_last_meter = {}; static bool g_last_read_ok = false; static bool g_last_tx_ok = false; static uint32_t g_last_read_ts = 0; static uint32_t g_last_tx_ts = 0; static uint32_t g_last_read_ms = 0; static uint32_t g_last_tx_ms = 0; static uint8_t g_sender_queue_depth = 0; static bool g_sender_build_pending = false; static uint16_t g_sender_last_acked_batch_id = 0; static uint16_t g_sender_current_batch_id = 0; static FaultType g_last_error = FaultType::None; static uint32_t g_last_error_ts = 0; static uint32_t g_last_error_ms = 0; static const SenderStatus *g_statuses = nullptr; static uint8_t g_status_count = 0; static bool g_ap_mode = false; static String g_wifi_ssid; static bool g_mqtt_ok = false; static bool g_oled_on = true; static bool g_prev_ctrl_high = false; static uint32_t g_last_page_ms = 0; static uint8_t g_page = 0; static uint32_t g_last_activity_ms = 0; static bool g_display_ready = false; static uint32_t g_last_init_attempt_ms = 0; static bool g_last_oled_on = true; #ifdef ENABLE_TEST_MODE static char g_test_code[8] = ""; static char g_test_codes[NUM_SENDERS][8] = {}; #endif static void oled_set_power(bool on) { if (on) { display.ssd1306_command(SSD1306_DISPLAYON); } else { display.ssd1306_command(SSD1306_DISPLAYOFF); } } void display_power_down() { if (!g_display_ready) { return; } display.clearDisplay(); display.display(); oled_set_power(false); g_oled_on = false; g_last_oled_on = false; } void display_init() { if (g_role == DeviceRole::Sender) { pinMode(PIN_OLED_CTRL, INPUT_PULLDOWN); } Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL); Wire.setClock(100000); g_display_ready = display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR); if (g_display_ready) { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.ssd1306_command(SSD1306_DISPLAYON); display.display(); } g_last_init_attempt_ms = millis(); g_last_activity_ms = millis(); } void display_set_role(DeviceRole role) { g_role = role; } void display_set_self_ids(uint16_t short_id, const char *device_id) { g_short_id = short_id; strncpy(g_device_id, device_id, sizeof(g_device_id)); g_device_id[sizeof(g_device_id) - 1] = '\0'; } void display_set_sender_statuses(const SenderStatus *statuses, uint8_t count) { g_statuses = statuses; g_status_count = count; } void display_set_last_meter(const MeterData &data) { g_last_meter = data; } void display_set_last_read(bool ok, uint32_t ts_utc) { g_last_read_ok = ok; g_last_read_ts = ts_utc; g_last_read_ms = millis(); } void display_set_last_tx(bool ok, uint32_t ts_utc) { g_last_tx_ok = ok; g_last_tx_ts = ts_utc; g_last_tx_ms = millis(); } void display_set_sender_queue(uint8_t depth, bool build_pending) { g_sender_queue_depth = depth; g_sender_build_pending = build_pending; } void display_set_sender_batches(uint16_t last_acked_batch_id, uint16_t current_batch_id) { g_sender_last_acked_batch_id = last_acked_batch_id; g_sender_current_batch_id = current_batch_id; } void display_set_last_error(FaultType type, uint32_t ts_utc, uint32_t ts_ms) { g_last_error = type; g_last_error_ts = ts_utc; g_last_error_ms = ts_ms; } void display_set_receiver_status(bool ap_mode, const char *ssid, bool mqtt_ok) { g_ap_mode = ap_mode; g_wifi_ssid = ssid ? ssid : ""; g_mqtt_ok = mqtt_ok; } #ifdef ENABLE_TEST_MODE void display_set_test_code(const char *code) { strncpy(g_test_code, code, sizeof(g_test_code)); g_test_code[sizeof(g_test_code) - 1] = '\0'; } void display_set_test_code_for_sender(uint8_t index, const char *code) { if (index >= NUM_SENDERS) { return; } strncpy(g_test_codes[index], code, sizeof(g_test_codes[index])); g_test_codes[index][sizeof(g_test_codes[index]) - 1] = '\0'; } #endif static uint32_t age_seconds(uint32_t ts_utc, uint32_t ts_ms) { if (time_is_synced() && ts_utc > 0) { uint32_t now = time_get_utc(); return now > ts_utc ? now - ts_utc : 0; } return (millis() - ts_ms) / 1000; } static int32_t round_power_w(float value) { if (isnan(value)) { return 0; } long rounded = lroundf(value); if (rounded > INT32_MAX) { return INT32_MAX; } if (rounded < INT32_MIN) { return INT32_MIN; } return static_cast(rounded); } static bool render_last_error_line(uint8_t y) { if (g_last_error == FaultType::None) { return false; } const char *label = "unk"; if (g_last_error == FaultType::MeterRead) { label = "meter"; } else if (g_last_error == FaultType::Decode) { label = "decode"; } else if (g_last_error == FaultType::LoraTx) { label = "lora"; } else if (g_last_error == FaultType::TimeSync) { label = "timesync"; } display.setCursor(0, y); display.printf("Err: %s %lus", label, static_cast(age_seconds(g_last_error_ts, g_last_error_ms))); return true; } static void render_last_sync_line(uint8_t y, bool include_time) { display.setCursor(0, y); uint32_t last_sync = time_get_last_sync_utc(); if (last_sync == 0 || !time_is_synced()) { display.print("Sync: --"); return; } uint32_t age = time_get_last_sync_age_sec(); if (include_time) { time_t t = last_sync; struct tm timeinfo; localtime_r(&t, &timeinfo); display.printf("Sync: %lus %02d:%02d", static_cast(age), timeinfo.tm_hour, timeinfo.tm_min); } else { display.printf("Sync: %lus ago", static_cast(age)); } } static void render_sender_status() { display.clearDisplay(); display.setCursor(0, 0); display.printf("SENDER %s", g_device_id); char time_buf[8]; time_get_local_hhmm(time_buf, sizeof(time_buf)); display.setCursor(0, 12); display.printf("%s %.2fV %u%%", time_buf, g_last_meter.battery_voltage_v, g_last_meter.battery_percent); display.setCursor(0, 24); display.printf("Read %s %lus ago", g_last_read_ok ? "OK" : "ERR", static_cast(age_seconds(g_last_read_ts, g_last_read_ms))); display.setCursor(0, 36); display.printf("TX %s %lus Q%u%s A%u C%u", g_last_tx_ok ? "OK" : "ERR", static_cast(age_seconds(g_last_tx_ts, g_last_tx_ms)), g_sender_queue_depth, g_sender_build_pending ? "+" : "", g_sender_last_acked_batch_id, g_sender_current_batch_id); #ifdef ENABLE_TEST_MODE if (strlen(g_test_code) > 0) { display.setCursor(0, 48); display.printf("Test %s", g_test_code); } else #endif { if (!render_last_error_line(48)) { render_last_sync_line(48, true); } } display.display(); } static void render_sender_measurement() { display.clearDisplay(); display.setCursor(0, 0); display.printf("E %.2f kWh", g_last_meter.energy_total_kwh); display.setCursor(0, 12); display.printf("P %dW", static_cast(round_power_w(g_last_meter.total_power_w))); display.setCursor(0, 24); display.printf("L1 %dW", static_cast(round_power_w(g_last_meter.phase_power_w[0]))); display.setCursor(0, 36); display.printf("L2 %dW", static_cast(round_power_w(g_last_meter.phase_power_w[1]))); display.setCursor(0, 48); display.printf("L3 %dW", static_cast(round_power_w(g_last_meter.phase_power_w[2]))); display.display(); } static void render_receiver_status() { display.clearDisplay(); display.setCursor(0, 0); display.printf("RECEIVER %s", g_device_id); display.setCursor(0, 12); if (g_ap_mode) { display.print("WiFi: AP"); } else { display.printf("WiFi: %s", g_wifi_ssid.c_str()); } display.setCursor(0, 24); display.printf("MQTT: %s", g_mqtt_ok ? "OK" : "RETRY"); char time_buf[8]; time_get_local_hhmm(time_buf, sizeof(time_buf)); display.setCursor(0, 36); display.printf("Time: %s", time_buf); uint32_t latest = 0; if (g_statuses) { for (uint8_t i = 0; i < g_status_count; ++i) { if (g_statuses[i].has_data && g_statuses[i].last_update_ts_utc > latest) { latest = g_statuses[i].last_update_ts_utc; } } } display.setCursor(0, 48); if (latest == 0 || !time_is_synced()) { display.print("Upd --:--"); } else { time_t t = latest; struct tm timeinfo; localtime_r(&t, &timeinfo); display.printf("Upd %02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); } render_last_error_line(56); display.display(); } static void render_receiver_sender(uint8_t index) { display.clearDisplay(); if (!g_statuses || index >= g_status_count) { display.setCursor(0, 0); display.print("No sender"); display.display(); return; } const SenderStatus &status = g_statuses[index]; display.setCursor(0, 0); uint8_t bat = status.has_data ? status.last_data.battery_percent : 0; if (status.has_data) { const char *device_id = status.last_data.device_id; if (strlen(device_id) >= 4 && strncmp(device_id, "dd3-", 4) == 0) { device_id += 4; } if (status.last_data.link_valid) { display.printf("%s R:%d S:%.1f", device_id, status.last_data.link_rssi_dbm, status.last_data.link_snr_db); } else { display.printf("%s B%u", device_id, bat); } } else { display.printf("%s B--", status.last_data.device_id); } if (!status.has_data) { display.setCursor(0, 12); display.print("No data"); display.display(); return; } #ifdef ENABLE_TEST_MODE if (strlen(g_test_codes[index]) > 0) { display.setCursor(0, 12); display.printf("Test %s", g_test_codes[index]); display.display(); return; } #endif display.setCursor(0, 12); display.printf("E %.2f kWh", status.last_data.energy_total_kwh); display.setCursor(0, 22); display.printf("L1 %dW", static_cast(round_power_w(status.last_data.phase_power_w[0]))); display.setCursor(0, 32); display.printf("L2 %dW", static_cast(round_power_w(status.last_data.phase_power_w[1]))); display.setCursor(0, 42); display.printf("L3 %dW", static_cast(round_power_w(status.last_data.phase_power_w[2]))); display.setCursor(0, 52); display.print("P"); char p_buf[16]; snprintf(p_buf, sizeof(p_buf), "%dW", static_cast(round_power_w(status.last_data.total_power_w))); int16_t x1 = 0; int16_t y1 = 0; uint16_t w = 0; uint16_t h = 0; display.getTextBounds(p_buf, 0, 0, &x1, &y1, &w, &h); int16_t x = static_cast(display.width() - w); if (x < 0) { x = 0; } display.setCursor(x, 52); display.print(p_buf); display.display(); } void display_tick() { if (!g_display_ready) { if (millis() - g_last_init_attempt_ms > 1000) { g_last_init_attempt_ms = millis(); g_display_ready = display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR); if (g_display_ready) { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.ssd1306_command(SSD1306_DISPLAYON); display.display(); } } return; } bool ctrl_high = false; if (g_role == DeviceRole::Sender) { ctrl_high = digitalRead(PIN_OLED_CTRL) == HIGH; } uint32_t now_ms = millis(); bool ctrl_falling_edge = g_prev_ctrl_high && !ctrl_high; if (g_role == DeviceRole::Receiver) { g_oled_on = true; g_last_activity_ms = now_ms; } else { if (ctrl_high || ctrl_falling_edge) { g_last_activity_ms = now_ms; } g_oled_on = (now_ms - g_last_activity_ms) < OLED_AUTO_OFF_MS; } if (g_oled_on) { if (!g_last_oled_on) { oled_set_power(true); g_last_page_ms = millis(); } } else { if (g_last_oled_on) { display.clearDisplay(); display.display(); oled_set_power(false); } g_last_oled_on = g_oled_on; g_prev_ctrl_high = ctrl_high; return; } g_last_oled_on = g_oled_on; uint32_t now = millis(); uint8_t page_count = g_role == DeviceRole::Sender ? 2 : (1 + g_status_count); if (page_count == 0) { page_count = 1; } if (now - g_last_page_ms > OLED_PAGE_INTERVAL_MS) { g_last_page_ms = now; g_page = (g_page + 1) % page_count; } if (g_role == DeviceRole::Sender) { if (g_page == 0) { render_sender_status(); } else { render_sender_measurement(); } } else { if (g_page == 0) { render_receiver_status(); } else { render_receiver_sender(g_page - 1); } } g_prev_ctrl_high = ctrl_high; }