add t-echo board

fix e-ink displays
fix one-button confirmation bug (eg shutdown)
This commit is contained in:
liquidraver
2026-04-24 10:10:58 +02:00
parent 235890407e
commit 1dde69c653
19 changed files with 1198 additions and 452 deletions
+1
View File
@@ -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
+1
View File
@@ -17,6 +17,7 @@ nRF_boards=(
ikoka_nano_30dbm
sensecap_solar
xiao_nrf52840
lilygo_techo
)
ESP32_boards=(
+1 -1
View File
@@ -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")
+38
View File
@@ -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
+1
View File
@@ -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 ---- */
&reg0 {
status = "okay";
};
&reg1 {
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>; };
};
};
};
+1
View File
@@ -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
View File
@@ -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;
}
+50 -35
View File
@@ -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");
}
}
+6 -6
View File
@@ -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 {