feat: battery monitoring, external RTC persistence, SD used-space fix
- Add BQ27220 fuel gauge: reads SOC percentage every 5s - Add BQ25896 PMIC: detects charging status (pre-charge/fast/done) - Add MAX31329 external RTC: reads time on boot, syncs from NTP - Battery UI: icon + percentage in header, cyan when charging - Fix SD used bytes: fallback to recursive dir walk when usedBytes() returns 0 - Libraries: PMIC_BQ25896, kode_bq27220, kode_MAX31329
This commit is contained in:
@@ -65,3 +65,6 @@ lib_deps =
|
||||
https://github.com/bitbank2/bb_captouch.git
|
||||
https://github.com/xreef/SimpleFTPServer.git
|
||||
bblanchon/ArduinoJson @ ^7
|
||||
https://github.com/sqmsmu/PMIC_BQ25896.git
|
||||
https://github.com/kodediy/kode_bq27220.git
|
||||
https://github.com/kodediy/kode_MAX31329.git
|
||||
|
||||
+149
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <kodedot/display_manager.h>
|
||||
#include <kodedot/pin_config.h>
|
||||
@@ -19,6 +20,9 @@
|
||||
#include <ArduinoJson.h>
|
||||
#include <SimpleFTPServer.h>
|
||||
#include <time.h>
|
||||
#include <PMIC_BQ25896.h>
|
||||
#include <BQ27220.h>
|
||||
#include <kode_MAX31329.h>
|
||||
#include "ui.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -37,6 +41,15 @@ static Adafruit_NeoPixel pixel(NEO_PIXEL_COUNT, NEO_PIXEL_PIN, NEO_GRB + NEO_KHZ
|
||||
// FTP server
|
||||
FtpServer ftpSrv;
|
||||
|
||||
// Battery fuel gauge (BQ27220 @ 0x55)
|
||||
static BQ27220 fuelGauge;
|
||||
|
||||
// PMIC / Charger (BQ25896 @ 0x6B)
|
||||
static PMIC_BQ25896 pmic;
|
||||
|
||||
// External RTC (MAX31329 @ 0xD0)
|
||||
static MAX31329 rtc;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LED colour definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -53,7 +66,11 @@ static bool sd_ok = false;
|
||||
static bool wifi_connected = false;
|
||||
static bool ftp_client_on = false;
|
||||
static bool ftp_transfer = false;
|
||||
static bool rtc_ok = false;
|
||||
static bool gauge_ok = false;
|
||||
static bool pmic_ok = false;
|
||||
static unsigned long last_time_update = 0;
|
||||
static unsigned long last_batt_update = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NeoPixel helpers
|
||||
@@ -84,10 +101,30 @@ static void updateLed() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Get used / total space on SD card in MiB and push to UI */
|
||||
static uint64_t calcDirSize(fs::FS &fs, const char *dirname) {
|
||||
uint64_t total = 0;
|
||||
File root = fs.open(dirname);
|
||||
if (!root || !root.isDirectory()) return 0;
|
||||
File f = root.openNextFile();
|
||||
while (f) {
|
||||
if (f.isDirectory()) {
|
||||
total += calcDirSize(fs, f.path());
|
||||
} else {
|
||||
total += f.size();
|
||||
}
|
||||
f = root.openNextFile();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static void updateSdInfo() {
|
||||
if (!sd_ok) return;
|
||||
uint64_t total = SD_MMC.totalBytes() / (1024ULL * 1024ULL);
|
||||
uint64_t used = SD_MMC.usedBytes() / (1024ULL * 1024ULL);
|
||||
/* SD_MMC.usedBytes() returns 0 on exFAT / large cards — walk filesystem */
|
||||
if (used == 0 && total > 0) {
|
||||
used = calcDirSize(SD_MMC, "/") / (1024ULL * 1024ULL);
|
||||
}
|
||||
ui_set_sd(used, total);
|
||||
}
|
||||
|
||||
@@ -157,6 +194,36 @@ void ftpTransferCallback(FtpTransferOperation ftpOperation, const char *name, ui
|
||||
updateLed();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Battery monitoring
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Update battery percentage and charging status every 5 seconds */
|
||||
static void updateBattery() {
|
||||
unsigned long now = millis();
|
||||
if (now - last_batt_update < 5000) return;
|
||||
last_batt_update = now;
|
||||
|
||||
int soc = -1;
|
||||
bool charging = false;
|
||||
|
||||
/* Read state-of-charge from fuel gauge */
|
||||
if (gauge_ok) {
|
||||
soc = fuelGauge.readStateOfChargePercent();
|
||||
if (soc < 0 || soc > 100) soc = -1; // sanity check
|
||||
}
|
||||
|
||||
/* Read charging status from PMIC */
|
||||
if (pmic_ok) {
|
||||
pmic.setCONV_START(true); // trigger ADC conversion
|
||||
uint8_t chrg_stat = pmic.get_VBUS_STAT_reg().chrg_stat;
|
||||
// chrg_stat: 0=Not Charging, 1=Pre-charge, 2=Fast Charging, 3=Charge Done
|
||||
charging = (chrg_stat == 1 || chrg_stat == 2);
|
||||
}
|
||||
|
||||
ui_set_battery(soc, charging);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Time display
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -269,6 +336,82 @@ static bool initWiFi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Initialise I2C bus and battery / RTC peripherals */
|
||||
static void initPeripherals() {
|
||||
Wire.begin(48, 47); // SDA=48, SCL=47
|
||||
|
||||
/* ── Fuel Gauge (BQ27220 @ 0x55) ──────────────────────────── */
|
||||
gauge_ok = fuelGauge.begin();
|
||||
if (gauge_ok) {
|
||||
int soc = fuelGauge.readStateOfChargePercent();
|
||||
int mv = fuelGauge.readVoltageMillivolts();
|
||||
Serial.printf("[BAT] Fuel gauge ready — SOC: %d%% Voltage: %d mV\n", soc, mv);
|
||||
} else {
|
||||
Serial.println("[BAT] Warning: BQ27220 fuel gauge not found");
|
||||
}
|
||||
|
||||
/* ── PMIC (BQ25896 @ 0x6B) ───────────────────────────────── */
|
||||
pmic.begin();
|
||||
pmic_ok = pmic.isConnected();
|
||||
if (pmic_ok) {
|
||||
pmic.setCONV_START(true); // trigger first ADC conversion
|
||||
Serial.printf("[BAT] PMIC ready — VBUS: %d mV VBAT: %d mV\n",
|
||||
pmic.getVBUSV(), pmic.getBATV());
|
||||
} else {
|
||||
Serial.println("[BAT] Warning: BQ25896 PMIC not found");
|
||||
}
|
||||
|
||||
/* ── External RTC (MAX31329 @ 0xD0) ──────────────────────── */
|
||||
rtc.begin();
|
||||
/* Try reading time — if year is valid (>2020), set ESP32 system clock */
|
||||
if (rtc.readTime()) {
|
||||
if (rtc.t.year >= 2020 && rtc.t.year <= 2099) {
|
||||
rtc_ok = true;
|
||||
struct tm t = {};
|
||||
t.tm_year = rtc.t.year - 1900;
|
||||
t.tm_mon = rtc.t.month - 1;
|
||||
t.tm_mday = rtc.t.day;
|
||||
t.tm_hour = rtc.t.hour;
|
||||
t.tm_min = rtc.t.minute;
|
||||
t.tm_sec = rtc.t.second;
|
||||
struct timeval tv = { .tv_sec = mktime(&t), .tv_usec = 0 };
|
||||
settimeofday(&tv, NULL);
|
||||
Serial.printf("[RTC] External RTC time: %04d-%02d-%02d %02d:%02d:%02d\n",
|
||||
rtc.t.year, rtc.t.month, rtc.t.day,
|
||||
rtc.t.hour, rtc.t.minute, rtc.t.second);
|
||||
} else {
|
||||
Serial.printf("[RTC] External RTC has invalid year (%d) — will set from NTP\n", rtc.t.year);
|
||||
}
|
||||
} else {
|
||||
Serial.println("[RTC] Warning: MAX31329 external RTC not responding");
|
||||
}
|
||||
}
|
||||
|
||||
/** Write current system time to external RTC (call after NTP sync) */
|
||||
static void syncRtcFromSystem() {
|
||||
if (!rtc_ok && !gauge_ok) return; // RTC not available
|
||||
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) return;
|
||||
|
||||
rtc.t.year = timeinfo.tm_year + 1900;
|
||||
rtc.t.month = timeinfo.tm_mon + 1;
|
||||
rtc.t.day = timeinfo.tm_mday;
|
||||
rtc.t.hour = timeinfo.tm_hour;
|
||||
rtc.t.minute = timeinfo.tm_min;
|
||||
rtc.t.second = timeinfo.tm_sec;
|
||||
rtc.t.dayOfWeek = timeinfo.tm_wday;
|
||||
|
||||
if (rtc.writeTime()) {
|
||||
rtc_ok = true;
|
||||
Serial.printf("[RTC] Updated external RTC: %04d-%02d-%02d %02d:%02d:%02d\n",
|
||||
rtc.t.year, rtc.t.month, rtc.t.day,
|
||||
rtc.t.hour, rtc.t.minute, rtc.t.second);
|
||||
} else {
|
||||
Serial.println("[RTC] Warning: Failed to write time to external RTC");
|
||||
}
|
||||
}
|
||||
|
||||
/** Configure NTP with Vienna timezone and wait briefly for sync */
|
||||
static void initNtp() {
|
||||
Serial.println("[NTP] Configuring time (Vienna CET/CEST)...");
|
||||
@@ -280,6 +423,8 @@ static void initNtp() {
|
||||
char buf[32];
|
||||
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
|
||||
Serial.printf("[NTP] Time synced: %s\n", buf);
|
||||
/* Write NTP time to external RTC for persistence */
|
||||
syncRtcFromSystem();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -336,6 +481,9 @@ void setup() {
|
||||
// Create the LVGL status UI
|
||||
ui_init();
|
||||
|
||||
// Initialise I2C peripherals (battery, RTC)
|
||||
initPeripherals();
|
||||
|
||||
// IO Expander — check SD card presence
|
||||
if (!checkSdPresence()) {
|
||||
Serial.println("[SD] WARNING: SD card not detected by IO expander");
|
||||
@@ -388,5 +536,6 @@ void loop() {
|
||||
ftpSrv.handleFTP();
|
||||
display.update();
|
||||
updateTimeDisplay();
|
||||
updateBattery();
|
||||
delay(5);
|
||||
}
|
||||
+63
@@ -42,6 +42,9 @@ static lv_obj_t *lbl_time;
|
||||
static lv_obj_t *lbl_date;
|
||||
static lv_obj_t *obj_error_bar;
|
||||
static lv_obj_t *lbl_error;
|
||||
static lv_obj_t *lbl_battery;
|
||||
static lv_obj_t *obj_batt_icon;
|
||||
static lv_obj_t *obj_batt_fill;
|
||||
|
||||
/* ── cached IP for FTP URL ───────────────────────────────────────── */
|
||||
static char cached_ip[48] = "0.0.0.0";
|
||||
@@ -77,6 +80,32 @@ void ui_init()
|
||||
LV_ALIGN_TOP_MID, 0, 16, "FTP Explorer");
|
||||
lv_obj_set_style_text_letter_space(lbl_title, 2, 0);
|
||||
|
||||
/* ── battery indicator (top-right) ─────────────────────────── */
|
||||
/* battery outline (22x12 rounded rect) */
|
||||
obj_batt_icon = lv_obj_create(scr);
|
||||
lv_obj_set_size(obj_batt_icon, 30, 14);
|
||||
lv_obj_set_style_radius(obj_batt_icon, 3, 0);
|
||||
lv_obj_set_style_bg_opa(obj_batt_icon, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_color(obj_batt_icon, lv_color_hex(CLR_GRAY), 0);
|
||||
lv_obj_set_style_border_width(obj_batt_icon, 1, 0);
|
||||
lv_obj_set_style_pad_all(obj_batt_icon, 2, 0);
|
||||
lv_obj_clear_flag(obj_batt_icon, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_align(obj_batt_icon, LV_ALIGN_TOP_RIGHT, -28, 22);
|
||||
|
||||
/* battery fill bar (inside the outline) */
|
||||
obj_batt_fill = lv_obj_create(obj_batt_icon);
|
||||
lv_obj_set_size(obj_batt_fill, 0, 8);
|
||||
lv_obj_set_style_radius(obj_batt_fill, 1, 0);
|
||||
lv_obj_set_style_bg_color(obj_batt_fill, lv_color_hex(CLR_GREEN), 0);
|
||||
lv_obj_set_style_bg_opa(obj_batt_fill, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(obj_batt_fill, 0, 0);
|
||||
lv_obj_clear_flag(obj_batt_fill, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_align(obj_batt_fill, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
/* battery percentage text */
|
||||
lbl_battery = make_label(scr, &lv_font_montserrat_12, CLR_GRAY,
|
||||
LV_ALIGN_TOP_RIGHT, -62, 23, "--");
|
||||
|
||||
/* subtle separator line */
|
||||
static lv_point_precise_t line_pts[] = {{40, 0}, {370, 0}};
|
||||
line_sep = lv_line_create(scr);
|
||||
@@ -322,3 +351,37 @@ void ui_set_error(const char *msg)
|
||||
lv_obj_add_flag(obj_error_bar, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── ui_set_battery ──────────────────────────────────────────────── */
|
||||
void ui_set_battery(int pct, bool charging)
|
||||
{
|
||||
/* pct < 0 means fuel gauge unavailable */
|
||||
if (pct < 0) {
|
||||
lv_label_set_text(lbl_battery, "--");
|
||||
lv_obj_set_size(obj_batt_fill, 0, 8);
|
||||
return;
|
||||
}
|
||||
if (pct > 100) pct = 100;
|
||||
|
||||
/* percentage label */
|
||||
if (charging) {
|
||||
lv_label_set_text_fmt(lbl_battery, LV_SYMBOL_CHARGE " %d%%", pct);
|
||||
lv_obj_set_style_text_color(lbl_battery, lv_color_hex(CLR_CYAN), 0);
|
||||
} else {
|
||||
lv_label_set_text_fmt(lbl_battery, "%d%%", pct);
|
||||
lv_obj_set_style_text_color(lbl_battery, lv_color_hex(CLR_GRAY), 0);
|
||||
}
|
||||
|
||||
/* fill bar width: max ~22px (inner width of battery outline) */
|
||||
int fill_w = (22 * pct) / 100;
|
||||
if (fill_w < 1 && pct > 0) fill_w = 1;
|
||||
lv_obj_set_size(obj_batt_fill, fill_w, 8);
|
||||
|
||||
/* colour: green > 20%, red <= 20%, cyan when charging */
|
||||
uint32_t clr = CLR_GREEN;
|
||||
if (charging) clr = CLR_CYAN;
|
||||
else if (pct <= 20) clr = CLR_RED;
|
||||
lv_obj_set_style_bg_color(obj_batt_fill, lv_color_hex(clr), 0);
|
||||
lv_obj_set_style_border_color(obj_batt_icon,
|
||||
lv_color_hex(charging ? CLR_CYAN : CLR_GRAY), 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user