Battery/USB readout and wrap-around menu scrolling
BAT_ADC (GPIO4) sits between two 100k resistors on the switched supply rail, so it reads the cell on battery and ~4.4 V on USB; above 4.25 V is reported as USB power. On battery the caption shows a glyph + percentage from a LiPo OCV table; the menu shows voltage and source. Menu lists now wrap at both ends.
This commit is contained in:
1 parent
1f37ccfb61
commit
f238cfc9bf
11 files changed
+151
-12
No files matched your search
@@ -16,7 +16,9 @@ Either LilyGO T-Embed. The firmware tells them apart at boot by probing the I2C
|
||||
bus (ES7210 on the 2022 board, BQ27220/BQ25896/PN532 on the CC1101 board) and
|
||||
picks the matching pin map from `main/board.c`. Nothing beyond the board is
|
||||
needed: ESP32-S3R8, 16 MB flash, 8 MB PSRAM, ST7789 170×320, EC11 encoder,
|
||||
APA102 (×7) or WS2812 (×8) ring.
|
||||
APA102 (×7) or WS2812 (×8) ring. Printable shells for the original board are in
|
||||
LilyGO's repo under
|
||||
[`Shell_file/`](https://github.com/Xinyuan-LilyGO/T-Embed/tree/master/Shell_file).
|
||||
|
||||
## Build and flash
|
||||
|
||||
@@ -50,8 +52,9 @@ your Wi-Fi. Holding the knob down while the widget starts forces the portal.
|
||||
| double-click | hide / show the caption |
|
||||
| hold | open the menu (hold again, or *Close*, to leave it) |
|
||||
|
||||
In the menu: turn to move, click to select or toggle, click a number to edit it
|
||||
(turn changes it, click confirms), hold to go back. Entries:
|
||||
In the menu: turn to move (the list wraps around), click to select or toggle,
|
||||
click a number to edit it (turn changes it, click confirms), hold to go back.
|
||||
Entries:
|
||||
|
||||
* **Save as favourite / Remove from favourites** — the camera that was playing;
|
||||
saving asks for one of the 12 slots (overwrites it).
|
||||
@@ -60,11 +63,20 @@ In the menu: turn to move, click to select or toggle, click a number to edit it
|
||||
* **Source** — *Random* draws from insecam, *Favourites* cycles the saved slots.
|
||||
* **Time per camera**, **Listing** (mixed / top rated / newest), **Screen
|
||||
brightness**, **Ring brightness**, **Caption**.
|
||||
* **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), and the
|
||||
board name with its IP.
|
||||
* **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), the board
|
||||
name with its IP, and the power source.
|
||||
|
||||
The menu closes by itself after 45 s without input.
|
||||
|
||||
## Battery
|
||||
|
||||
The original board has no fuel gauge, only a 100 k/100 k divider on the supply
|
||||
rail (GPIO 4). On USB that rail sits at ~4.4 V (5 V minus a Schottky drop), so
|
||||
anything above 4.25 V is reported as *USB powered*; below that it is the cell
|
||||
voltage, turned into a rough percentage from a LiPo discharge table. While on
|
||||
battery the caption bar shows a small battery glyph with the percentage; on USB
|
||||
it stays out of the way. The CC1101 board's BQ27220 gauge is not supported.
|
||||
|
||||
## LED ring
|
||||
|
||||
| ring | meaning |
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS "main.c" "board.c" "display.c" "ui.c" "leds.c" "input.c" "net.c"
|
||||
"httpstream.c" "jpegframe.c" "jpegdec.c" "insecam.c" "slideshow.c" "settings.c" "menu.c"
|
||||
"httpstream.c" "jpegframe.c" "jpegdec.c" "insecam.c" "slideshow.c" "settings.c" "menu.c" "battery.c"
|
||||
INCLUDE_DIRS "."
|
||||
PRIV_REQUIRES esp_lcd esp_wifi nvs_flash esp_netif esp_event esp_http_server driver esp_timer
|
||||
PRIV_REQUIRES esp_lcd esp_wifi nvs_flash esp_netif esp_event esp_http_server driver esp_timer esp_adc
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "battery.h"
|
||||
#include "board.h"
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
#include "esp_adc/adc_cali_scheme.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "battery";
|
||||
static adc_oneshot_unit_handle_t unit;
|
||||
static adc_cali_handle_t cali;
|
||||
static adc_channel_t channel;
|
||||
static bool ready;
|
||||
|
||||
void battery_init(void)
|
||||
{
|
||||
if (board->bat_adc < 0) return;
|
||||
adc_unit_t unit_id;
|
||||
if (adc_oneshot_io_to_channel(board->bat_adc, &unit_id, &channel) != ESP_OK || unit_id != ADC_UNIT_1) {
|
||||
ESP_LOGW(TAG, "GPIO%d is not an ADC1 pin", board->bat_adc);
|
||||
return;
|
||||
}
|
||||
adc_oneshot_unit_init_cfg_t ucfg = {.unit_id = ADC_UNIT_1};
|
||||
ESP_ERROR_CHECK(adc_oneshot_new_unit(&ucfg, &unit));
|
||||
adc_oneshot_chan_cfg_t ccfg = {.atten = ADC_ATTEN_DB_12, .bitwidth = ADC_BITWIDTH_DEFAULT};
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(unit, channel, &ccfg));
|
||||
adc_cali_curve_fitting_config_t cal = {.unit_id = ADC_UNIT_1, .chan = channel, .atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT};
|
||||
if (adc_cali_create_scheme_curve_fitting(&cal, &cali) != ESP_OK) cali = NULL;
|
||||
ready = true;
|
||||
int mv, pct;
|
||||
bool usb;
|
||||
if (battery_read(&mv, &pct, &usb)) ESP_LOGI(TAG, "%d mV: %s", mv, usb ? "USB powered" : "on battery");
|
||||
}
|
||||
|
||||
// Open-circuit voltage -> charge, single LiPo cell.
|
||||
static int percent_from_mv(int mv)
|
||||
{
|
||||
static const int table[][2] = {{4200, 100}, {4100, 90}, {4000, 78}, {3900, 62}, {3800, 45},
|
||||
{3700, 25}, {3600, 10}, {3500, 3}, {3300, 0}};
|
||||
const int n = sizeof table / sizeof table[0];
|
||||
if (mv >= table[0][0]) return 100;
|
||||
for (int i = 1; i < n; i++) {
|
||||
if (mv >= table[i][0]) {
|
||||
int v1 = table[i - 1][0], p1 = table[i - 1][1], v0 = table[i][0], p0 = table[i][1];
|
||||
return p0 + (mv - v0) * (p1 - p0) / (v1 - v0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define USB_THRESHOLD_MV 4250
|
||||
|
||||
bool battery_read(int *millivolts, int *percent, bool *on_usb)
|
||||
{
|
||||
if (!ready) return false;
|
||||
long sum = 0;
|
||||
const int samples = 32;
|
||||
for (int i = 0; i < samples; i++) {
|
||||
int raw = 0, mv = 0;
|
||||
if (adc_oneshot_read(unit, channel, &raw) != ESP_OK) return false;
|
||||
if (cali && adc_cali_raw_to_voltage(cali, raw, &mv) == ESP_OK) sum += mv;
|
||||
else sum += raw * 3100 / 4095; // rough fallback without calibration
|
||||
}
|
||||
*millivolts = (int)(sum / samples) * 2; // 100k/100k divider
|
||||
*on_usb = *millivolts > USB_THRESHOLD_MV;
|
||||
*percent = *on_usb ? 100 : percent_from_mv(*millivolts);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
|
||||
// Battery sense on the original T-Embed: BAT_ADC (GPIO4) sits between two 100k
|
||||
// resistors on the switched supply rail (schematic net V_BAT), so V = 2 x V_pin.
|
||||
// That rail is the cell when running on battery, but USB minus a Schottky drop
|
||||
// (~4.4 V) when a cable is in — which is how we tell the two apart. There is no
|
||||
// fuel gauge; the percentage is an open-circuit LiPo estimate. The CC1101 board
|
||||
// is not handled (it has a BQ27220 instead).
|
||||
void battery_init(void);
|
||||
// Averages a burst of samples; returns false if the board has no ADC divider.
|
||||
// *on_usb is set when the rail is above what a LiPo cell can reach.
|
||||
bool battery_read(int *millivolts, int *percent, bool *on_usb);
|
||||
@@ -15,6 +15,7 @@ static const board_t tembed = {
|
||||
.lcd_bgr = false,
|
||||
.enc_a = 2, .enc_b = 1, .enc_btn = 0, .user_key = -1,
|
||||
.led_ws2812 = false, .led_clk = 45, .led_data = 42, .led_count = 7,
|
||||
.bat_adc = 4,
|
||||
.i2c_sda = 18, .i2c_scl = 8,
|
||||
.spi_other_cs = {-1, -1, -1},
|
||||
};
|
||||
@@ -26,6 +27,7 @@ static const board_t tembed_cc1101 = {
|
||||
.lcd_bgr = true,
|
||||
.enc_a = 4, .enc_b = 5, .enc_btn = 0, .user_key = 6,
|
||||
.led_ws2812 = true, .led_clk = -1, .led_data = 14, .led_count = 8,
|
||||
.bat_adc = -1,
|
||||
.i2c_sda = 8, .i2c_scl = 18,
|
||||
.spi_other_cs = {13, 12, 44}, // SD, CC1101, nRF24
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ typedef struct {
|
||||
// LED ring: APA102 (SPI, clk+data) on the original, WS2812 (one wire) on the CC1101
|
||||
bool led_ws2812;
|
||||
int led_clk, led_data, led_count;
|
||||
// battery voltage divider (original board only; -1 = none)
|
||||
int bat_adc;
|
||||
// I2C, used only to tell the two boards apart
|
||||
int i2c_sda, i2c_scl;
|
||||
// chip selects sharing the LCD SPI bus on the CC1101 board; parked high
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "battery.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include "input.h"
|
||||
@@ -39,6 +40,7 @@ void app_main(void)
|
||||
display_set_backlight(settings.lcd_brightness);
|
||||
|
||||
ESP_ERROR_CHECK(jpegdec_init());
|
||||
battery_init();
|
||||
net_init();
|
||||
|
||||
// Holding the knob after power-up forces the setup hotspot.
|
||||
|
||||
+10
-3
@@ -1,4 +1,5 @@
|
||||
#include "menu.h"
|
||||
#include "battery.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include "input.h"
|
||||
@@ -18,7 +19,7 @@ static const char *TAG = "menu";
|
||||
|
||||
typedef enum { VIEW_TOP, VIEW_FAV_PLAY, VIEW_FAV_SAVE, VIEW_FAV_FORGET } view_t;
|
||||
enum { ROW_FAV_TOGGLE, ROW_FAVS, ROW_SOURCE, ROW_INTERVAL, ROW_LISTING, ROW_LCD, ROW_RING, ROW_CAPTION,
|
||||
ROW_FORGET, ROW_WIFI, ROW_IP, ROW_CLOSE, TOP_ROWS };
|
||||
ROW_FORGET, ROW_WIFI, ROW_IP, ROW_BATTERY, ROW_CLOSE, TOP_ROWS };
|
||||
|
||||
static ui_menu_row_t rows[FAV_SLOTS + 1];
|
||||
static char values[TOP_ROWS][20];
|
||||
@@ -49,6 +50,12 @@ static int build_top(const cam_info_t *current, int fav_slot)
|
||||
rows[ROW_FORGET] = (ui_menu_row_t){"Forget a favourite", NULL, fav_count() == 0};
|
||||
rows[ROW_WIFI] = (ui_menu_row_t){"Wi-Fi setup", NULL, false};
|
||||
rows[ROW_IP] = (ui_menu_row_t){board->name, net_ip_str(), true};
|
||||
int mv, pct;
|
||||
bool usb;
|
||||
if (!battery_read(&mv, &pct, &usb)) strlcpy(values[ROW_BATTERY], "no monitor", sizeof values[0]);
|
||||
else if (usb) snprintf(values[ROW_BATTERY], sizeof values[0], "USB %d.%02d V", mv / 1000, (mv % 1000) / 10);
|
||||
else snprintf(values[ROW_BATTERY], sizeof values[0], "%d.%02d V %d%%", mv / 1000, (mv % 1000) / 10, pct);
|
||||
rows[ROW_BATTERY] = (ui_menu_row_t){usb ? "Power" : "Battery", values[ROW_BATTERY], true};
|
||||
rows[ROW_CLOSE] = (ui_menu_row_t){"Close", NULL, false};
|
||||
return TOP_ROWS;
|
||||
}
|
||||
@@ -110,8 +117,8 @@ menu_result_t menu_run(const cam_info_t *current, int *fav_slot_out)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev == INPUT_CW) { sel++; continue; }
|
||||
if (ev == INPUT_CCW) { sel--; continue; }
|
||||
if (ev == INPUT_CW) { sel = (sel + 1) % count; continue; } // wraps around
|
||||
if (ev == INPUT_CCW) { sel = (sel + count - 1) % count; continue; }
|
||||
if (ev == INPUT_LONG || ev == INPUT_KEY) {
|
||||
if (view == VIEW_TOP) break;
|
||||
view = VIEW_TOP;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "slideshow.h"
|
||||
#include "battery.h"
|
||||
#include "display.h"
|
||||
#include "httpstream.h"
|
||||
#include "input.h"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "lvgl.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <stdio.h>
|
||||
@@ -56,6 +58,23 @@ static bool paused;
|
||||
|
||||
static int64_t now_ms(void) { return esp_timer_get_time() / 1000; }
|
||||
|
||||
// Shows "78%" with a battery glyph while running on the battery; hidden on USB power.
|
||||
static void update_battery(void)
|
||||
{
|
||||
static int64_t last;
|
||||
int64_t now = now_ms();
|
||||
if (last && now - last < 15000) return;
|
||||
last = now;
|
||||
int mv, pct;
|
||||
bool usb;
|
||||
if (!battery_read(&mv, &pct, &usb) || usb) { ui_set_battery(NULL); return; }
|
||||
const char *glyph = pct > 80 ? LV_SYMBOL_BATTERY_FULL : pct > 55 ? LV_SYMBOL_BATTERY_3 :
|
||||
pct > 30 ? LV_SYMBOL_BATTERY_2 : pct > 10 ? LV_SYMBOL_BATTERY_1 : LV_SYMBOL_BATTERY_EMPTY;
|
||||
char text[24];
|
||||
snprintf(text, sizeof text, "%s %d%%", glyph, pct);
|
||||
ui_set_battery(text);
|
||||
}
|
||||
|
||||
static insecam_listing_t pick_listing(void)
|
||||
{
|
||||
if (settings.listing == LISTING_RATING) return INSECAM_BY_RATING;
|
||||
@@ -189,6 +208,7 @@ static leave_t show_camera(const cam_info_t *cam)
|
||||
if (shown_ms >= interval_ms) { leave = LEAVE_NEXT; goto done; }
|
||||
if (now - last_progress >= 200) {
|
||||
last_progress = now;
|
||||
update_battery();
|
||||
int permille = 1000 - (int)(shown_ms * 1000 / interval_ms);
|
||||
ui_set_progress(permille);
|
||||
leds_set_countdown(permille);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <string.h>
|
||||
|
||||
static lv_obj_t *scr_status, *lbl_title, *lbl_msg;
|
||||
static lv_obj_t *scr_cam, *canvas, *overlay, *lbl_place, *lbl_meta, *badge, *marker, *bar, *toast;
|
||||
static lv_obj_t *scr_cam, *canvas, *overlay, *lbl_place, *lbl_meta, *lbl_batt, *badge, *marker, *bar, *toast;
|
||||
static lv_obj_t *scr_menu, *menu_title, *menu_row[UI_MENU_ROWS], *menu_label[UI_MENU_ROWS], *menu_value[UI_MENU_ROWS];
|
||||
static lv_obj_t *prev_screen;
|
||||
static lv_timer_t *toast_timer;
|
||||
@@ -69,9 +69,12 @@ void ui_init(void)
|
||||
lv_label_set_long_mode(lbl_place, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_pos(lbl_place, 8, 3);
|
||||
lbl_meta = label(overlay, &lv_font_montserrat_12, 0xc4ccd8);
|
||||
lv_obj_set_width(lbl_meta, LCD_H_RES - 16);
|
||||
lv_obj_set_width(lbl_meta, LCD_H_RES - 76);
|
||||
lv_label_set_long_mode(lbl_meta, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_pos(lbl_meta, 8, 26);
|
||||
lbl_batt = label(overlay, &lv_font_montserrat_12, 0xc4ccd8);
|
||||
lv_obj_align(lbl_batt, LV_ALIGN_BOTTOM_RIGHT, -8, -3);
|
||||
lv_obj_set_hidden(lbl_batt, true);
|
||||
|
||||
bar = lv_bar_create(scr_cam);
|
||||
lv_obj_remove_style_all(bar);
|
||||
@@ -261,3 +264,11 @@ void ui_menu_hide(void)
|
||||
if (lv_screen_active() == scr_menu) lv_screen_load(prev_screen ? prev_screen : scr_cam);
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
void ui_set_battery(const char *text)
|
||||
{
|
||||
lvgl_port_lock(0);
|
||||
if (!text) lv_obj_set_hidden(lbl_batt, true);
|
||||
else { lv_label_set_text(lbl_batt, text); lv_obj_set_hidden(lbl_batt, false); }
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
@@ -23,6 +23,8 @@ void ui_set_overlay_visible(bool visible);
|
||||
bool ui_overlay_visible(void);
|
||||
// Small marker in the top-left corner of the camera page ("FAV 3"); NULL hides it.
|
||||
void ui_set_marker(const char *text);
|
||||
// Battery readout at the right end of the caption bar; NULL hides it.
|
||||
void ui_set_battery(const char *text);
|
||||
|
||||
// --- menu page ---
|
||||
#define UI_MENU_ROWS 6
|
||||
|
||||
Reference in new issue
Block a user