mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-09 19:25:37 +00:00
add t-echo board
fix e-ink displays fix one-button confirmation bug (eg shutdown)
This commit is contained in:
@@ -25,6 +25,7 @@ Other benefits:
|
||||
| **RAK3401 1W** | SX1262 + SKY66122 (30 dBm) | GPS (u-blox MAX-7Q, optional), I2C sensors |
|
||||
| **RAK WisMesh Tag** | SX1262 | GPS (AT6558R), accelerometer, RGB LEDs, buzzer |
|
||||
| **ThinkNode M1** | SX1262 | GPS, e-paper display (SSD1681), QSPI flash, buzzer, RGB LEDs |
|
||||
| **LilyGo T-Echo** | SX1262 (TCXO 1.8V) | GPS (L76K), 1.54" e-paper (SSD1681), BME280, QSPI flash, touch-button backlight |
|
||||
| **Ikoka Nano 30dBm** | SX1262 (E22-900M30S, 30dBm PA) | RGB LEDs |
|
||||
|
||||
### ESP32
|
||||
|
||||
@@ -17,6 +17,7 @@ nRF_boards=(
|
||||
ikoka_nano_30dbm
|
||||
sensecap_solar
|
||||
xiao_nrf52840
|
||||
lilygo_techo
|
||||
)
|
||||
|
||||
ESP32_boards=(
|
||||
|
||||
@@ -222,7 +222,7 @@ set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_com
|
||||
# Detect platform from board name and add platform-specific common config
|
||||
# For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD
|
||||
# string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840")
|
||||
if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840")
|
||||
if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo")
|
||||
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf")
|
||||
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*")
|
||||
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf")
|
||||
|
||||
@@ -541,6 +541,44 @@ config ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS
|
||||
Time in milliseconds before the display turns off
|
||||
after the last user interaction. Set to 0 to disable.
|
||||
|
||||
config ZEPHCORE_UI_CONFIRM_WINDOW_MS
|
||||
int "Double-press confirmation window (ms)"
|
||||
default 500
|
||||
depends on ZEPHCORE_UI_DISPLAY
|
||||
help
|
||||
Maximum time between the two KEY_ENTER presses that confirm
|
||||
destructive actions (shutdown, DFU, offgrid toggle). The default
|
||||
500 ms suits boards where ENTER is a direct short tap. Boards
|
||||
with only one button that emit ENTER via a zephyr,input-longpress
|
||||
filter (T-Echo, RAK4631 Pocket) need a longer window — two
|
||||
1 s holds plus the release gap do not fit in 500 ms. Set to
|
||||
3000 on such boards.
|
||||
|
||||
config ZEPHCORE_DISPLAY_INSET
|
||||
int "Display inset margin (pixels)"
|
||||
default 0
|
||||
depends on ZEPHCORE_UI_DISPLAY
|
||||
help
|
||||
Shrink the usable drawing area by this many pixels on each side.
|
||||
mc_display_width/height() report the reduced size and all draw
|
||||
primitives are offset by this amount. Useful on panels (e.g.
|
||||
LilyGo T-Echo SSD1681) where the outermost rows/columns show
|
||||
hardware artefacts — a small inset hides them behind a clean
|
||||
background margin. Set to 0 on boards without edge artefacts.
|
||||
|
||||
config ZEPHCORE_DISPLAY_LARGE_FONT
|
||||
bool "Use a larger built-in CFB font"
|
||||
default n
|
||||
depends on ZEPHCORE_UI_DISPLAY && CHARACTER_FRAMEBUFFER_USE_DEFAULT_FONTS
|
||||
help
|
||||
Select the smallest Zephyr built-in CFB font whose height is
|
||||
>= 16 pixels (typically the 10x16 font). Intended for larger
|
||||
e-paper panels (T-Echo, ThinkNode M1) where the default 6x8
|
||||
glyph is too small to read comfortably. The ui_pages.c layout
|
||||
auto-centers its content rows using the active font height, so
|
||||
no per-page changes are needed. Requires Zephyr's default CFB
|
||||
fonts to be compiled in (CONFIG_CHARACTER_FRAMEBUFFER_USE_DEFAULT_FONTS).
|
||||
|
||||
config ZEPHCORE_EASTER_EGG_DOOM
|
||||
bool "Enable Doom raycaster easter egg"
|
||||
default n
|
||||
|
||||
@@ -20,6 +20,7 @@ Supported Boards
|
||||
| ThinkNode M6 | `west build -b thinknode_m6 zephcore` | UF2 drag-drop or `west flash` |
|
||||
| RAK WisMesh Tag | `west build -b rak_wismesh_tag zephcore` | UF2 drag-drop or `west flash` |
|
||||
| Ikoka Nano 30dBm | `west build -b ikoka_nano_30dbm zephcore` | UF2 drag-drop |
|
||||
| LilyGo T-Echo | `west build -b lilygo_techo zephcore` | UF2 drag-drop or `west flash` |
|
||||
|
||||
UF2 flash: Double-tap reset button, drag `build/zephyr/zephyr.uf2` to the USB drive.
|
||||
SWD flash: `west flash` (requires J-Link, pyocd, or nrfjprog connected).
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# LilyGo T-Echo board configuration
|
||||
# Copyright (c) 2026 ZephCore
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
config BOARD_LILYGO_TECHO
|
||||
select SOC_NRF52840_QIAA
|
||||
|
||||
# Board power enable (P0.12) + LoRa power enable (P1.05) must go HIGH
|
||||
# before the SX1262 and QSPI flash respond. QSPI NOR init priority is 41 —
|
||||
# the fixed regulators that drive these rails have to run first.
|
||||
if REGULATOR_FIXED
|
||||
config REGULATOR_FIXED_INIT_PRIORITY
|
||||
default 30
|
||||
endif
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2026 ZephCore
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000")
|
||||
board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000")
|
||||
|
||||
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
|
||||
|
||||
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
|
||||
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
|
||||
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)
|
||||
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
|
||||
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
|
||||
@@ -0,0 +1,41 @@
|
||||
# LilyGo T-Echo (nRF52840 + SX1262 + SSD1681 EPD + L76K GPS)
|
||||
# Board-specific configuration
|
||||
#
|
||||
# Hardware:
|
||||
# - nRF52840 SoC, SX1262 LoRa (22 dBm, TCXO 1.8V)
|
||||
# - 1.54" SSD1681 e-paper (200x200 mono) with front-light on P1.11
|
||||
# - L76K GPS module on UART0 @ 9600 baud (on-off via P1.02)
|
||||
# - MX25R1635F 2MB QSPI flash
|
||||
# - BME280 at I2C 0x77 (alt address)
|
||||
# - Battery ADC on AIN2 (P0.04), 150K+150K voltage divider
|
||||
# - Single user button (P1.10, active-low) — no buzzer, e-ink device
|
||||
# - 3-color LEDs (active-low): RED=P0.13, BLUE=P0.14, GREEN=P0.15 (TX-LED)
|
||||
|
||||
# Board identification (matches Arduino MeshCore variant name)
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="LilyGo T-Echo"
|
||||
|
||||
# Device Information Service model name
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="LilyGo T-Echo"
|
||||
|
||||
# SoftDevice firmware ID — T-Echo Adafruit bootloader ships with s140 v6.1.1
|
||||
# (platformio.ini uses nrf52840_s140_v6.ld → app at 0x26000).
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x00B6
|
||||
|
||||
# ========== RAM budget ==========
|
||||
# SSD1681 200x200 CFB framebuffer needs 5000 bytes from k_malloc.
|
||||
# Default heap (2048) is sized for SSD1306 128x64 (1024 bytes).
|
||||
CONFIG_HEAP_MEM_POOL_SIZE=6144
|
||||
|
||||
# Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB).
|
||||
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024
|
||||
|
||||
# ========== Display cosmetics ==========
|
||||
# 200x200 SSD1681 — the default 6x8 font is too small to read comfortably.
|
||||
# Switch to the smallest Zephyr built-in CFB font >= 16 px (typically 10x16).
|
||||
# (Earlier edge-row artefacts were fixed via the partial-update border
|
||||
# waveform in the DTS, so no inset is needed — full 200x200 is usable.)
|
||||
CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT=y
|
||||
|
||||
# Confirmation window — single-button board uses 1 s longpress for ENTER,
|
||||
# so two ENTERs span >2 s. Widen the double-press window accordingly.
|
||||
CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS=3000
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* LilyGo T-Echo — DTS is self-contained.
|
||||
* This overlay is intentionally empty; all nodes live in lilygo_techo.dts.
|
||||
* Any user-level tweaks (e.g. swapping EPD rotation) can be added here.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 ZephCore
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
board:
|
||||
name: lilygo_techo
|
||||
full_name: LilyGo T-Echo (nRF52840 + SX1262 + SSD1681 EPD + L76K GPS)
|
||||
vendor: lilygo
|
||||
socs:
|
||||
- name: nrf52840
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* LilyGo T-Echo pin control definitions
|
||||
* Copyright (c) 2026 ZephCore
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Pin mappings (from Arduino MeshCore variant.h):
|
||||
*
|
||||
* SPI2 (LoRa SX1262): SCK=P0.19, MISO=P0.23, MOSI=P0.22, CS=P0.24
|
||||
* SPI3 (EPD SSD1681): SCK=P0.31, MISO=P1.06, MOSI=P0.29
|
||||
* I2C0 (BME280): SDA=P0.26, SCL=P0.27
|
||||
* UART0 (L76K GPS 9600):TX=P1.08 (→GPS RX), RX=P1.09 (←GPS TX)
|
||||
* QSPI (MX25R1635F): SCK=P1.14, CS=P1.15, IO0=P1.12, IO1=P1.13,
|
||||
* IO2=P0.07, IO3=P0.05
|
||||
*/
|
||||
|
||||
&pinctrl {
|
||||
/* UART0 for L76K GPS at 9600 baud */
|
||||
uart0_default: uart0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(UART_TX, 1, 8)>; /* P1.08 → GPS RX */
|
||||
};
|
||||
group2 {
|
||||
psels = <NRF_PSEL(UART_RX, 1, 9)>; /* P1.09 ← GPS TX */
|
||||
bias-pull-up;
|
||||
};
|
||||
};
|
||||
|
||||
uart0_sleep: uart0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(UART_TX, 1, 8)>,
|
||||
<NRF_PSEL(UART_RX, 1, 9)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
|
||||
/* I2C0 for BME280 sensor */
|
||||
i2c0_default: i2c0_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(TWIM_SDA, 0, 26)>, /* P0.26 */
|
||||
<NRF_PSEL(TWIM_SCL, 0, 27)>; /* P0.27 */
|
||||
};
|
||||
};
|
||||
|
||||
i2c0_sleep: i2c0_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(TWIM_SDA, 0, 26)>,
|
||||
<NRF_PSEL(TWIM_SCL, 0, 27)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
|
||||
/* SPI2 for LoRa SX1262 */
|
||||
spi2_default: spi2_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(SPIM_SCK, 0, 19)>, /* P0.19 */
|
||||
<NRF_PSEL(SPIM_MOSI, 0, 22)>, /* P0.22 */
|
||||
<NRF_PSEL(SPIM_MISO, 0, 23)>; /* P0.23 */
|
||||
};
|
||||
};
|
||||
|
||||
spi2_sleep: spi2_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(SPIM_SCK, 0, 19)>,
|
||||
<NRF_PSEL(SPIM_MOSI, 0, 22)>,
|
||||
<NRF_PSEL(SPIM_MISO, 0, 23)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
|
||||
/* SPI3 for E-Paper SSD1681 — MISO is routed on T-Echo (P1.06)
|
||||
* even though the SSD1681 is write-only; Nordic SPIM needs MISO
|
||||
* configured to avoid driver config errors. */
|
||||
spi3_default: spi3_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(SPIM_SCK, 0, 31)>, /* P0.31 */
|
||||
<NRF_PSEL(SPIM_MOSI, 0, 29)>, /* P0.29 */
|
||||
<NRF_PSEL(SPIM_MISO, 1, 6)>; /* P1.06 */
|
||||
};
|
||||
};
|
||||
|
||||
spi3_sleep: spi3_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(SPIM_SCK, 0, 31)>,
|
||||
<NRF_PSEL(SPIM_MOSI, 0, 29)>,
|
||||
<NRF_PSEL(SPIM_MISO, 1, 6)>;
|
||||
low-power-enable;
|
||||
};
|
||||
};
|
||||
|
||||
/* QSPI for MX25R1635F 2MB external flash — identical pin routing
|
||||
* to ThinkNode M1, T-Echo re-uses Adafruit's reference layout. */
|
||||
qspi_default: qspi_default {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(QSPI_SCK, 1, 14)>,
|
||||
<NRF_PSEL(QSPI_IO0, 1, 12)>,
|
||||
<NRF_PSEL(QSPI_IO1, 1, 13)>,
|
||||
<NRF_PSEL(QSPI_IO2, 0, 7)>,
|
||||
<NRF_PSEL(QSPI_IO3, 0, 5)>,
|
||||
<NRF_PSEL(QSPI_CSN, 1, 15)>;
|
||||
};
|
||||
};
|
||||
|
||||
qspi_sleep: qspi_sleep {
|
||||
group1 {
|
||||
psels = <NRF_PSEL(QSPI_SCK, 1, 14)>,
|
||||
<NRF_PSEL(QSPI_IO0, 1, 12)>,
|
||||
<NRF_PSEL(QSPI_IO1, 1, 13)>,
|
||||
<NRF_PSEL(QSPI_IO2, 0, 7)>,
|
||||
<NRF_PSEL(QSPI_IO3, 0, 5)>;
|
||||
low-power-enable;
|
||||
};
|
||||
group2 {
|
||||
psels = <NRF_PSEL(QSPI_CSN, 1, 15)>;
|
||||
low-power-enable;
|
||||
bias-pull-up;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* LilyGo T-Echo — nRF52840 + SX1262 + SSD1681 1.54" EPD + L76K GPS
|
||||
* Copyright (c) 2026 ZephCore
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Hardware:
|
||||
* - nRF52840 SoC with BLE 5
|
||||
* - SX1262 LoRa radio (22 dBm, DIO2 as RF switch, external TCXO 1.8V)
|
||||
* - SSD1681 1.54" 200x200 mono e-paper display (SPI) with front-light (P1.11)
|
||||
* - MX25R1635F 2MB QSPI flash
|
||||
* - L76K GPS on UART0 (9600 baud, on/off via P1.02)
|
||||
* - BME280 environment sensor on I2C0 @ 0x77 (alt address)
|
||||
* - Battery ADC on AIN2 (P0.04), 150K+150K voltage divider
|
||||
* - Single user button (P1.10, active-low, INPUT_PULLUP)
|
||||
* - 3-color LEDs active-low: RED=P0.13, BLUE=P0.14, GREEN=P0.15
|
||||
*
|
||||
* Power rails (all must be HIGH for peripherals to function):
|
||||
* - Board power enable P0.12 (main peripheral power)
|
||||
* - LoRa power enable P1.05 (also gates GPS reset — shared signal)
|
||||
* - TCXO power enable P0.21 (external 32 MHz TCXO for SX1262)
|
||||
*
|
||||
* Secondary "backlight" touch input (P0.11):
|
||||
* - Capacitive touch pad (TTP223-style) that presents an active-low
|
||||
* momentary output. Its only job is to wake the e-paper front-light:
|
||||
* emits INPUT_KEY_BRIGHTNESSUP, which hits ui_task's default branch —
|
||||
* no page action, but the display-on / auto-off-reset path still runs,
|
||||
* so the backlight lights up for CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS.
|
||||
*
|
||||
* Note: Arduino ENV_INCLUDE_BME280 sits at 0x77; sensors-i2c.dtsi default
|
||||
* is 0x76 — we override below.
|
||||
*/
|
||||
|
||||
/dts-v1/;
|
||||
#include <nordic/nrf52840_qiaa.dtsi>
|
||||
#include "lilygo_techo-pinctrl.dtsi"
|
||||
#include <zephyr/dt-bindings/lora/sx126x.h>
|
||||
#include <zephyr/dt-bindings/adc/nrf-saadc.h>
|
||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||
|
||||
/ {
|
||||
model = "LilyGo T-Echo";
|
||||
compatible = "lilygo,techo";
|
||||
|
||||
chosen {
|
||||
zephyr,sram = &sram0;
|
||||
zephyr,flash = &flash0;
|
||||
zephyr,code-partition = &code_partition;
|
||||
zephyr,console = &cdc_acm_uart;
|
||||
zephyr,shell-uart = &cdc_acm_uart;
|
||||
zephyr,display = &epd;
|
||||
};
|
||||
|
||||
aliases {
|
||||
lora0 = &lora;
|
||||
/* LEDs: green blinks on LoRa TX (matches Arduino P_LORA_TX_LED=LED_GREEN),
|
||||
* blue acts as activity/heartbeat. Red stays available for errors. */
|
||||
led0 = &led_blue;
|
||||
led1 = &led_green;
|
||||
lora-tx-led = &led_green;
|
||||
sw0 = &user_button;
|
||||
watchdog0 = &wdt0;
|
||||
gps-enable = &gps_enable_pin;
|
||||
};
|
||||
|
||||
/* ---- LEDs (active-LOW per Arduino variant.cpp: LED_STATE_ON=LOW) ---- */
|
||||
leds {
|
||||
compatible = "gpio-leds";
|
||||
|
||||
led_red: led_0 {
|
||||
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
|
||||
label = "Red LED";
|
||||
};
|
||||
|
||||
led_blue: led_1 {
|
||||
gpios = <&gpio0 14 GPIO_ACTIVE_LOW>;
|
||||
label = "Blue LED";
|
||||
};
|
||||
|
||||
led_green: led_2 {
|
||||
gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
|
||||
label = "Green LED (LoRa TX)";
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- Main user button (P1.10) ----
|
||||
*
|
||||
* Button interaction scheme (matches RAK4631's single-button mapping):
|
||||
* short press (release < 1s) → KEY_A → feeds multi-tap filter:
|
||||
* 1 tap → KEY_1 = next page
|
||||
* 2 taps → KEY_LEFT = previous page
|
||||
* long press (≥1s) → KEY_ENTER = activate / confirm */
|
||||
buttons: buttons {
|
||||
compatible = "gpio-keys";
|
||||
|
||||
user_button: button_0 {
|
||||
gpios = <&gpio1 10 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
|
||||
zephyr,code = <INPUT_KEY_0>;
|
||||
label = "User Button";
|
||||
};
|
||||
};
|
||||
|
||||
/* short-press → KEY_A (multi-tap), long-press (≥1s) → KEY_ENTER */
|
||||
user_btn_longpress {
|
||||
compatible = "zephyr,input-longpress";
|
||||
input = <&buttons>;
|
||||
input-codes = <INPUT_KEY_0>;
|
||||
short-codes = <INPUT_KEY_A>;
|
||||
long-codes = <INPUT_KEY_ENTER>;
|
||||
long-delay-ms = <1000>;
|
||||
};
|
||||
|
||||
page_btn_multitap {
|
||||
compatible = "zephcore,input-multi-tap";
|
||||
input-codes = <INPUT_KEY_A>;
|
||||
tap-codes = <INPUT_KEY_1 INPUT_KEY_LEFT>;
|
||||
tap-delay-ms = <400>;
|
||||
};
|
||||
|
||||
/* ---- Backlight touch input (P0.11) ----
|
||||
*
|
||||
* Separate gpio-keys node (not under `buttons`) so this event does
|
||||
* NOT feed the longpress/multitap filter chain above. Emits
|
||||
* INPUT_KEY_BRIGHTNESSUP — ui_task.c treats unknown codes as
|
||||
* display-wake only: a tap lights the front-light for
|
||||
* CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS, just like any other
|
||||
* board's "tap to wake display". */
|
||||
backlight_btn: backlight-btn {
|
||||
compatible = "gpio-keys";
|
||||
|
||||
backlight_button: button_0 {
|
||||
gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
|
||||
zephyr,code = <INPUT_KEY_BRIGHTNESSUP>;
|
||||
label = "Backlight Touch";
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- Power control ----
|
||||
*
|
||||
* Three rails must come up in order:
|
||||
* 1. pwr_enable (P0.12) — board-wide peripheral power
|
||||
* 2. lora_pwr_enable (P1.05) — SX1262 power (shared with GPS reset;
|
||||
* HIGH releases GPS from reset too — active-LOW GPS reset)
|
||||
* 3. tcxo_enable (P0.21) — external TCXO for SX1262
|
||||
*
|
||||
* These are all regulator-fixed with regulator-boot-on so Zephyr's
|
||||
* regulator framework brings them up before the SX1262 driver init. */
|
||||
|
||||
pwr_enable: pwr-enable {
|
||||
compatible = "regulator-fixed";
|
||||
regulator-name = "pwr-enable";
|
||||
enable-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>;
|
||||
regulator-boot-on;
|
||||
};
|
||||
|
||||
lora_pwr_enable: lora-pwr-enable {
|
||||
compatible = "regulator-fixed";
|
||||
regulator-name = "lora-pwr-enable";
|
||||
enable-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>;
|
||||
regulator-boot-on;
|
||||
startup-delay-us = <10000>;
|
||||
};
|
||||
|
||||
tcxo_enable: tcxo-enable {
|
||||
compatible = "regulator-fixed";
|
||||
regulator-name = "tcxo-enable";
|
||||
enable-gpios = <&gpio0 21 GPIO_ACTIVE_HIGH>;
|
||||
regulator-boot-on;
|
||||
};
|
||||
|
||||
/* E-paper front-light enable (P1.11). Boot-on so the display is lit
|
||||
* out of the box; display.c can drive this off when entering auto-off. */
|
||||
disp_pwr_enable: disp-pwr-enable {
|
||||
compatible = "regulator-fixed";
|
||||
regulator-name = "disp-pwr-enable";
|
||||
enable-gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>;
|
||||
};
|
||||
|
||||
/* ---- GPS power control ---- */
|
||||
gps_en: gps-enable {
|
||||
compatible = "gpio-leds";
|
||||
gps_enable_pin: gps_enable {
|
||||
gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;
|
||||
label = "GPS Enable";
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- Battery ADC ----
|
||||
* 150K+150K voltage divider (2:1) on AIN2 (P0.04). Full-scale 7.2V
|
||||
* with ADC_REF_INTERNAL (0.6V) × gain 1/6 × divider 2.0. */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 2>;
|
||||
vbat-mv-multiplier = <7200>;
|
||||
};
|
||||
|
||||
/* ---- E-Paper display (MIPI DBI SPI) ----
|
||||
* SSD1681 1.54" 200x200 monochrome EPD on SPI3. */
|
||||
mipi_dbi {
|
||||
compatible = "zephyr,mipi-dbi-spi";
|
||||
spi-dev = <&spi3>;
|
||||
dc-gpios = <&gpio0 28 GPIO_ACTIVE_HIGH>;
|
||||
reset-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
epd: ssd16xxfb@0 {
|
||||
compatible = "solomon,ssd1681";
|
||||
mipi-max-frequency = <4000000>;
|
||||
reg = <0>;
|
||||
width = <200>;
|
||||
height = <200>;
|
||||
rotation = <180>;
|
||||
busy-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
|
||||
|
||||
/* Use internal temperature sensor for OTP waveform selection. */
|
||||
tssv = <0x80>;
|
||||
|
||||
/* OTP waveforms — no explicit LUT needed for SSD1681.
|
||||
* border-waveform controls the 1-px border during refresh:
|
||||
* full 0x05 = border follows source (clean full reset)
|
||||
* partial 0x80 = HiZ (GxEPD convention; prevents the
|
||||
* 1-px left/bottom edge residue that 0x3C
|
||||
* leaves on this SSD1681 revision). */
|
||||
full { border-waveform = <0x05>; };
|
||||
partial { border-waveform = <0x80>; };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- SoC configuration ---- */
|
||||
|
||||
®0 {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
®1 {
|
||||
regulator-initial-mode = <NRF5X_REG_MODE_DCDC>;
|
||||
};
|
||||
|
||||
&adc {
|
||||
status = "okay";
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
/* Battery voltage on P0.04 (AIN2) */
|
||||
channel@2 {
|
||||
reg = <2>;
|
||||
zephyr,gain = "ADC_GAIN_1_6";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 10)>;
|
||||
zephyr,input-positive = <NRF_SAADC_AIN2>;
|
||||
zephyr,resolution = <12>;
|
||||
};
|
||||
};
|
||||
|
||||
&uicr {
|
||||
nfct-pins-as-gpios;
|
||||
};
|
||||
|
||||
&gpiote {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
&gpio0 {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
&gpio1 {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
/* ---- GPS on UART0 (9600 baud) ----
|
||||
* L76K behaves like Air530Z (PCAS/NMEA). Driver manages power via
|
||||
* on-off-gpios (P1.02 = GPS_EN); no deferred-init needed at 9600 baud. */
|
||||
&uart0 {
|
||||
compatible = "nordic,nrf-uarte";
|
||||
status = "okay";
|
||||
current-speed = <9600>;
|
||||
pinctrl-0 = <&uart0_default>;
|
||||
pinctrl-1 = <&uart0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
|
||||
gnss: gnss {
|
||||
compatible = "luatos,air530z";
|
||||
on-off-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- I2C0 for environment sensors ---- */
|
||||
&i2c0 {
|
||||
compatible = "nordic,nrf-twim";
|
||||
status = "okay";
|
||||
clock-frequency = <I2C_BITRATE_FAST>;
|
||||
pinctrl-0 = <&i2c0_default>;
|
||||
pinctrl-1 = <&i2c0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
|
||||
/* Pull in the full sensor list — runtime auto-detect takes care of
|
||||
* devices that aren't populated. T-Echo factory sensor is BME280 at
|
||||
* the alt address 0x77, added separately below. */
|
||||
#include "../../common/sensors-i2c.dtsi"
|
||||
};
|
||||
|
||||
/* T-Echo BME280 sits at I2C 0x77 (alt address), conflicting with the
|
||||
* BMP388 node from sensors-i2c.dtsi. Remove BMP388 and replace with
|
||||
* BME280 at 0x77 (same bosch,bme280 driver). */
|
||||
/delete-node/ &bmp388;
|
||||
|
||||
&i2c0 {
|
||||
bme280_alt: bme280@77 {
|
||||
compatible = "bosch,bme280";
|
||||
reg = <0x77>;
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- SPI2 for LoRa SX1262 ---- */
|
||||
&spi2 {
|
||||
compatible = "nordic,nrf-spim";
|
||||
status = "okay";
|
||||
pinctrl-0 = <&spi2_default>;
|
||||
pinctrl-1 = <&spi2_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>; /* P0.24 LORA_CS */
|
||||
|
||||
lora: lora@0 {
|
||||
compatible = "semtech,sx1262";
|
||||
reg = <0>;
|
||||
spi-max-frequency = <8000000>;
|
||||
|
||||
/* LoRa control pins */
|
||||
reset-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>; /* P0.25 */
|
||||
busy-gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>; /* P0.17 */
|
||||
dio1-gpios = <&gpio0 20 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* P0.20 */
|
||||
|
||||
/* DIO2 drives RF switch for TX */
|
||||
dio2-tx-enable;
|
||||
|
||||
/* External TCXO on DIO3 — 1.8V reference */
|
||||
dio3-tcxo-voltage = <SX126X_DIO3_TCXO_1V8>;
|
||||
tcxo-power-startup-delay-ms = <10>;
|
||||
|
||||
/* RX boosted mode for better sensitivity (matches
|
||||
* Arduino SX126X_RX_BOOSTED_GAIN=1). */
|
||||
rx-boosted;
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- SPI3 for E-Paper display ---- */
|
||||
&spi3 {
|
||||
compatible = "nordic,nrf-spim";
|
||||
status = "okay";
|
||||
pinctrl-0 = <&spi3_default>;
|
||||
pinctrl-1 = <&spi3_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
cs-gpios = <&gpio0 30 GPIO_ACTIVE_LOW>; /* P0.30 DISP_CS */
|
||||
};
|
||||
|
||||
/* ---- QSPI Flash — MX25R1635F 2MB ---- */
|
||||
&qspi {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&qspi_default>;
|
||||
pinctrl-1 = <&qspi_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
|
||||
qspi_flash: qspi_flash@0 {
|
||||
compatible = "nordic,qspi-nor";
|
||||
reg = <0>;
|
||||
writeoc = "pp";
|
||||
readoc = "fastread";
|
||||
sck-frequency = <16000000>;
|
||||
jedec-id = [c2 28 15]; /* MX25R1635F */
|
||||
size = <16777216>; /* 2MB = 16Mbit */
|
||||
has-dpd;
|
||||
t-enter-dpd = <10000>;
|
||||
t-exit-dpd = <35000>;
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- USB CDC for console/CLI ---- */
|
||||
zephyr_udc0: &usbd {
|
||||
compatible = "nordic,nrf-usbd";
|
||||
status = "okay";
|
||||
|
||||
cdc_acm_uart: cdc_acm_uart {
|
||||
compatible = "zephyr,cdc-acm-uart";
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- Flash partitions (SoftDevice v6, app@0x26000) ---- */
|
||||
#include "../../common/nrf52_partitions_sdv6.dtsi"
|
||||
|
||||
/* ---- External QSPI flash partitions ---- */
|
||||
&qspi_flash {
|
||||
partitions {
|
||||
compatible = "fixed-partitions";
|
||||
#address-cells = <1>;
|
||||
#size-cells = <1>;
|
||||
|
||||
/* Full 2MB for LittleFS storage */
|
||||
qspi_storage_partition: partition@0 {
|
||||
label = "qspi_storage";
|
||||
reg = <0x00000000 0x00200000>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/* ---- External QSPI /ext automount ---- */
|
||||
#include "../../common/qspi-ext.dtsi"
|
||||
#include "../../common/nrf52_wakeup.dtsi"
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# LilyGo T-Echo default configuration
|
||||
|
||||
# Enable MPU
|
||||
CONFIG_ARM_MPU=y
|
||||
CONFIG_HW_STACK_PROTECTION=y
|
||||
|
||||
# Enable GPIO
|
||||
CONFIG_GPIO=y
|
||||
|
||||
# Enable console on USB CDC
|
||||
CONFIG_CONSOLE=y
|
||||
CONFIG_UART_CONSOLE=y
|
||||
CONFIG_SERIAL=y
|
||||
CONFIG_UART_LINE_CTRL=y
|
||||
|
||||
# Enable flash/NVS storage
|
||||
CONFIG_FLASH=y
|
||||
CONFIG_FLASH_MAP=y
|
||||
CONFIG_NVS=y
|
||||
|
||||
# Pinctrl
|
||||
CONFIG_PINCTRL=y
|
||||
|
||||
# SPI for LoRa + EPD display
|
||||
CONFIG_SPI=y
|
||||
|
||||
# ADC for battery
|
||||
CONFIG_ADC=y
|
||||
@@ -25,3 +25,12 @@ CONFIG_HEAP_MEM_POOL_SIZE=6144
|
||||
# Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB).
|
||||
# Only used during J-Link debugging, 1KB is Zephyr's default.
|
||||
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024
|
||||
|
||||
# ========== UI / display ==========
|
||||
# 200x200 SSD1681 — the default 6x8 font is too small to read comfortably.
|
||||
# Switch to the smallest Zephyr built-in CFB font >= 16 px (typically 10x16).
|
||||
CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT=y
|
||||
|
||||
# Single-button board with longpress filter: a "double long-press" is
|
||||
# physically slower than 500 ms, so widen the confirmation window.
|
||||
CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS=3000
|
||||
|
||||
@@ -200,9 +200,11 @@
|
||||
/* OTP waveforms — no explicit LUT needed for SSD1681.
|
||||
* border-waveform controls the 1-px border during refresh:
|
||||
* full 0x05 = border follows source (clean full reset)
|
||||
* partial 0x3c = Hi-Z (border held, no border flash) */
|
||||
* partial 0x80 = HiZ (GxEPD convention; avoids the 1-px
|
||||
* edge residue that 0x3C leaves on this
|
||||
* SSD1681 revision). */
|
||||
full { border-waveform = <0x05>; };
|
||||
partial { border-waveform = <0x3c>; };
|
||||
partial { border-waveform = <0x80>; };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ rak_wismesh_tag
|
||||
ikoka_nano_30dbm
|
||||
sensecap_solar
|
||||
xiao_nrf52840
|
||||
lilygo_techo
|
||||
```
|
||||
|
||||
> **RAK WisMesh Pocket** (WisBlock pocket): use `-b rak4631` — same board string and firmware as **RAK4631**.
|
||||
|
||||
+445
-408
@@ -1,408 +1,445 @@
|
||||
/*
|
||||
* ZephCore - Display Abstraction (CFB)
|
||||
* Copyright (c) 2025 ZephCore
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Wraps Zephyr's Character Framebuffer (CFB) subsystem.
|
||||
* Auto-detects any Zephyr-supported display from devicetree:
|
||||
* 1. "zephyr,display" chosen node (standard — works for any display)
|
||||
* 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat)
|
||||
*
|
||||
* Resolution is queried from the driver at runtime — no hardcoded
|
||||
* dimensions. Layout code should use mc_display_width/height().
|
||||
*
|
||||
* Auto-off timer turns display off after CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS.
|
||||
*/
|
||||
|
||||
#include "display.h"
|
||||
#include "doom_game.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/display/cfb.h>
|
||||
#include <zephyr/drivers/display.h>
|
||||
#include <zephyr/drivers/regulator.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(zephcore_display, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
|
||||
|
||||
/* ========== State ========== */
|
||||
|
||||
static const struct device *disp_dev;
|
||||
static bool disp_on;
|
||||
static bool disp_initialized;
|
||||
|
||||
/* Runtime display geometry (queried from driver) */
|
||||
static uint16_t disp_width;
|
||||
static uint16_t disp_height;
|
||||
static uint8_t font_w;
|
||||
static uint8_t font_h;
|
||||
static bool is_epd; /* true for e-paper displays */
|
||||
|
||||
/* Optional display backlight regulator (e.g. e-paper frontlight).
|
||||
* Boards define a "disp_pwr_enable" regulator-fixed node to gate the
|
||||
* backlight circuit. When present, backlight follows display on/off. */
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(disp_pwr_enable))
|
||||
static const struct device *backlight_reg =
|
||||
DEVICE_DT_GET_OR_NULL(DT_NODELABEL(disp_pwr_enable));
|
||||
#else
|
||||
static const struct device *backlight_reg;
|
||||
#endif
|
||||
|
||||
static bool backlight_on;
|
||||
|
||||
static inline void backlight_set(bool on)
|
||||
{
|
||||
if (backlight_reg && device_is_ready(backlight_reg) && on != backlight_on) {
|
||||
if (on) {
|
||||
regulator_enable(backlight_reg);
|
||||
} else {
|
||||
regulator_disable(backlight_reg);
|
||||
}
|
||||
backlight_on = on;
|
||||
}
|
||||
}
|
||||
|
||||
/* Auto-off work */
|
||||
static struct k_work_delayable auto_off_work;
|
||||
|
||||
static void auto_off_handler(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
/* Don't blank display while Doom easter egg is playing */
|
||||
if (doom_game_is_running()) {
|
||||
return;
|
||||
}
|
||||
/* E-paper content persists without power — blanking wastes a full
|
||||
* refresh cycle (~2s) for no benefit. Just turn off the backlight
|
||||
* and mark display "off" so the next button press triggers
|
||||
* mc_display_on() → backlight restore. */
|
||||
if (is_epd) {
|
||||
backlight_set(false);
|
||||
disp_on = false;
|
||||
return;
|
||||
}
|
||||
if (disp_on) {
|
||||
mc_display_off();
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Early blanking ==========
|
||||
* OLED controllers (SSD1306, SH1106) turn the display ON during driver init,
|
||||
* showing stale VRAM from before reset. Our mc_display_init() runs much later
|
||||
* (after BLE, LoRa, etc.), so there's a visible garbage flash.
|
||||
*
|
||||
* Fix: SYS_INIT hook runs right after the driver, sending "Display OFF" before
|
||||
* main() starts. This is harmless for non-OLED displays (blanking is a no-op
|
||||
* or already blanked). */
|
||||
static int display_early_blank(void)
|
||||
{
|
||||
const struct device *dev = NULL;
|
||||
|
||||
/* Try standard chosen node first */
|
||||
#if DT_HAS_CHOSEN(zephyr_display)
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_display));
|
||||
#endif
|
||||
/* Legacy nodelabel fallback */
|
||||
if (!dev) {
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sh1106));
|
||||
}
|
||||
if (!dev) {
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ssd1306));
|
||||
}
|
||||
if (dev && device_is_ready(dev)) {
|
||||
/* EPD displays are bistable and already show clean white after
|
||||
* driver init's full refresh. Calling blanking_on here would
|
||||
* leave blanking_on=true so the subsequent blanking_off in
|
||||
* mc_display_init() triggers an extra unnecessary full refresh.
|
||||
* Skip blanking for EPD; OLED still needs it to hide stale VRAM. */
|
||||
struct display_capabilities caps;
|
||||
|
||||
display_get_capabilities(dev, &caps);
|
||||
if (!(caps.screen_info & SCREEN_INFO_EPD)) {
|
||||
display_blanking_on(dev);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
SYS_INIT(display_early_blank, APPLICATION, 99);
|
||||
|
||||
/* ========== Public API ========== */
|
||||
|
||||
int mc_display_init(void)
|
||||
{
|
||||
/* Find display device from devicetree.
|
||||
* Priority: zephyr,display chosen > sh1106 nodelabel > ssd1306 nodelabel.
|
||||
* This supports any Zephyr display driver (SSD1306, SH1106, ST7735,
|
||||
* ILI9341, SSD1681 e-ink, etc.) via the standard chosen mechanism. */
|
||||
#if DT_HAS_CHOSEN(zephyr_display)
|
||||
disp_dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_display));
|
||||
#endif
|
||||
if (!disp_dev) {
|
||||
disp_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sh1106));
|
||||
}
|
||||
if (!disp_dev) {
|
||||
disp_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ssd1306));
|
||||
}
|
||||
|
||||
if (!disp_dev || !device_is_ready(disp_dev)) {
|
||||
LOG_INF("no display found - display disabled");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
/* Query actual resolution from display driver */
|
||||
struct display_capabilities caps;
|
||||
|
||||
display_get_capabilities(disp_dev, &caps);
|
||||
disp_width = caps.x_resolution;
|
||||
disp_height = caps.y_resolution;
|
||||
is_epd = (caps.screen_info & SCREEN_INFO_EPD) != 0;
|
||||
|
||||
LOG_INF("display: %ux%u%s", disp_width, disp_height,
|
||||
is_epd ? " (e-paper)" : "");
|
||||
|
||||
/* OLED: blank before CFB init so stale VRAM isn't visible while we
|
||||
* build the first frame. EPD: driver init already performed a clean
|
||||
* full refresh — the panel shows white. Skip blanking to avoid the
|
||||
* extra full refresh that blanking_off would trigger. */
|
||||
if (!is_epd) {
|
||||
display_blanking_on(disp_dev);
|
||||
}
|
||||
|
||||
/* Initialize CFB */
|
||||
int ret = cfb_framebuffer_init(disp_dev);
|
||||
|
||||
if (ret) {
|
||||
LOG_ERR("CFB init failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Select smallest font — scan all registered fonts and pick the one
|
||||
* with the smallest height for best text density. Our custom 6x8
|
||||
* Latin-1 font will typically win. */
|
||||
int num_fonts = cfb_get_numof_fonts(disp_dev);
|
||||
|
||||
LOG_DBG("display: %d fonts available", num_fonts);
|
||||
|
||||
int best_idx = 0;
|
||||
uint8_t best_h = 255;
|
||||
|
||||
for (int i = 0; i < num_fonts; i++) {
|
||||
uint8_t fw = 0, fh = 0;
|
||||
|
||||
cfb_get_font_size(disp_dev, i, &fw, &fh);
|
||||
LOG_DBG(" font[%d]: %ux%u", i, fw, fh);
|
||||
if (fh < best_h) {
|
||||
best_h = fh;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
cfb_framebuffer_set_font(disp_dev, best_idx);
|
||||
cfb_get_font_size(disp_dev, best_idx, &font_w, &font_h);
|
||||
LOG_INF("display: selected font[%d] (%ux%u)", best_idx, font_w, font_h);
|
||||
|
||||
/* CFB inversion no longer needed — Zephyr commit 2374ef62f97 fixed
|
||||
* the MONO10/MONO01 polarity logic in cfb_framebuffer_finalize().
|
||||
* SSD1306 OLED reports MONO01 by default, which CFB now handles
|
||||
* correctly (white pixels on black background) without manual invert. */
|
||||
|
||||
/* Clear CPU-side framebuffer (zeroes the RAM buffer — no SPI transfer). */
|
||||
cfb_framebuffer_clear(disp_dev, false);
|
||||
|
||||
/* Unblank the display so the driver uses partial refresh for all
|
||||
* subsequent renders (ssd16xx: partial_refresh = !blanking_on).
|
||||
* OLED: also push a blank frame first to clear stale VRAM.
|
||||
* EPD: skip the frame push — partial refresh will write real content. */
|
||||
if (!is_epd) {
|
||||
cfb_framebuffer_finalize(disp_dev);
|
||||
}
|
||||
display_blanking_off(disp_dev);
|
||||
backlight_set(true);
|
||||
disp_on = true;
|
||||
disp_initialized = true;
|
||||
|
||||
/* Set up auto-off timer and schedule initial timeout */
|
||||
k_work_init_delayable(&auto_off_work, auto_off_handler);
|
||||
mc_display_reset_auto_off();
|
||||
|
||||
LOG_INF("display initialized (%ux%u, font %ux%u)",
|
||||
disp_width, disp_height, font_w, font_h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t mc_display_width(void)
|
||||
{
|
||||
return disp_width;
|
||||
}
|
||||
|
||||
uint16_t mc_display_height(void)
|
||||
{
|
||||
return disp_height;
|
||||
}
|
||||
|
||||
uint8_t mc_display_font_width(void)
|
||||
{
|
||||
return font_w;
|
||||
}
|
||||
|
||||
uint8_t mc_display_font_height(void)
|
||||
{
|
||||
return font_h;
|
||||
}
|
||||
|
||||
void mc_display_on(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!disp_on) {
|
||||
/* EPD: content persists (bistable) — no need to unblank,
|
||||
* just restore backlight. OLED: actually unblank. */
|
||||
if (!is_epd) {
|
||||
display_blanking_off(disp_dev);
|
||||
}
|
||||
disp_on = true;
|
||||
}
|
||||
backlight_set(true);
|
||||
|
||||
mc_display_reset_auto_off();
|
||||
}
|
||||
|
||||
void mc_display_off(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disp_on) {
|
||||
display_blanking_on(disp_dev);
|
||||
backlight_set(false);
|
||||
disp_on = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool mc_display_is_on(void)
|
||||
{
|
||||
return disp_on;
|
||||
}
|
||||
|
||||
bool mc_display_is_epd(void)
|
||||
{
|
||||
return is_epd;
|
||||
}
|
||||
|
||||
void mc_display_clear(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
cfb_framebuffer_clear(disp_dev, false);
|
||||
}
|
||||
|
||||
void mc_display_text(int x, int y, const char *text, bool invert)
|
||||
{
|
||||
if (!disp_initialized || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (invert) {
|
||||
cfb_framebuffer_invert(disp_dev);
|
||||
}
|
||||
|
||||
cfb_print(disp_dev, text, x, y);
|
||||
|
||||
if (invert) {
|
||||
cfb_framebuffer_invert(disp_dev);
|
||||
}
|
||||
}
|
||||
|
||||
void mc_display_fill_rect(int x, int y, int w, int h)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* CFB doesn't have a native fill_rect, so we draw line by line */
|
||||
for (int row = y; row < y + h && row < disp_height; row++) {
|
||||
struct cfb_position start = { .x = x, .y = row };
|
||||
struct cfb_position end = { .x = x + w - 1, .y = row };
|
||||
cfb_draw_line(disp_dev, &start, &end);
|
||||
}
|
||||
}
|
||||
|
||||
void mc_display_hline(int x, int y, int w)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct cfb_position start = { .x = x, .y = y };
|
||||
struct cfb_position end = { .x = x + w - 1, .y = y };
|
||||
cfb_draw_line(disp_dev, &start, &end);
|
||||
}
|
||||
|
||||
void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h)
|
||||
{
|
||||
if (!disp_initialized || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Adafruit drawBitmap format (MSB first): row-major, bit 7 = leftmost.
|
||||
* This matches the Arduino MeshCore logo data from icons.h.
|
||||
* Each row is padded to byte boundary: bytes_per_row = (w+7)/8 */
|
||||
int bytes_per_row = (w + 7) / 8;
|
||||
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
int byte_idx = row * bytes_per_row + col / 8;
|
||||
int bit_idx = 7 - (col % 8); /* MSB first */
|
||||
|
||||
if (data[byte_idx] & (1 << bit_idx)) {
|
||||
struct cfb_position pos = {
|
||||
.x = (int16_t)(x + col),
|
||||
.y = (int16_t)(y + row)
|
||||
};
|
||||
cfb_draw_point(disp_dev, &pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mc_display_finalize(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Don't let CFB overwrite display while Doom is rendering directly */
|
||||
if (doom_game_is_running()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cfb_framebuffer_finalize(disp_dev);
|
||||
}
|
||||
|
||||
void mc_display_reset_auto_off(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS
|
||||
uint32_t timeout = CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS;
|
||||
|
||||
if (timeout > 0) {
|
||||
k_work_reschedule(&auto_off_work, K_MSEC(timeout));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
const struct device *mc_display_get_device(void)
|
||||
{
|
||||
return disp_initialized ? disp_dev : NULL;
|
||||
}
|
||||
/*
|
||||
* ZephCore - Display Abstraction (CFB)
|
||||
* Copyright (c) 2025 ZephCore
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Wraps Zephyr's Character Framebuffer (CFB) subsystem.
|
||||
* Auto-detects any Zephyr-supported display from devicetree:
|
||||
* 1. "zephyr,display" chosen node (standard — works for any display)
|
||||
* 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat)
|
||||
*
|
||||
* Resolution is queried from the driver at runtime — no hardcoded
|
||||
* dimensions. Layout code should use mc_display_width/height().
|
||||
*
|
||||
* Auto-off timer turns display off after CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS.
|
||||
*/
|
||||
|
||||
#include "display.h"
|
||||
#include "doom_game.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/display/cfb.h>
|
||||
#include <zephyr/drivers/display.h>
|
||||
#include <zephyr/drivers/regulator.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(zephcore_display, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
|
||||
|
||||
/* ========== State ========== */
|
||||
|
||||
static const struct device *disp_dev;
|
||||
static bool disp_on;
|
||||
static bool disp_initialized;
|
||||
|
||||
/* Runtime display geometry (queried from driver) */
|
||||
static uint16_t disp_width;
|
||||
static uint16_t disp_height;
|
||||
static uint8_t font_w;
|
||||
static uint8_t font_h;
|
||||
static bool is_epd; /* true for e-paper displays */
|
||||
|
||||
/* Optional symmetric inset (pixels). Shrinks reported width/height and
|
||||
* offsets all draw primitives so panels with edge artefacts can hide them
|
||||
* behind a clean background margin. */
|
||||
#define DISP_INSET ((int)CONFIG_ZEPHCORE_DISPLAY_INSET)
|
||||
|
||||
/* Optional display backlight regulator (e.g. e-paper frontlight).
|
||||
* Boards define a "disp_pwr_enable" regulator-fixed node to gate the
|
||||
* backlight circuit. When present, backlight follows display on/off. */
|
||||
#if DT_NODE_EXISTS(DT_NODELABEL(disp_pwr_enable))
|
||||
static const struct device *backlight_reg =
|
||||
DEVICE_DT_GET_OR_NULL(DT_NODELABEL(disp_pwr_enable));
|
||||
#else
|
||||
static const struct device *backlight_reg;
|
||||
#endif
|
||||
|
||||
static bool backlight_on;
|
||||
|
||||
static inline void backlight_set(bool on)
|
||||
{
|
||||
if (backlight_reg && device_is_ready(backlight_reg) && on != backlight_on) {
|
||||
if (on) {
|
||||
regulator_enable(backlight_reg);
|
||||
} else {
|
||||
regulator_disable(backlight_reg);
|
||||
}
|
||||
backlight_on = on;
|
||||
}
|
||||
}
|
||||
|
||||
/* Auto-off work */
|
||||
static struct k_work_delayable auto_off_work;
|
||||
|
||||
static void auto_off_handler(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
/* Don't blank display while Doom easter egg is playing */
|
||||
if (doom_game_is_running()) {
|
||||
return;
|
||||
}
|
||||
/* E-paper content persists without power — blanking wastes a full
|
||||
* refresh cycle (~2s) for no benefit. Just turn off the backlight
|
||||
* and mark display "off" so the next button press triggers
|
||||
* mc_display_on() → backlight restore. */
|
||||
if (is_epd) {
|
||||
backlight_set(false);
|
||||
disp_on = false;
|
||||
return;
|
||||
}
|
||||
if (disp_on) {
|
||||
mc_display_off();
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Early blanking ==========
|
||||
* OLED controllers (SSD1306, SH1106) turn the display ON during driver init,
|
||||
* showing stale VRAM from before reset. Our mc_display_init() runs much later
|
||||
* (after BLE, LoRa, etc.), so there's a visible garbage flash.
|
||||
*
|
||||
* Fix: SYS_INIT hook runs right after the driver, sending "Display OFF" before
|
||||
* main() starts. This is harmless for non-OLED displays (blanking is a no-op
|
||||
* or already blanked). */
|
||||
static int display_early_blank(void)
|
||||
{
|
||||
const struct device *dev = NULL;
|
||||
|
||||
/* Try standard chosen node first */
|
||||
#if DT_HAS_CHOSEN(zephyr_display)
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_display));
|
||||
#endif
|
||||
/* Legacy nodelabel fallback */
|
||||
if (!dev) {
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sh1106));
|
||||
}
|
||||
if (!dev) {
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ssd1306));
|
||||
}
|
||||
if (dev && device_is_ready(dev)) {
|
||||
/* EPD displays are bistable and already show clean white after
|
||||
* driver init's full refresh. Calling blanking_on here would
|
||||
* leave blanking_on=true so the subsequent blanking_off in
|
||||
* mc_display_init() triggers an extra unnecessary full refresh.
|
||||
* Skip blanking for EPD; OLED still needs it to hide stale VRAM. */
|
||||
struct display_capabilities caps;
|
||||
|
||||
display_get_capabilities(dev, &caps);
|
||||
if (!(caps.screen_info & SCREEN_INFO_EPD)) {
|
||||
display_blanking_on(dev);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
SYS_INIT(display_early_blank, APPLICATION, 99);
|
||||
|
||||
/* ========== Public API ========== */
|
||||
|
||||
int mc_display_init(void)
|
||||
{
|
||||
/* Find display device from devicetree.
|
||||
* Priority: zephyr,display chosen > sh1106 nodelabel > ssd1306 nodelabel.
|
||||
* This supports any Zephyr display driver (SSD1306, SH1106, ST7735,
|
||||
* ILI9341, SSD1681 e-ink, etc.) via the standard chosen mechanism. */
|
||||
#if DT_HAS_CHOSEN(zephyr_display)
|
||||
disp_dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_display));
|
||||
#endif
|
||||
if (!disp_dev) {
|
||||
disp_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(sh1106));
|
||||
}
|
||||
if (!disp_dev) {
|
||||
disp_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ssd1306));
|
||||
}
|
||||
|
||||
if (!disp_dev || !device_is_ready(disp_dev)) {
|
||||
LOG_INF("no display found - display disabled");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
/* Query actual resolution from display driver */
|
||||
struct display_capabilities caps;
|
||||
|
||||
display_get_capabilities(disp_dev, &caps);
|
||||
disp_width = caps.x_resolution;
|
||||
disp_height = caps.y_resolution;
|
||||
is_epd = (caps.screen_info & SCREEN_INFO_EPD) != 0;
|
||||
|
||||
LOG_INF("display: %ux%u%s", disp_width, disp_height,
|
||||
is_epd ? " (e-paper)" : "");
|
||||
|
||||
/* OLED: blank before CFB init so stale VRAM isn't visible while we
|
||||
* build the first frame. EPD: driver init already performed a clean
|
||||
* full refresh — the panel shows white. Skip blanking to avoid the
|
||||
* extra full refresh that blanking_off would trigger. */
|
||||
if (!is_epd) {
|
||||
display_blanking_on(disp_dev);
|
||||
}
|
||||
|
||||
/* Initialize CFB */
|
||||
int ret = cfb_framebuffer_init(disp_dev);
|
||||
|
||||
if (ret) {
|
||||
LOG_ERR("CFB init failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Font selection.
|
||||
* Default: smallest height for best text density — our custom 6x8
|
||||
* Latin-1 font typically wins on OLEDs.
|
||||
* LARGE_FONT: smallest font whose height is >= 16 — picks Zephyr's
|
||||
* built-in 10x16 (cfb_fonts.c) for larger e-paper panels where 6x8
|
||||
* is too small to read. Falls back to smallest-overall if no tall
|
||||
* font is compiled in. */
|
||||
const bool want_large = IS_ENABLED(CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT);
|
||||
int num_fonts = cfb_get_numof_fonts(disp_dev);
|
||||
|
||||
LOG_DBG("display: %d fonts available", num_fonts);
|
||||
|
||||
int best_idx = -1;
|
||||
uint8_t best_h = 255;
|
||||
|
||||
for (int i = 0; i < num_fonts; i++) {
|
||||
uint8_t fw = 0, fh = 0;
|
||||
|
||||
cfb_get_font_size(disp_dev, i, &fw, &fh);
|
||||
LOG_DBG(" font[%d]: %ux%u", i, fw, fh);
|
||||
if (want_large) {
|
||||
if (fh >= 16 && fh < best_h) {
|
||||
best_h = fh;
|
||||
best_idx = i;
|
||||
}
|
||||
} else {
|
||||
if (fh < best_h) {
|
||||
best_h = fh;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best_idx < 0) {
|
||||
/* No font satisfied the LARGE_FONT threshold — fall back to
|
||||
* the smallest so we still render something. */
|
||||
best_idx = 0;
|
||||
for (int i = 0; i < num_fonts; i++) {
|
||||
uint8_t fw = 0, fh = 0;
|
||||
|
||||
cfb_get_font_size(disp_dev, i, &fw, &fh);
|
||||
if (fh < best_h) {
|
||||
best_h = fh;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfb_framebuffer_set_font(disp_dev, best_idx);
|
||||
cfb_get_font_size(disp_dev, best_idx, &font_w, &font_h);
|
||||
LOG_INF("display: selected font[%d] (%ux%u)", best_idx, font_w, font_h);
|
||||
|
||||
/* CFB inversion no longer needed — Zephyr commit 2374ef62f97 fixed
|
||||
* the MONO10/MONO01 polarity logic in cfb_framebuffer_finalize().
|
||||
* SSD1306 OLED reports MONO01 by default, which CFB now handles
|
||||
* correctly (white pixels on black background) without manual invert. */
|
||||
|
||||
/* Clear CPU-side framebuffer (zeroes the RAM buffer — no SPI transfer). */
|
||||
cfb_framebuffer_clear(disp_dev, false);
|
||||
|
||||
/* Unblank the display so the driver uses partial refresh for all
|
||||
* subsequent renders (ssd16xx: partial_refresh = !blanking_on).
|
||||
* OLED: also push a blank frame first to clear stale VRAM.
|
||||
* EPD: skip the frame push — partial refresh will write real content. */
|
||||
if (!is_epd) {
|
||||
cfb_framebuffer_finalize(disp_dev);
|
||||
}
|
||||
display_blanking_off(disp_dev);
|
||||
backlight_set(true);
|
||||
disp_on = true;
|
||||
disp_initialized = true;
|
||||
|
||||
/* Set up auto-off timer and schedule initial timeout */
|
||||
k_work_init_delayable(&auto_off_work, auto_off_handler);
|
||||
mc_display_reset_auto_off();
|
||||
|
||||
LOG_INF("display initialized (%ux%u, font %ux%u)",
|
||||
disp_width, disp_height, font_w, font_h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t mc_display_width(void)
|
||||
{
|
||||
int w = (int)disp_width - 2 * DISP_INSET;
|
||||
|
||||
return (w > 0) ? (uint16_t)w : 0;
|
||||
}
|
||||
|
||||
uint16_t mc_display_height(void)
|
||||
{
|
||||
int h = (int)disp_height - 2 * DISP_INSET;
|
||||
|
||||
return (h > 0) ? (uint16_t)h : 0;
|
||||
}
|
||||
|
||||
uint8_t mc_display_font_width(void)
|
||||
{
|
||||
return font_w;
|
||||
}
|
||||
|
||||
uint8_t mc_display_font_height(void)
|
||||
{
|
||||
return font_h;
|
||||
}
|
||||
|
||||
void mc_display_on(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!disp_on) {
|
||||
/* EPD: content persists (bistable) — no need to unblank,
|
||||
* just restore backlight. OLED: actually unblank. */
|
||||
if (!is_epd) {
|
||||
display_blanking_off(disp_dev);
|
||||
}
|
||||
disp_on = true;
|
||||
}
|
||||
backlight_set(true);
|
||||
|
||||
mc_display_reset_auto_off();
|
||||
}
|
||||
|
||||
void mc_display_off(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disp_on) {
|
||||
display_blanking_on(disp_dev);
|
||||
backlight_set(false);
|
||||
disp_on = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool mc_display_is_on(void)
|
||||
{
|
||||
return disp_on;
|
||||
}
|
||||
|
||||
bool mc_display_is_epd(void)
|
||||
{
|
||||
return is_epd;
|
||||
}
|
||||
|
||||
void mc_display_clear(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
cfb_framebuffer_clear(disp_dev, false);
|
||||
}
|
||||
|
||||
void mc_display_text(int x, int y, const char *text, bool invert)
|
||||
{
|
||||
if (!disp_initialized || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (invert) {
|
||||
cfb_framebuffer_invert(disp_dev);
|
||||
}
|
||||
|
||||
cfb_print(disp_dev, text, x + DISP_INSET, y + DISP_INSET);
|
||||
|
||||
if (invert) {
|
||||
cfb_framebuffer_invert(disp_dev);
|
||||
}
|
||||
}
|
||||
|
||||
void mc_display_fill_rect(int x, int y, int w, int h)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* CFB doesn't have a native fill_rect, so we draw line by line */
|
||||
const int row_clamp = (int)disp_height - DISP_INSET;
|
||||
|
||||
for (int row = y + DISP_INSET; row < y + h + DISP_INSET && row < row_clamp; row++) {
|
||||
struct cfb_position start = { .x = x + DISP_INSET, .y = row };
|
||||
struct cfb_position end = { .x = x + w - 1 + DISP_INSET, .y = row };
|
||||
cfb_draw_line(disp_dev, &start, &end);
|
||||
}
|
||||
}
|
||||
|
||||
void mc_display_hline(int x, int y, int w)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct cfb_position start = { .x = x + DISP_INSET, .y = y + DISP_INSET };
|
||||
struct cfb_position end = { .x = x + w - 1 + DISP_INSET, .y = y + DISP_INSET };
|
||||
cfb_draw_line(disp_dev, &start, &end);
|
||||
}
|
||||
|
||||
void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h)
|
||||
{
|
||||
if (!disp_initialized || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Adafruit drawBitmap format (MSB first): row-major, bit 7 = leftmost.
|
||||
* This matches the Arduino MeshCore logo data from icons.h.
|
||||
* Each row is padded to byte boundary: bytes_per_row = (w+7)/8 */
|
||||
int bytes_per_row = (w + 7) / 8;
|
||||
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
int byte_idx = row * bytes_per_row + col / 8;
|
||||
int bit_idx = 7 - (col % 8); /* MSB first */
|
||||
|
||||
if (data[byte_idx] & (1 << bit_idx)) {
|
||||
struct cfb_position pos = {
|
||||
.x = (int16_t)(x + col + DISP_INSET),
|
||||
.y = (int16_t)(y + row + DISP_INSET)
|
||||
};
|
||||
cfb_draw_point(disp_dev, &pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mc_display_finalize(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Don't let CFB overwrite display while Doom is rendering directly */
|
||||
if (doom_game_is_running()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cfb_framebuffer_finalize(disp_dev);
|
||||
}
|
||||
|
||||
void mc_display_reset_auto_off(void)
|
||||
{
|
||||
if (!disp_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS
|
||||
uint32_t timeout = CONFIG_ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS;
|
||||
|
||||
if (timeout > 0) {
|
||||
k_work_reschedule(&auto_off_work, K_MSEC(timeout));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
const struct device *mc_display_get_device(void)
|
||||
{
|
||||
return disp_initialized ? disp_dev : NULL;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,23 @@ void utf8_to_latin1(char *dst, const char *src, size_t dst_size)
|
||||
#define CONTENT_Y (SEP_Y + 4) /* below separator */
|
||||
#define MAX_CHARS (DISP_W / (FONT_W ? FONT_W : 1))
|
||||
|
||||
/* Vertically center `total` rows of text within the content area and
|
||||
* return the y pixel position for row `idx` (0-based). This replaces
|
||||
* the old hand-picked offsets (CONTENT_Y+16, +28, +34, ...) that were
|
||||
* calibrated for the 6x8 font and broke when switching to 10x16.
|
||||
* Works for any font size / display resolution. */
|
||||
static inline int centered_row(int idx, int total)
|
||||
{
|
||||
int content_h = (int)DISP_H - CONTENT_Y;
|
||||
int used_h = total * LINE_H;
|
||||
int top = (content_h - used_h) / 2;
|
||||
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
return CONTENT_Y + top + idx * LINE_H;
|
||||
}
|
||||
|
||||
/* ========== Global State ========== */
|
||||
static struct ui_state state;
|
||||
|
||||
@@ -284,29 +301,27 @@ static void draw_centered(int y, const char *text)
|
||||
|
||||
static void render_messages(void)
|
||||
{
|
||||
/* Large centered message count (matching Arduino "MSG: X") */
|
||||
/* 3 centered rows: msg count, BLE status, offgrid status */
|
||||
char buf[24];
|
||||
|
||||
snprintf(buf, sizeof(buf), "MSG: %u", state.msg_count);
|
||||
draw_centered(CONTENT_Y + 2, buf);
|
||||
draw_centered(centered_row(0, 3), buf);
|
||||
|
||||
/* Connection status centered below */
|
||||
if (state.ble_connected) {
|
||||
draw_centered(CONTENT_Y + 16, "< Connected >");
|
||||
draw_centered(centered_row(1, 3), "< Connected >");
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 16, "Waiting for app...");
|
||||
draw_centered(centered_row(1, 3), "Waiting for app...");
|
||||
}
|
||||
|
||||
/* Offgrid mode status */
|
||||
snprintf(buf, sizeof(buf), "Offgrid: %s",
|
||||
state.offgrid_enabled ? "ON" : "OFF");
|
||||
draw_centered(CONTENT_Y + 32, buf);
|
||||
draw_centered(centered_row(2, 3), buf);
|
||||
}
|
||||
|
||||
static void render_recent(void)
|
||||
{
|
||||
if (state.recent_count == 0) {
|
||||
draw_centered(CONTENT_Y + 10, "(no contacts heard)");
|
||||
draw_centered(centered_row(0, 1), "(no contacts heard)");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -384,17 +399,17 @@ static void render_radio(void)
|
||||
static void render_bluetooth(void)
|
||||
{
|
||||
if (!state.ble_enabled) {
|
||||
draw_centered(CONTENT_Y + 10, "BLE: OFF");
|
||||
draw_centered(CONTENT_Y + 28, "Press to Enable");
|
||||
draw_centered(centered_row(0, 2), "BLE: OFF");
|
||||
draw_centered(centered_row(1, 2), "Press to Enable");
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.ble_connected) {
|
||||
draw_centered(CONTENT_Y + 4, "BLE: Connected");
|
||||
draw_centered(CONTENT_Y + 20, "Press to Disable");
|
||||
draw_centered(centered_row(0, 2), "BLE: Connected");
|
||||
draw_centered(centered_row(1, 2), "Press to Disable");
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 4, "BLE: Advertising");
|
||||
draw_centered(CONTENT_Y + 20, "Press to Disable");
|
||||
draw_centered(centered_row(0, 2), "BLE: Advertising");
|
||||
draw_centered(centered_row(1, 2), "Press to Disable");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,19 +420,19 @@ static void render_advert(void)
|
||||
bool just_sent = (state.advert_sent_time > 0 &&
|
||||
(now - state.advert_sent_time) < 2000);
|
||||
|
||||
draw_centered(CONTENT_Y, "Send Zero-Hop Advert");
|
||||
draw_centered(centered_row(0, 3), "Send Zero-Hop Advert");
|
||||
|
||||
if (just_sent) {
|
||||
if (state.advert_was_flood) {
|
||||
draw_centered(CONTENT_Y + 16, ">> Flood Sent! <<");
|
||||
draw_centered(centered_row(1, 3), ">> Flood Sent! <<");
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 16, ">>> Sent! <<<");
|
||||
draw_centered(centered_row(1, 3), ">>> Sent! <<<");
|
||||
}
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 16, "Press to Send");
|
||||
draw_centered(centered_row(1, 3), "Press to Send");
|
||||
}
|
||||
|
||||
draw_centered(CONTENT_Y + 34, "(Double Press: Flood)");
|
||||
draw_centered(centered_row(2, 3), "(2x Press: Flood)");
|
||||
}
|
||||
|
||||
/* Format seconds into compact time string: "3m20s", "1h05m", "12s" */
|
||||
@@ -574,24 +589,24 @@ static void render_sensors(void)
|
||||
|
||||
static void render_offgrid(void)
|
||||
{
|
||||
/* Check if confirmation has expired (500ms) */
|
||||
/* Check if confirmation has expired */
|
||||
if (state.offgrid_confirm_time != 0 &&
|
||||
(k_uptime_get_32() - state.offgrid_confirm_time) > 500) {
|
||||
(k_uptime_get_32() - state.offgrid_confirm_time) > CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS) {
|
||||
state.offgrid_confirm_time = 0;
|
||||
}
|
||||
|
||||
draw_centered(CONTENT_Y + 2, "Offgrid Mode");
|
||||
draw_centered(centered_row(0, 3), "Offgrid Mode");
|
||||
|
||||
char buf[24];
|
||||
|
||||
snprintf(buf, sizeof(buf), "Status: %s",
|
||||
state.offgrid_enabled ? "ON" : "OFF");
|
||||
draw_centered(CONTENT_Y + 16, buf);
|
||||
draw_centered(centered_row(1, 3), buf);
|
||||
|
||||
if (state.offgrid_confirm_time != 0) {
|
||||
draw_centered(CONTENT_Y + 34, "Press to confirm");
|
||||
draw_centered(centered_row(2, 3), "Press to confirm");
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 34,
|
||||
draw_centered(centered_row(2, 3),
|
||||
state.offgrid_enabled ? "Press to Disable"
|
||||
: "Press to Enable");
|
||||
}
|
||||
@@ -599,33 +614,33 @@ static void render_offgrid(void)
|
||||
|
||||
static void render_dfu(void)
|
||||
{
|
||||
/* Check if confirmation has expired (500ms) */
|
||||
/* Check if confirmation has expired */
|
||||
if (state.dfu_confirm_time != 0 &&
|
||||
(k_uptime_get_32() - state.dfu_confirm_time) > 500) {
|
||||
(k_uptime_get_32() - state.dfu_confirm_time) > CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS) {
|
||||
state.dfu_confirm_time = 0;
|
||||
}
|
||||
|
||||
draw_centered(CONTENT_Y + 10, "BLE DFU Update");
|
||||
draw_centered(centered_row(0, 2), "BLE DFU Update");
|
||||
if (state.dfu_confirm_time != 0) {
|
||||
draw_centered(CONTENT_Y + 28, "Press to confirm");
|
||||
draw_centered(centered_row(1, 2), "Press to confirm");
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 28, "Press to Run");
|
||||
draw_centered(centered_row(1, 2), "Press to Run");
|
||||
}
|
||||
}
|
||||
|
||||
static void render_shutdown(void)
|
||||
{
|
||||
/* Check if confirmation has expired (500ms) */
|
||||
/* Check if confirmation has expired */
|
||||
if (state.shutdown_confirm_time != 0 &&
|
||||
(k_uptime_get_32() - state.shutdown_confirm_time) > 500) {
|
||||
(k_uptime_get_32() - state.shutdown_confirm_time) > CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS) {
|
||||
state.shutdown_confirm_time = 0;
|
||||
}
|
||||
|
||||
draw_centered(CONTENT_Y + 10, "Power Off");
|
||||
draw_centered(centered_row(0, 2), "Power Off");
|
||||
if (state.shutdown_confirm_time != 0) {
|
||||
draw_centered(CONTENT_Y + 28, "Press to confirm");
|
||||
draw_centered(centered_row(1, 2), "Press to confirm");
|
||||
} else {
|
||||
draw_centered(CONTENT_Y + 28, "Press to Run");
|
||||
draw_centered(centered_row(1, 2), "Press to Run");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -353,12 +353,12 @@ static void action_page_enter(void)
|
||||
break;
|
||||
|
||||
case UI_PAGE_OFFGRID: {
|
||||
/* Double-press confirmation (500ms window) */
|
||||
/* Double-press confirmation (CONFIRM_WINDOW_MS window) */
|
||||
struct ui_state *st_og = get_state();
|
||||
uint32_t now_og = k_uptime_get_32();
|
||||
|
||||
if (st_og->offgrid_confirm_time != 0 &&
|
||||
(now_og - st_og->offgrid_confirm_time) <= 500) {
|
||||
(now_og - st_og->offgrid_confirm_time) <= CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS) {
|
||||
/* Confirmed — toggle offgrid mode */
|
||||
bool new_state = !st_og->offgrid_enabled;
|
||||
st_og->offgrid_enabled = new_state;
|
||||
@@ -374,12 +374,12 @@ static void action_page_enter(void)
|
||||
}
|
||||
|
||||
case UI_PAGE_DFU: {
|
||||
/* Double-press confirmation (500ms window) */
|
||||
/* Double-press confirmation (CONFIRM_WINDOW_MS window) */
|
||||
struct ui_state *st_dfu = get_state();
|
||||
uint32_t now_dfu = k_uptime_get_32();
|
||||
|
||||
if (st_dfu->dfu_confirm_time != 0 &&
|
||||
(now_dfu - st_dfu->dfu_confirm_time) <= 500) {
|
||||
(now_dfu - st_dfu->dfu_confirm_time) <= CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS) {
|
||||
/* Confirmed — reboot into BLE DFU */
|
||||
action_enter_dfu();
|
||||
} else {
|
||||
@@ -391,12 +391,12 @@ static void action_page_enter(void)
|
||||
}
|
||||
|
||||
case UI_PAGE_SHUTDOWN: {
|
||||
/* Double-press confirmation (500ms window) */
|
||||
/* Double-press confirmation (CONFIRM_WINDOW_MS window) */
|
||||
struct ui_state *st = get_state();
|
||||
uint32_t now = k_uptime_get_32();
|
||||
|
||||
if (st->shutdown_confirm_time != 0 &&
|
||||
(now - st->shutdown_confirm_time) <= 500) {
|
||||
(now - st->shutdown_confirm_time) <= CONFIG_ZEPHCORE_UI_CONFIRM_WINDOW_MS) {
|
||||
/* Confirmed — shut down */
|
||||
action_deep_sleep();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user