Initial commit
This commit is contained in:
322
src/display_ui.cpp
Normal file
322
src/display_ui.cpp
Normal file
@@ -0,0 +1,322 @@
|
||||
#include "display_ui.h"
|
||||
#include "config.h"
|
||||
#include "time_manager.h"
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Adafruit_SSD1306.h>
|
||||
#include <time.h>
|
||||
|
||||
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 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_oled_off_start = 0;
|
||||
static uint32_t g_last_page_ms = 0;
|
||||
static uint8_t g_page = 0;
|
||||
static uint32_t g_boot_ms = 0;
|
||||
static bool g_display_ready = false;
|
||||
static uint32_t g_last_init_attempt_ms = 0;
|
||||
|
||||
#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_init() {
|
||||
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_boot_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_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 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<unsigned long>(age_seconds(g_last_read_ts, g_last_read_ms)));
|
||||
|
||||
display.setCursor(0, 36);
|
||||
display.printf("TX %s %lus ago", g_last_tx_ok ? "OK" : "ERR", static_cast<unsigned long>(age_seconds(g_last_tx_ts, g_last_tx_ms)));
|
||||
|
||||
#ifdef ENABLE_TEST_MODE
|
||||
if (strlen(g_test_code) > 0) {
|
||||
display.setCursor(0, 48);
|
||||
display.printf("Test %s", g_test_code);
|
||||
}
|
||||
#endif
|
||||
|
||||
display.display();
|
||||
}
|
||||
|
||||
static void render_sender_measurement() {
|
||||
display.clearDisplay();
|
||||
display.setCursor(0, 0);
|
||||
display.printf("E %.1f kWh", g_last_meter.energy_total_kwh);
|
||||
display.setCursor(0, 12);
|
||||
display.printf("P %.0fW", g_last_meter.total_power_w);
|
||||
display.setCursor(0, 24);
|
||||
display.printf("L1 %.0fV %.0fW", g_last_meter.phase_voltage_v[0], g_last_meter.phase_power_w[0]);
|
||||
display.setCursor(0, 36);
|
||||
display.printf("L2 %.0fV %.0fW", g_last_meter.phase_voltage_v[1], g_last_meter.phase_power_w[1]);
|
||||
display.setCursor(0, 48);
|
||||
display.printf("L3 %.0fV %.0fW", g_last_meter.phase_voltage_v[2], 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");
|
||||
|
||||
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, 36);
|
||||
if (latest == 0 || !time_is_synced()) {
|
||||
display.print("Last upd: --:--");
|
||||
} else {
|
||||
time_t t = latest;
|
||||
struct tm timeinfo;
|
||||
localtime_r(&t, &timeinfo);
|
||||
display.printf("Last upd: %02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
||||
}
|
||||
|
||||
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) {
|
||||
display.printf("%s B%u", status.last_data.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 %.1f kWh", status.last_data.energy_total_kwh);
|
||||
display.setCursor(0, 24);
|
||||
display.printf("P %.0fW", status.last_data.total_power_w);
|
||||
display.setCursor(0, 36);
|
||||
display.printf("L1 %.0fV %.0fW", status.last_data.phase_voltage_v[0], status.last_data.phase_power_w[0]);
|
||||
display.setCursor(0, 48);
|
||||
display.printf("L2 %.0fV %.0fW", status.last_data.phase_voltage_v[1], status.last_data.phase_power_w[1]);
|
||||
display.setCursor(0, 56);
|
||||
display.printf("L3 %.0fV %.0fW", status.last_data.phase_voltage_v[2], status.last_data.phase_power_w[2]);
|
||||
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 = digitalRead(PIN_OLED_CTRL) == HIGH;
|
||||
|
||||
bool in_boot_window = (millis() - g_boot_ms) < OLED_AUTO_OFF_MS;
|
||||
if (in_boot_window) {
|
||||
g_oled_on = true;
|
||||
oled_set_power(true);
|
||||
} else {
|
||||
if (ctrl_high) {
|
||||
g_oled_on = true;
|
||||
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();
|
||||
}
|
||||
|
||||
if (!ctrl_high && g_oled_off_start > 0 && millis() - g_oled_off_start > OLED_AUTO_OFF_MS) {
|
||||
g_oled_on = false;
|
||||
}
|
||||
|
||||
if (g_oled_on) {
|
||||
oled_set_power(true);
|
||||
} else {
|
||||
oled_set_power(false);
|
||||
g_prev_ctrl_high = ctrl_high;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user