mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 18:38:17 +00:00
tiny ui v1
This commit is contained in:
@@ -32,7 +32,7 @@ Bootloader is Adafruit UF2 / SoftDevice **s140 v6.1.1**, app @ `0x26000`
|
||||
|-----------------|--------------------|-----------------------------------------------|
|
||||
| MCU | nRF52840 | BLE5, native USB, 1 MB flash / 256 KB RAM |
|
||||
| LoRa | SX1262 (AcSip S62F)| 22 dBm, embedded 32 MHz TCXO on DIO3 |
|
||||
| Display | SSD1315 128x64 OLED| I2C0 @ 0x3C (SSD1306-compatible, no reset) |
|
||||
| Display | SSD1315 64x32 OLED | I2C0 @ 0x3C (64x32 glass at RAM col 32/row 32) |
|
||||
| GNSS | u-blox MIA-M10Q | UART0 @ 38400, enable P1.10 |
|
||||
| Ext flash | ZD25WQ32C 32 Mbit | QSPI -> `/ext` LittleFS (contacts/channels) |
|
||||
| IMU | ICM-20948 | 2nd I2C bus @ 0x69 — present, not used |
|
||||
@@ -90,3 +90,28 @@ The QSPI flash needs no verification: the ZD25WQ32C JEDEC id (`ba 60 16`) and
|
||||
deep-power-down timings are datasheet-confirmed, and `/ext` reuses the same
|
||||
`nordic,qspi-nor` + LittleFS automount path as the older MX25R boards — a blank
|
||||
chip auto-formats on first mount, no formatter step required.
|
||||
|
||||
## Display / UI status (known incomplete)
|
||||
|
||||
The panel is a **64x32** glass mapped into the SSD1315's 128x64 RAM at column
|
||||
32 / row 32 (LilyGo `Display.ino` inits `Adafruit_SSD1306(128,64)` then
|
||||
`setOffsetCursor(32,32)`). The devicetree now expresses this — `width=64`,
|
||||
`height=32`, `segment-offset=32`, `page-offset=4` — but two things remain:
|
||||
|
||||
1. **Offsets are inferred, not bench-verified.** If the visible window is
|
||||
misplaced, try the alternate `multiplex-ratio=31` + `display-offset=32` +
|
||||
`page-offset=0`.
|
||||
2. **Tiny UI (implemented, needs bench polish).** `helpers/ui-button/ui_pages.c`
|
||||
now has a runtime "tiny mode" (auto-enabled when the panel is <48 px tall):
|
||||
it drops the top bar + page-dots + separator, uses the full height for
|
||||
content with tightened line spacing, and flashes the page title for ~1 s on
|
||||
each change (there is no persistent bar — matches the "no bar, full content"
|
||||
choice). Compact layouts are provided for Messages, Radio, GPS and Status;
|
||||
the remaining pages (Recent, Traffic, Sensors, toggles) render through the
|
||||
normal path at full height and may still clip horizontally until they get
|
||||
their own compact branch. 128x64 boards are unaffected. Pixel positioning
|
||||
was written from the datasheet, not a bench — expect to nudge it on hardware.
|
||||
|
||||
Separately, early hardware testing showed **occasional spontaneous reboots** —
|
||||
untriaged, likely power/brownout on TX or a fault loop; capture an RTT/USB
|
||||
console log from a debug build to get a backtrace.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Hardware:
|
||||
# - nRF52840 SoC, SX1262 LoRa (22 dBm, embedded 32 MHz TCXO via DIO3)
|
||||
# - GPIO antenna switch: VC1=TX (P1.13), VC2=RX (P1.07)
|
||||
# - SSD1315 128x64 OLED @ I2C 0x3C (SSD1306-compatible, no reset pin)
|
||||
# - SSD1315 64x32 OLED @ I2C 0x3C (SSD1306-compatible, 64x32 glass at RAM 32,32)
|
||||
# - u-blox MIA-M10Q GPS on UART0 @ 38400, power enable P1.10
|
||||
# - ZD25WQ32C 32Mbit (4MB) QSPI flash -> /ext
|
||||
# - Battery ADC on AIN3 (P0.05), divider enable P0.25 (drive HIGH to read)
|
||||
|
||||
@@ -236,10 +236,24 @@
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- I2C0: SSD1315 OLED ----
|
||||
/* ---- I2C0: SSD1315 OLED (64x32 glass) ----
|
||||
* SSD1315 is SSD1306-compatible; the solomon,ssd1306 driver drives it. No
|
||||
* hardware reset line on this board (SCREEN_RST = -1). Standard 0.96" 128x64
|
||||
* orientation — if the panel is mounted rotated, flip segment-remap/com-invdir. */
|
||||
* hardware reset line (SCREEN_RST = -1).
|
||||
*
|
||||
* The physical glass is only 64x32, mapped into the controller's 128x64 RAM
|
||||
* at column 32 / row 32 — LilyGo's Display.ino inits Adafruit_SSD1306(128,64)
|
||||
* then calls setOffsetCursor(32,32), i.e. content is drawn into the visible
|
||||
* window at RAM (32,32). We express that as a 64x32 panel whose origin is
|
||||
* segment 32 / page 4 (row 32 = page 4), while keeping the vendor's 128x64
|
||||
* COM scan: multiplex-ratio 63, display-offset 0, alternative COM pins
|
||||
* (NOT com-sequential). The offsets are inferred from the vendor example and
|
||||
* may need bench trimming; if the window is misplaced, the usual alternate is
|
||||
* multiplex-ratio 31 + display-offset 32 + page-offset 0.
|
||||
*
|
||||
* NOTE: this only fixes the panel geometry. ZephCore's UI is laid out for
|
||||
* 128x64 — a 64x32 panel needs a simplified "tiny" UI (not yet in ZephCore;
|
||||
* upstream MeshCore has a ui-tiny reference). Until then content will overflow
|
||||
* the small window even though it is now positioned correctly. */
|
||||
&i2c0 {
|
||||
compatible = "nordic,nrf-twim";
|
||||
status = "okay";
|
||||
@@ -251,10 +265,10 @@
|
||||
oled: ssd1306@3c {
|
||||
compatible = "solomon,ssd1306";
|
||||
reg = <0x3c>;
|
||||
width = <128>;
|
||||
height = <64>;
|
||||
segment-offset = <0>;
|
||||
page-offset = <0>;
|
||||
width = <64>;
|
||||
height = <32>;
|
||||
segment-offset = <32>;
|
||||
page-offset = <4>;
|
||||
display-offset = <0>;
|
||||
multiplex-ratio = <63>;
|
||||
segment-remap;
|
||||
|
||||
@@ -49,13 +49,36 @@ LOG_MODULE_REGISTER(ui_pages, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
|
||||
#define FONT_H mc_display_font_height()
|
||||
#define DISP_W mc_display_width()
|
||||
#define DISP_H mc_display_height()
|
||||
#define LINE_H (FONT_H + 2) /* font height + 2px gap */
|
||||
#define TOP_BAR_Y 0
|
||||
#define DOTS_Y (FONT_H + 2) /* just below top bar */
|
||||
#define SEP_Y (DOTS_Y + 4) /* below dots */
|
||||
#define CONTENT_Y (SEP_Y + 4) /* below separator */
|
||||
#define MAX_CHARS (DISP_W / (FONT_W ? FONT_W : 1))
|
||||
|
||||
/* ---- Tiny-panel mode (e.g. 64x32 wristband OLED) ----
|
||||
* When the panel is too short for the top bar + page dots + separator, drop
|
||||
* that chrome and hand the whole panel to page content, tightening the line
|
||||
* spacing so four rows of the 6x8 font fit in 32px. Auto-detected from the
|
||||
* panel height, so 128x64 boards are unaffected. There is no persistent title
|
||||
* bar on tiny panels — the page name is flashed briefly on a page change (see
|
||||
* tiny_flash_kick / render_tiny_title). */
|
||||
static inline bool ui_tiny(void)
|
||||
{
|
||||
return DISP_H < 48;
|
||||
}
|
||||
|
||||
static inline int ui_line_h(void)
|
||||
{
|
||||
return ui_tiny() ? FONT_H : (FONT_H + 2); /* drop the inter-line gap */
|
||||
}
|
||||
|
||||
static inline int ui_content_y(void)
|
||||
{
|
||||
return ui_tiny() ? 0 : (SEP_Y + 4); /* full height when tiny */
|
||||
}
|
||||
|
||||
#define LINE_H (ui_line_h())
|
||||
#define CONTENT_Y (ui_content_y())
|
||||
|
||||
/* Compact RGB565 palette for small color TFTs. These are used only through
|
||||
* mc_display_has_color(); monochrome displays keep the existing CFB path. */
|
||||
#define UI_COLOR_BG MC_COLOR_BLACK
|
||||
@@ -96,6 +119,18 @@ static inline int centered_row(int idx, int total)
|
||||
/* ========== Global State ========== */
|
||||
static struct ui_state state;
|
||||
|
||||
/* Tiny-mode page-title flash: after a page change we show the page name for
|
||||
* TINY_FLASH_MS, then re-render into content (there is no persistent bar on
|
||||
* tiny panels). The follow-up render is scheduled by the UI task via
|
||||
* ui_pages_flash_remaining_ms(). */
|
||||
#define TINY_FLASH_MS 1000U
|
||||
static uint32_t tiny_flash_until;
|
||||
|
||||
static void tiny_flash_kick(void)
|
||||
{
|
||||
tiny_flash_until = k_uptime_get_32() + TINY_FLASH_MS;
|
||||
}
|
||||
|
||||
static uint8_t activity_rx[ACTIVITY_GRAPH_SAMPLES];
|
||||
static uint8_t activity_tx[ACTIVITY_GRAPH_SAMPLES];
|
||||
static uint8_t activity_head;
|
||||
@@ -171,6 +206,10 @@ static char time_source_tag(enum time_sync_source src)
|
||||
|
||||
static void render_top_bar(void)
|
||||
{
|
||||
if (ui_tiny()) {
|
||||
return; /* no room for a bar; page title is flashed on change */
|
||||
}
|
||||
|
||||
bool color = mc_display_has_color();
|
||||
/* Right side: "HH:MM<S> XX%" — clock (with 1-char source tag) then
|
||||
* battery, right-aligned. Built first so the node name can be clipped
|
||||
@@ -252,6 +291,10 @@ static void render_top_bar(void)
|
||||
|
||||
static void render_page_indicator(void)
|
||||
{
|
||||
if (ui_tiny()) {
|
||||
return; /* no room for dots/separator on a tiny panel */
|
||||
}
|
||||
|
||||
/* Centered dots matching Arduino layout:
|
||||
* Each dot spaced 10px apart, centered on screen.
|
||||
* Current page = filled 3x3 block, others = single pixel. */
|
||||
@@ -489,11 +532,65 @@ static void draw_activity_graph(int x, int y, int w, int h)
|
||||
|
||||
/* ========== Page Renderers ========== */
|
||||
|
||||
/* ========== Tiny-panel page title (flashed on page change) ========== */
|
||||
|
||||
static const char *tiny_page_title(enum ui_page p)
|
||||
{
|
||||
switch (p) {
|
||||
case UI_PAGE_MESSAGES: return "MESSAGES";
|
||||
case UI_PAGE_RECENT: return "RECENT";
|
||||
case UI_PAGE_RADIO: return "RADIO";
|
||||
case UI_PAGE_TRAFFIC: return "TRAFFIC";
|
||||
case UI_PAGE_BLUETOOTH: return "BLUETOOTH";
|
||||
case UI_PAGE_ADVERT: return "ADVERT";
|
||||
case UI_PAGE_GPS: return "GPS";
|
||||
case UI_PAGE_BUZZER: return "BUZZER";
|
||||
case UI_PAGE_LEDS: return "LEDS";
|
||||
case UI_PAGE_SENSORS: return "SENSORS";
|
||||
case UI_PAGE_OFFGRID: return "OFFGRID";
|
||||
case UI_PAGE_DFU: return "DFU";
|
||||
case UI_PAGE_SHUTDOWN: return "SHUTDOWN";
|
||||
case UI_PAGE_STATUS: return "STATUS";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
static void render_tiny_title(void)
|
||||
{
|
||||
const char *title = tiny_page_title(active_pages[current_page_idx]);
|
||||
int rows = (state.battery_mv > 0) ? 2 : 1;
|
||||
|
||||
draw_centered(centered_row(0, rows), title);
|
||||
|
||||
if (state.battery_mv > 0) {
|
||||
char buf[8];
|
||||
uint8_t pct = state.battery_pct ? state.battery_pct
|
||||
: calc_battery_pct(state.battery_mv);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%u%%", pct);
|
||||
draw_centered(centered_row(1, rows), buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void render_messages(void)
|
||||
{
|
||||
/* 3 centered rows: msg count, BLE status, offgrid status */
|
||||
char buf[24];
|
||||
|
||||
if (ui_tiny()) {
|
||||
snprintf(buf, sizeof(buf), "MSG:%u", state.msg_count);
|
||||
draw_centered(centered_row(0, 3), buf);
|
||||
draw_centered(centered_row(1, 3),
|
||||
state.ble_connected ? "BLE:conn" : "BLE:adv");
|
||||
|
||||
uint32_t up_s = (uint32_t)(k_uptime_get() / 1000);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%uh%02um",
|
||||
(up_s % 86400) / 3600, (up_s % 3600) / 60);
|
||||
draw_centered(centered_row(2, 3), buf);
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_compact_color_home()) {
|
||||
int y = CONTENT_Y;
|
||||
uint16_t ble_color = state.ble_connected ? UI_COLOR_OK : UI_COLOR_WARN;
|
||||
@@ -659,6 +756,19 @@ static void render_radio(void)
|
||||
uint16_t warn_color = (state.lora_apc_enabled && state.lora_apc_reduction > 0)
|
||||
? UI_COLOR_WARN : UI_COLOR_OK;
|
||||
|
||||
if (ui_tiny()) {
|
||||
int tx = state.lora_effective_tx_power > 0
|
||||
? state.lora_effective_tx_power : state.lora_tx_power;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%u.%uM", freq_mhz, freq_frac / 100);
|
||||
draw_centered(centered_row(0, 3), buf);
|
||||
snprintf(buf, sizeof(buf), "SF%u BW%u", state.lora_sf, bw_int);
|
||||
draw_centered(centered_row(1, 3), buf);
|
||||
snprintf(buf, sizeof(buf), "P%d %s", tx, rx_mode);
|
||||
draw_centered(centered_row(2, 3), buf);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mc_display_has_color()) {
|
||||
uint16_t state_color = state.lora_tx_active ? UI_COLOR_WARN :
|
||||
state.lora_in_rx ? UI_COLOR_ACTIVE :
|
||||
@@ -927,6 +1037,30 @@ static void render_gps(void)
|
||||
int y = CONTENT_Y;
|
||||
bool color = mc_display_has_color();
|
||||
|
||||
if (ui_tiny()) {
|
||||
if (!state.gps_available) {
|
||||
draw_centered(centered_row(0, 1), "No GPS");
|
||||
} else if (!state.gps_enabled) {
|
||||
draw_centered(centered_row(0, 1), "GPS off");
|
||||
} else if (state.gps_state == 2) {
|
||||
snprintf(buf, sizeof(buf), "Sats:%u", state.gps_satellites);
|
||||
draw_centered(centered_row(0, 2), "Searching");
|
||||
draw_centered(centered_row(1, 2), buf);
|
||||
} else if (state.gps_state == 1) {
|
||||
draw_centered(centered_row(0, 2), "Standby");
|
||||
if (state.gps_last_fix_age_s != UINT32_MAX) {
|
||||
char tb[12];
|
||||
|
||||
fmt_duration(tb, sizeof(tb), state.gps_last_fix_age_s);
|
||||
snprintf(buf, sizeof(buf), "fix %s", tb);
|
||||
draw_centered(centered_row(1, 2), buf);
|
||||
}
|
||||
} else {
|
||||
draw_centered(centered_row(0, 1), "GPS off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* No GPS hardware on this board */
|
||||
if (!state.gps_available) {
|
||||
if (color) {
|
||||
@@ -1261,6 +1395,33 @@ static void render_status(void)
|
||||
int y = CONTENT_Y;
|
||||
bool color = mc_display_has_color();
|
||||
|
||||
if (ui_tiny()) {
|
||||
uint32_t up_s = (uint32_t)(k_uptime_get() / 1000);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Up %ud%02uh",
|
||||
up_s / 86400, (up_s % 86400) / 3600);
|
||||
draw_centered(centered_row(0, 3), buf);
|
||||
|
||||
if (state.rtc_epoch > 1735689600) {
|
||||
uint32_t ds = state.rtc_epoch % 86400;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%02u:%02u UTC",
|
||||
(unsigned)(ds / 3600), (unsigned)((ds % 3600) / 60));
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "no time");
|
||||
}
|
||||
draw_centered(centered_row(1, 3), buf);
|
||||
|
||||
if (state.battery_mv > 0) {
|
||||
uint8_t pct = state.battery_pct ? state.battery_pct
|
||||
: calc_battery_pct(state.battery_mv);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Batt %u%%", pct);
|
||||
draw_centered(centered_row(2, 3), buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Role label */
|
||||
if (color) {
|
||||
draw_badge(0, y, "MODE", UI_COLOR_ACTIVE);
|
||||
@@ -1364,6 +1525,16 @@ void ui_pages_render(void)
|
||||
ui_refresh_battery();
|
||||
|
||||
mc_display_clear();
|
||||
|
||||
/* Tiny panels flash the page title briefly after a change, then fall
|
||||
* through to content on the follow-up render (scheduled by the UI task
|
||||
* via ui_pages_flash_remaining_ms()). */
|
||||
if (ui_tiny() && k_uptime_get_32() < tiny_flash_until) {
|
||||
render_tiny_title();
|
||||
mc_display_finalize();
|
||||
return;
|
||||
}
|
||||
|
||||
render_top_bar();
|
||||
render_page_indicator();
|
||||
|
||||
@@ -1376,6 +1547,20 @@ void ui_pages_render(void)
|
||||
mc_display_finalize();
|
||||
}
|
||||
|
||||
/* Milliseconds until the tiny-panel title flash expires (0 when not flashing).
|
||||
* The UI task uses this to schedule the one follow-up render that swaps the
|
||||
* flashed title for page content. */
|
||||
uint32_t ui_pages_flash_remaining_ms(void)
|
||||
{
|
||||
if (!ui_tiny()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t now = k_uptime_get_32();
|
||||
|
||||
return (now < tiny_flash_until) ? (tiny_flash_until - now) : 0;
|
||||
}
|
||||
|
||||
void ui_pages_render_splash(void)
|
||||
{
|
||||
mc_display_clear();
|
||||
@@ -1404,6 +1589,7 @@ void ui_pages_render_splash(void)
|
||||
void ui_pages_next(void)
|
||||
{
|
||||
state.shutdown_confirm_time = 0; /* Reset confirmation on navigate */
|
||||
tiny_flash_kick();
|
||||
current_page_idx++;
|
||||
if (current_page_idx >= ACTIVE_PAGE_COUNT) {
|
||||
current_page_idx = 0;
|
||||
@@ -1414,6 +1600,7 @@ void ui_pages_next(void)
|
||||
void ui_pages_prev(void)
|
||||
{
|
||||
state.shutdown_confirm_time = 0; /* Reset confirmation on navigate */
|
||||
tiny_flash_kick();
|
||||
current_page_idx--;
|
||||
if (current_page_idx < 0) {
|
||||
current_page_idx = ACTIVE_PAGE_COUNT - 1;
|
||||
@@ -1423,6 +1610,7 @@ void ui_pages_prev(void)
|
||||
|
||||
void ui_pages_set(enum ui_page page)
|
||||
{
|
||||
tiny_flash_kick();
|
||||
/* Find page in active list, fall back to first active page */
|
||||
for (int i = 0; i < ACTIVE_PAGE_COUNT; i++) {
|
||||
if (active_pages[i] == page) {
|
||||
|
||||
@@ -138,6 +138,14 @@ struct ui_state *ui_pages_get_state(void);
|
||||
*/
|
||||
void ui_pages_render(void);
|
||||
|
||||
/**
|
||||
* Milliseconds until the tiny-panel page-title flash expires, or 0 when not
|
||||
* flashing (and always 0 on normal-sized panels). The UI task uses this to
|
||||
* schedule the single follow-up render that replaces the flashed title with
|
||||
* page content.
|
||||
*/
|
||||
uint32_t ui_pages_flash_remaining_ms(void);
|
||||
|
||||
/**
|
||||
* Render a splash screen.
|
||||
*/
|
||||
|
||||
@@ -162,6 +162,14 @@ static void render_work_handler(struct k_work *work)
|
||||
* OLED: only render when display is on (screen is black when off). */
|
||||
if ((mc_display_is_on() || mc_display_is_epd()) && !splash_active) {
|
||||
ui_pages_render();
|
||||
|
||||
/* Tiny panels flash the page title on a change, then need one
|
||||
* more render to swap it for content once the flash expires. */
|
||||
uint32_t flash_ms = ui_pages_flash_remaining_ms();
|
||||
|
||||
if (flash_ms > 0) {
|
||||
k_work_reschedule(&render_work, K_MSEC(flash_ms + 20));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user