This commit is contained in:
Your Name
2026-05-01 13:19:19 +02:00
parent a45abbdd5d
commit 499745a027
27 changed files with 0 additions and 2185 deletions
-14
View File
@@ -1,14 +0,0 @@
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrfwarch)
target_sources(app PRIVATE
src/main.c
src/epd_drv.c
src/watchface.c
src/fonts.c
)
target_include_directories(app PRIVATE src)
-178
View File
@@ -1,178 +0,0 @@
# nrfwarch
Smartwatch firmware for the [Seeed Xiao nRF54L15 Sense](https://www.seeedstudio.com/XIAO-nRF54L15-Sense-p-6494.html) with a GDEY0154D61LT 1.54" e-ink display.
Built on [Zephyr RTOS](https://docs.zephyrproject.org/) v4.0. Targets the `xiao_nrf54l15/nrf54l15/cpuapp` board. Includes a display simulator that renders the watchface on your desktop using [feh](https://feh.finalrewind.org/).
## Hardware
- **MCU**: Nordic nRF54L15 (ARM Cortex-M33, 128KB RAM, 1MB Flash)
- **Board**: Seeed Xiao nRF54L15 Sense (includes LSM6DSO IMU, PDM mic)
- **Display**: Good Display GDEY0154D61LT 1.54" e-ink, 200x200px, SSD1680 controller, SPI
### E-ink wiring
| Signal | Xiao Pin | Function |
|--------|----------|----------|
| MOSI | D10 | SPI data out |
| CLK | D8 | SPI clock |
| CS | D1 | Chip select |
| DC | D3 | Data/command |
| RST | D0 | Hardware reset |
| BUSY | D5 | Busy status |
## Quick start
### 1. Enter dev shell
```bash
nix develop
```
### 2. Set up Zephyr SDK (first time only)
Downloads Zephyr v4.0 workspace and SDK 0.17.0, patches binaries for NixOS:
```bash
nix run .#setup-sdk
```
### 3. Run the display simulator
Builds and runs the watchface simulator. Time advances 1 minute per second. A feh window opens showing the e-ink output in real time. The simulator uses the same rendering code that runs on the real hardware.
```bash
nix run .#sim
```
This pipes the native_sim binary output through `sim/view.py`, which converts the framebuffer to PBM and auto-launches feh with `--auto-reload`. The output file is `/tmp/nrfwarch.pbm`.
### 4. Run tests
17 ztest cases covering framebuffer operations, font data, and rendering:
```bash
nix run .#test
```
### 5. Build for hardware
Open the project in [nRF Connect for VS Code](https://marketplace.visualstudio.com/items?itemName=nordic-semiconductor.nrf-connect) and add the application with board target `xiao_nrf54l15/nrf54l15/cpuapp`. Or build from the command line:
```bash
west build -b xiao_nrf54l15/nrf54l15/cpuapp .
```
## Display simulation
The simulator lets you develop the watchface visually without any hardware. It runs the actual Zephyr kernel and the real `watchface.c` / `fonts.c` code -- the only difference is the EPD driver writes to a file instead of SPI.
How it works:
1. `sim/src/epd_sim.c` implements the same API as `src/epd_drv.c`, but instead of sending SPI commands it dumps the raw framebuffer over stdout as hex, framed with `!F!` / `!E!` markers
2. `sim/view.py` reads the hex frames from stdin, inverts the polarity (framebuffer uses 1=white, PBM uses 1=black), and writes a PBM image to `/tmp/nrfwarch.pbm`
3. feh watches the file with `--auto-reload` and updates the display window
The simulated time starts at 12:00 and advances 1 minute every 0.8 seconds. Battery decreases by 2% per simulated hour. A full 24-hour cycle runs in about 19 minutes.
### Manual usage
If you want to run the sim without feh or pipe the output yourself:
```bash
# Run the binary directly (outputs hex frames to stdout)
./build-sim/zephyr/zephyr.exe
# Convert a single frame to PBM manually
./build-sim/zephyr/zephyr.exe | python3 sim/view.py
# Just view the current frame
feh /tmp/nrfwarch.pbm
# Convert to PNG for sharing
pnmtopng /tmp/nrfwarch.pbm > watchface.png
```
### Iterating on the watchface
1. Edit `src/watchface.c` or modify digit shapes in `tools/gen_fonts.py`
2. If fonts changed: `python3 tools/gen_fonts.py`
3. Rebuild sim: `cmake -B build-sim -GNinja -DBOARD=qemu_x86 -S sim && ninja -C build-sim`
4. Run: `./build-sim/zephyr/zephyr.exe | python3 sim/view.py`
5. feh auto-reloads when the PBM changes
## Project layout
```
src/
main.c Entry point, minute-by-minute watch face loop
epd_drv.c/h SSD1680 e-ink driver (SPI, full/partial refresh, deep sleep)
watchface.c/h Framebuffer renderer (200x200 monochrome)
fonts.c/h Bitmap fonts (24x40 digits, 8x40 colon)
sim/
src/main.c Simulated main loop (accelerated time, no hardware)
src/epd_sim.c EPD mock - dumps framebuffer as hex over stdout
view.py Reads hex frames, writes PBM, launches feh
CMakeLists.txt Sim build config
prj.conf Sim Kconfig
boards/
xiao_nrf54l15_nrf54l15_cpuapp.overlay Devicetree overlay (SPI, GPIO, I2C, BT)
xiao_nrf54l15_nrf54l15_cpuapp.conf Board-specific Kconfig
dts/bindings/
nrfwarch,epd-ctrl.yaml Custom binding for EPD GPIO control node
tests/
src/main.c 17 ztest cases (framebuffer, pixels, fonts, rendering)
prj.conf Test build config
prj.conf Main application config
CMakeLists.txt Build definition
flake.nix Nix flake (dev shell, setup-sdk, test, sim)
tools/
gen_fonts.py Font generator (run to regenerate src/fonts.*)
```
## Flake commands
| Command | Description |
|---------|-------------|
| `nix develop` | Enter dev shell with all tools |
| `nix run .#setup-sdk` | Download and set up Zephyr SDK + workspace |
| `nix run .#test` | Build and run ztest suite on QEMU |
| `nix run .#sim` | Run display simulator with feh viewer |
## What works
- SSD1680 e-ink driver with full refresh and partial refresh
- Framebuffer-based watch face with time rendering (HH:MM) and battery indicator
- Deep sleep / wake for the display
- Display simulator with real-time feh output
- 17 passing tests covering framebuffer operations, font data integrity, and rendering
## In progress / planned
- BLE Current Time Service (CTS) client for phone time sync
- GRTC-based RTC for persistent timekeeping across deep sleep
- Battery monitoring via ADC (channel 7, VBAT regulator)
- LSM6DSO IMU wrist-tilt wake detection
- System-off power management with regulator control
- Partial refresh for minute updates (avoid full refresh flicker)
- BLE Battery Service (BAS) and notifications
## References
- [Seeed Xiao nRF54L15 Wiki](https://wiki.seeedstudio.com/xiao_nrf54l15_sense_getting_started/)
- [Zephyr RTOS Documentation](https://docs.zephyrproject.org/)
- [Zephyr QEMU x86 Board](https://docs.zephyrproject.org/latest/boards/qemu/x86/doc/index.html)
- [Zephyr ztest Framework](https://docs.zephyrproject.org/latest/develop/test/ztest.html)
- [SSD1680 datasheet](https://www.good-display.com/companyfile/409.html) (GDEY0154D61LT)
- [Sample repo: Toastee0/Seeed-Xiao-nRF54L15](https://github.com/Toastee0/Seeed-Xiao-nRF54L15)
- [E-ink driver reference: jcontrerasf/E-ink-display-Zephyr](https://github.com/jcontrerasf/E-ink-display-Zephyr)
## License
Apache 2.0
@@ -1,8 +0,0 @@
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_CONSOLE=y
CONFIG_GPIO=y
CONFIG_SPI=y
CONFIG_I2C=y
CONFIG_ADC=y
CONFIG_ADC_ASYNC=y
@@ -1,24 +0,0 @@
/ {
aliases {
vbat-reg = &vbat_pwr;
sw0 = &usr_btn;
led0 = &led0;
};
epd_ctrl: epd-ctrl {
compatible = "nrfwarch,epd-ctrl";
cs-gpios = <&xiao_d 1 GPIO_ACTIVE_LOW>;
dc-gpios = <&xiao_d 3 GPIO_ACTIVE_HIGH>;
rst-gpios = <&xiao_d 0 GPIO_ACTIVE_LOW>;
busy-gpios = <&xiao_d 5 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
};
};
&xiao_spi {
status = "okay";
};
&xiao_i2c {
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>;
};
-92
View File
@@ -1,92 +0,0 @@
# Development Log
## 2026-04-25: Initial Setup
### Project: nrfwarch
Smartwatch firmware for Seeed Xiao nRF54L15 Sense with GDEY0154D61LT 1.54" e-ink display (SSD1680 controller).
### Board setup
- Ported SSD1680 e-ink driver from jcontrerasf/E-ink-display-Zephyr (nRF52840/PlatformIO) to Xiao nRF54L15 with modern Zephyr APIs
- Created custom devicetree binding (`nrfwarch,epd-ctrl`) for EPD GPIO pins
- VCOM command (0x2C, value 0xA5) added to init sequence
- Partial update uses `0xCF` control byte (clock + analog + temperature, no LUT reload)
### Framebuffer renderer
- 200x200 monochrome framebuffer, 5000 bytes
- Y-flip in `wf_set_pixel` compensates for SSD1680 data entry mode 0x01 (Y decrement): `ram_row = (199 - y)`
- `wf_draw_glyph` draws a glyph bitmap onto the framebuffer at (dst_x, dst_y). Font data uses bit=1 for foreground (draw black), bit=0 for background (skip). Background pixels are NOT written so the framebuffer underneath is preserved.
- Stack overflow in `epd_clear` fixed by using static buffer instead of stack allocation
### Fonts
- `tools/gen_fonts.py` generates `src/fonts.h` and `src/fonts.c` programmatically
- Digits are 24x40px (120 bytes each), colon is 8x40px (40 bytes)
- Digits are drawn as stroked outlines: horizontal/vertical lines + filled rectangles
- Each digit is a seven-segment-style rendering with 2px stroke thickness and 3px margins
- To regenerate: `python3 tools/gen_fonts.py`
### EPD wiring (Xiao nRF54L15 Sense)
| Signal | Pin | GPIO |
|--------|-----|------|
| MOSI | D10 | SPI MOSI |
| CLK | D8 | SPI SCK |
| CS | D1 | xiao_d 1 |
| DC | D3 | xiao_d 3 |
| RST | D0 | xiao_d 0 |
| BUSY | D2 | xiao_d 2 |
## 2026-04-25: Display Simulator
### What was done
- `sim/` directory with display simulator for watchface development without hardware
- `sim/src/epd_sim.c` implements the same API as `src/epd_drv.c`, dumps framebuffer as hex over stdout with `!F!`/`!E!` framing
- `sim/view.py` reads hex frames from stdin, inverts polarity (framebuffer bit=1 is white, PBM bit=1 is black), writes PBM to `/tmp/nrfwarch.pbm`
- feh auto-launches with `--auto-reload` for live display
- Sim runs on Zephyr `native_sim` target (native Linux process, not QEMU). Time advances 1 min/sec.
### Sim architecture
```
Zephyr native_sim binary (zephyr.exe)
-> watchface.c renders to framebuffer (same code as hardware)
-> epd_sim.c dumps hex frames to stdout
-> view.py reads stdin, converts to PBM
-> feh displays the image with auto-reload
```
### Frame protocol
- `!F!` = frame start, then 5000 bytes as hex (32 bytes/line), `!E!` = frame end
- view.py ignores everything outside markers (handles Zephyr log output mixed in)
## 2026-04-25: Flake, Tests, and Font Fixes
### Nix flake
- Converted from shell.nix to flake.nix with three apps:
- `nix run .#setup-sdk` - downloads Zephyr v4.0 + SDK 0.17.0, patches all ELF binaries for NixOS
- `nix run .#test` - builds and runs ztest suite on QEMU x86
- `nix run .#sim` - builds and runs display simulator with feh viewer
- Dev shell includes: cmake, ninja, west, qemu_full, feh, dtc, patchelf
### NixOS SDK patching
- Zephyr SDK binaries are dynamically linked with `/lib64/ld-linux-x86-64.so.2` interpreter
- Must patchelf every ELF binary (bin/, libexec/, lib/) to point at NixOS's ld-linux
- The setup-sdk app handles this automatically
### Test suite (17 tests, all passing)
- **Framebuffer**: clear sets all 0xFF, pixel set/get roundtrip, bounds checking, Y-flip at corners
- **Fonts**: digit/colon array sizes match compile-time constants, all digits contain non-trivial data
- **Rendering**: draw_time modifies buffer, different times produce different buffers, battery indicator renders, 0% vs 100% battery differs
- **Glyph**: all-ones glyph sets pixel to black
### Bugs fixed this session
- **Inverted fonts**: `wf_draw_glyph` had `black = !(val & bit)` which inverted foreground/background. Fixed to only draw when bit=1 (foreground), skip bit=0 (background).
- **Leading bar artifact**: Background pixels were being explicitly written as white on every glyph byte, overwriting adjacent content. Fixed by skipping background pixels entirely (`continue` on bit=0).
- **Font data mismatch**: Original hand-written font data had wrong byte counts (112 bytes declared as 64). Replaced with programmatic generator.
- **epd_clear stack overflow**: 5000-byte array on stack blew past 4KB main stack. Changed to static buffer.
- **Devicetree binding**: `compatible = "simple-bus"` rejected custom GPIO properties. Created proper `nrfwarch,epd-ctrl.yaml` binding.
### Next steps
- BLE CTS client for time sync from phone
- GRTC-based RTC for persistent timekeeping across system-off
- Battery monitoring via ADC channel 7 with VBAT regulator
- LSM6DSO IMU wrist-tilt wake detection
- System-off power management with regulator control
- Partial refresh for minute updates (avoid full refresh flicker)
-20
View File
@@ -1,20 +0,0 @@
description: EPD GPIO control node for SSD1680
compatible: "nrfwarch,epd-ctrl"
properties:
cs-gpios:
type: phandle-array
required: true
description: Chip select GPIO
dc-gpios:
type: phandle-array
required: true
description: Data/command GPIO
rst-gpios:
type: phandle-array
required: true
description: Reset GPIO
busy-gpios:
type: phandle-array
required: true
description: Busy status GPIO
Generated
-61
View File
@@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
-167
View File
@@ -1,167 +0,0 @@
{
description = "nrfwarch - smartwatch firmware for Xiao nRF54L15 Sense";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
zephyrEnv = ''
export ZEPHYR_BASE="$(pwd)/.zephyr/zephyr"
export ZEPHYR_SDK_INSTALL_DIR="$(pwd)/.sdk/zephyr-sdk-0.17.4"
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
'';
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
cmake
ninja
python312
python312Packages.pip
python312Packages.west
python312Packages.pyelftools
python312Packages.jsonschema
qemu_full
dtc
git
patchelf
gnumake
feh
];
shellHook = zephyrEnv + ''
echo "--- nrfwarch ---"
echo "Zephyr: $ZEPHYR_BASE"
echo "SDK: $ZEPHYR_SDK_INSTALL_DIR"
echo ""
echo "Setup: nix run .#setup-sdk"
echo "Tests: nix run .#test"
echo "Simulate: nix run .#sim"
'';
ZEPHYR_BASE = toString ./.zephyr/zephyr;
ZEPHYR_SDK_INSTALL_DIR = toString ./.sdk/zephyr-sdk-0.17.4;
ZEPHYR_TOOLCHAIN_VARIANT = "zephyr";
};
apps = {
setup-sdk = {
type = "app";
program = toString (
pkgs.writeShellScript "setup-sdk" ''
set -euo pipefail
SDK_VER="0.17.4"
SDK_DIR=".sdk/zephyr-sdk-$SDK_VER"
ZEPHYR_DIR=".zephyr"
export PATH="${pkgs.git}/bin:${pkgs.python312Packages.west}/bin:${pkgs.wget}/bin:${pkgs.xz}/bin:${pkgs.patchelf}/bin:${pkgs.findutils}/bin:${pkgs.gnugrep}/bin:${pkgs.file}/bin:${pkgs.curl}/bin:${pkgs.gnutar}/bin:$PATH"
if [ ! -d "$ZEPHYR_DIR/zephyr" ]; then
echo "Initializing Zephyr workspace..."
west init -m https://github.com/zephyrproject-rtos/zephyr --mr v4.3.0 "$ZEPHYR_DIR"
(cd "$ZEPHYR_DIR" && west update)
echo "Zephyr workspace ready."
else
echo "Zephyr workspace already exists."
fi
if [ ! -d "$SDK_DIR" ]; then
echo "Downloading Zephyr SDK $SDK_VER..."
mkdir -p .sdk
curl -fsSL "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v$SDK_VER/zephyr-sdk-''${SDK_VER}_linux-x86_64.tar.xz" \
-o .sdk/zephyr-sdk.tar.xz
echo "Extracting..."
tar xf .sdk/zephyr-sdk.tar.xz -C .sdk
rm .sdk/zephyr-sdk.tar.xz
"$SDK_DIR/setup.sh" -t x86_64-zephyr-elf -t aarch64-zephyr-elf -t arm-zephyr-eabi
echo "Patching binaries for NixOS..."
LD_PATH="${pkgs.stdenv.cc.bintools.dynamicLinker}"
find "$SDK_DIR" -type f | while read -r f; do
if file "$f" 2>/dev/null | grep -q "ELF 64-bit.*dynamically linked"; then
patchelf --set-interpreter "$LD_PATH" "$f" 2>/dev/null || true
fi
done
echo "SDK ready."
else
echo "SDK already exists."
fi
echo ""
echo "All set. Run 'nix develop' to enter the dev shell."
''
);
};
test = {
type = "app";
program = toString (
pkgs.writeShellScript "test" ''
set -euo pipefail
${zephyrEnv}
if [ ! -d "$ZEPHYR_BASE" ]; then
echo "Zephyr not found. Run 'nix run .#setup-sdk' first."
exit 1
fi
BUILD_DIR="build-test"
if [ ! -f "$BUILD_DIR/build.ninja" ]; then
cmake -B "$BUILD_DIR" -GNinja -DBOARD=qemu_x86 -S tests
fi
ninja -C "$BUILD_DIR"
ninja -C "$BUILD_DIR" run
''
);
};
sim = {
type = "app";
program = toString (
pkgs.writeShellScript "sim" ''
set -euo pipefail
${zephyrEnv}
if [ ! -d "$ZEPHYR_BASE" ]; then
echo "Zephyr not found. Run 'nix run .#setup-sdk' first."
exit 1
fi
BUILD_DIR="build-sim"
if [ ! -f "$BUILD_DIR/build.ninja" ]; then
cmake -B "$BUILD_DIR" -GNinja -DBOARD=qemu_x86 -S sim
fi
ninja -C "$BUILD_DIR"
cleanup() {
kill $(jobs -p) 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup EXIT INT TERM
echo "Starting display sim (Ctrl+C to stop)..."
"${pkgs.python312}/bin/python3" sim/view.py < <(
"${pkgs.ninja}/bin/ninja" -C "$BUILD_DIR" run 2>/dev/null
)
''
);
};
};
}
);
}
-11
View File
@@ -1,11 +0,0 @@
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_LOG=y
CONFIG_PRINTK=y
CONFIG_SPI=y
CONFIG_GPIO=y
CONFIG_NEWLIB_LIBC=y
CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y
-7
View File
@@ -1,7 +0,0 @@
sample:
name: nrfwarch
description: Smartwatch firmware for Xiao nRF54L15 Sense with e-ink display
tests:
test:
build_only: true
platform_allow: xiao_nrf54l15/nrf54l15/cpuapp
-14
View File
@@ -1,14 +0,0 @@
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrfwarch_sim)
target_sources(app PRIVATE
src/main.c
src/epd_sim.c
../src/watchface.c
../src/fonts.c
)
target_include_directories(app PRIVATE ../src)
-6
View File
@@ -1,6 +0,0 @@
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_CONSOLE=y
CONFIG_PRINTK=y
CONFIG_LOG=y
CONFIG_MAIN_STACK_SIZE=8192
-69
View File
@@ -1,69 +0,0 @@
#include "epd_drv.h"
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#define CHUNK 32
static void hex_byte(char *out, uint8_t val)
{
out[0] = "0123456789abcdef"[val >> 4];
out[1] = "0123456789abcdef"[val & 0x0F];
}
static void dump_frame(const uint8_t *image)
{
printk("!F!\n");
char line[CHUNK * 2 + 1];
for (int i = 0; i < EPD_BUFFER_SIZE; i += CHUNK) {
int n = (EPD_BUFFER_SIZE - i > CHUNK) ? CHUNK : (EPD_BUFFER_SIZE - i);
for (int j = 0; j < n; j++) {
hex_byte(line + j * 2, image[i + j]);
}
line[n * 2] = '\0';
printk("%s\n", line);
}
printk("!E!\n");
}
int epd_init(void)
{
printk("SIM: epd_init\n");
return 0;
}
void epd_full_refresh(const uint8_t *image)
{
dump_frame(image);
}
void epd_clear(void)
{
uint8_t white[EPD_BUFFER_SIZE];
for (int i = 0; i < EPD_BUFFER_SIZE; i++) {
white[i] = 0xFF;
}
dump_frame(white);
}
void epd_set_base_image(const uint8_t *image)
{
dump_frame(image);
}
void epd_partial_region(const uint8_t *image, size_t len,
int x_start, int x_end,
int y_start, int y_end)
{
dump_frame(image);
}
void epd_deep_sleep(void)
{
printk("SIM: sleep\n");
}
void epd_wakeup(void)
{
printk("SIM: wakeup\n");
}
-41
View File
@@ -1,41 +0,0 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include "epd_drv.h"
#include "watchface.h"
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
void main(void)
{
int hours = 12;
int minutes = 0;
int battery = 87;
epd_init();
LOG_INF("nrfwarch sim started");
while (1) {
wf_clear();
wf_draw_time(hours, minutes);
wf_draw_battery_indicator(battery);
epd_set_base_image(wf_get_buffer());
LOG_INF("%02d:%02d bat=%d%%", hours, minutes, battery);
k_sleep(K_MSEC(800));
minutes++;
if (minutes >= 60) {
minutes = 0;
hours++;
if (hours >= 24) {
hours = 0;
}
battery -= 2;
if (battery < 0) {
battery = 87;
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""Reads hex-framed display data from stdin, writes PBM, auto-launches feh."""
import sys
import os
import signal
import subprocess
import time
WIDTH = 200
HEIGHT = 200
BUFSIZE = (WIDTH * HEIGHT) // 8
OUT = "/tmp/nrfwarch.pbm"
FRAME_START = "!F!"
FRAME_END = "!E!"
feh_proc = None
def write_pbm(data):
header = f"P4\n{WIDTH} {HEIGHT}\n".encode()
inverted = bytes((~b) & 0xFF for b in data)
tmp = OUT + ".tmp"
with open(tmp, "wb") as f:
f.write(header)
f.write(inverted)
os.replace(tmp, OUT)
def launch_feh():
global feh_proc
if feh_proc is not None and feh_proc.poll() is None:
return
try:
feh_proc = subprocess.Popen(
[
"feh",
"--force-aliasing",
"--scale",
"auto",
"--auto-reload",
"--title", "nrfwarch sim",
OUT,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except FileNotFoundError:
print("feh not found, writing PBM to " + OUT)
def cleanup(*_):
global feh_proc
if feh_proc and feh_proc.poll() is None:
feh_proc.terminate()
sys.exit(0)
def main():
signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)
write_pbm(bytes([0xFF] * BUFSIZE))
launch_feh()
capturing = False
hex_buf = ""
frame_count = 0
for line in sys.stdin:
line = line.strip()
if line == FRAME_START:
capturing = True
hex_buf = ""
elif line == FRAME_END:
if capturing and hex_buf:
try:
data = bytes.fromhex(hex_buf)
if len(data) == BUFSIZE:
write_pbm(data)
frame_count += 1
print(f" frame {frame_count}", file=sys.stderr)
except ValueError:
pass
capturing = False
hex_buf = ""
elif capturing:
hex_buf += line
if __name__ == "__main__":
main()
-246
View File
@@ -1,246 +0,0 @@
#include "epd_drv.h"
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(epd, LOG_LEVEL_INF);
#define EPD_SPI_BUS DT_NODELABEL(xiao_spi)
#define EPD_CTRL_NODE DT_NODELABEL(epd_ctrl)
static const struct gpio_dt_spec epd_cs = GPIO_DT_SPEC_GET(EPD_CTRL_NODE, cs_gpios);
static const struct gpio_dt_spec epd_dc = GPIO_DT_SPEC_GET(EPD_CTRL_NODE, dc_gpios);
static const struct gpio_dt_spec epd_rst = GPIO_DT_SPEC_GET(EPD_CTRL_NODE, rst_gpios);
static const struct gpio_dt_spec epd_busy = GPIO_DT_SPEC_GET(EPD_CTRL_NODE, busy_gpios);
static const struct device *spi_dev;
static struct spi_config spi_cfg;
#define SWRESET 0x12
#define DRIVER_OUT 0x01
#define DATA_ENTRY 0x11
#define RAM_X_RANGE 0x44
#define RAM_Y_RANGE 0x45
#define RAM_X_ADDR 0x4E
#define RAM_Y_ADDR 0x4F
#define BORDER_WF 0x3C
#define TEMP_SENSOR 0x18
#define UPDATE_CTRL2 0x22
#define ACTIVATE 0x20
#define WRITE_RAM_BW 0x24
#define DEEP_SLEEP 0x10
#define VCOM 0x2C
static int wait_busy(void)
{
int retries = 0;
while (gpio_pin_get_dt(&epd_busy) == 1) {
k_msleep(10);
if (++retries > 3000) {
LOG_ERR("BUSY timeout");
return -ETIMEDOUT;
}
}
return 0;
}
static int spi_send_buf(const uint8_t *data, size_t len)
{
struct spi_buf buf = { .buf = (void *)data, .len = len };
struct spi_buf_set set = { .buffers = &buf, .count = 1 };
return spi_write(spi_dev, &spi_cfg, &set);
}
static int send_command(uint8_t cmd)
{
gpio_pin_set_dt(&epd_cs, 0);
gpio_pin_set_dt(&epd_dc, 0);
int ret = spi_send_buf(&cmd, 1);
gpio_pin_set_dt(&epd_cs, 1);
return ret;
}
static int send_data(uint8_t data)
{
gpio_pin_set_dt(&epd_cs, 0);
gpio_pin_set_dt(&epd_dc, 1);
int ret = spi_send_buf(&data, 1);
gpio_pin_set_dt(&epd_cs, 1);
return ret;
}
static int send_data_buf(const uint8_t *data, size_t len)
{
gpio_pin_set_dt(&epd_cs, 0);
gpio_pin_set_dt(&epd_dc, 1);
int ret = spi_send_buf(data, len);
gpio_pin_set_dt(&epd_cs, 1);
return ret;
}
static void hw_reset(void)
{
gpio_pin_set_dt(&epd_rst, 0);
k_msleep(20);
gpio_pin_set_dt(&epd_rst, 1);
k_msleep(20);
wait_busy();
}
static int init_gpios(void)
{
if (!gpio_is_ready_dt(&epd_cs) || !gpio_is_ready_dt(&epd_dc) ||
!gpio_is_ready_dt(&epd_rst) || !gpio_is_ready_dt(&epd_busy)) {
LOG_ERR("EPD GPIOs not ready");
return -ENODEV;
}
gpio_pin_configure_dt(&epd_cs, GPIO_OUTPUT_HIGH);
gpio_pin_configure_dt(&epd_dc, GPIO_OUTPUT_LOW);
gpio_pin_configure_dt(&epd_rst, GPIO_OUTPUT_HIGH);
gpio_pin_configure_dt(&epd_busy, GPIO_INPUT);
return 0;
}
static int init_spi(void)
{
spi_dev = DEVICE_DT_GET(EPD_SPI_BUS);
if (!device_is_ready(spi_dev)) {
LOG_ERR("SPI device not ready");
return -ENODEV;
}
memset(&spi_cfg, 0, sizeof(spi_cfg));
spi_cfg.frequency = 4000000;
spi_cfg.operation = SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB |
SPI_WORD_SET(8) | SPI_LINES_SINGLE;
return 0;
}
static void set_ram_window(int x_start, int x_end,
int y_start, int y_end)
{
send_command(RAM_X_RANGE);
send_data(x_start);
send_data(x_end);
send_command(RAM_Y_RANGE);
send_data(y_end & 0xFF);
send_data((y_end >> 8) & 0xFF);
send_data(y_start & 0xFF);
send_data((y_start >> 8) & 0xFF);
}
static void set_ram_cursor(int x, int y)
{
send_command(RAM_X_ADDR);
send_data(x);
send_command(RAM_Y_ADDR);
send_data(y & 0xFF);
send_data((y >> 8) & 0xFF);
}
static void trigger_full_update(void)
{
send_command(UPDATE_CTRL2);
send_data(0xF7);
send_command(ACTIVATE);
wait_busy();
}
static void trigger_partial_update(void)
{
send_command(UPDATE_CTRL2);
send_data(0xCF);
send_command(ACTIVATE);
wait_busy();
}
static void init_panel(void)
{
send_command(SWRESET);
wait_busy();
send_command(DRIVER_OUT);
send_data(0xC7);
send_data(0x00);
send_data(0x00);
send_command(DATA_ENTRY);
send_data(0x01);
set_ram_window(0x00, 0x18, 0x00, 0xC7);
send_command(BORDER_WF);
send_data(0x05);
send_command(VCOM);
send_data(0xA5);
send_command(TEMP_SENSOR);
send_data(0x80);
}
int epd_init(void)
{
int ret = init_gpios();
if (ret) return ret;
ret = init_spi();
if (ret) return ret;
hw_reset();
init_panel();
LOG_INF("SSD1680 initialized");
return 0;
}
void epd_full_refresh(const uint8_t *image)
{
set_ram_window(0x00, 0x18, 0x00, 0xC7);
set_ram_cursor(0x00, 0xC7);
send_command(WRITE_RAM_BW);
send_data_buf(image, EPD_BUFFER_SIZE);
trigger_full_update();
}
static uint8_t white_buf[EPD_BUFFER_SIZE];
void epd_clear(void)
{
memset(white_buf, 0xFF, sizeof(white_buf));
epd_full_refresh(white_buf);
}
void epd_set_base_image(const uint8_t *image)
{
set_ram_window(0x00, 0x18, 0x00, 0xC7);
set_ram_cursor(0x00, 0xC7);
send_command(WRITE_RAM_BW);
send_data_buf(image, EPD_BUFFER_SIZE);
trigger_full_update();
}
void epd_partial_region(const uint8_t *image, size_t len,
int x_start, int x_end,
int y_start, int y_end)
{
set_ram_window(x_start, x_end, y_start, y_end);
set_ram_cursor(x_start, y_end);
send_command(WRITE_RAM_BW);
send_data_buf(image, len);
trigger_partial_update();
}
void epd_deep_sleep(void)
{
send_command(DEEP_SLEEP);
send_data(0x01);
k_msleep(100);
}
void epd_wakeup(void)
{
hw_reset();
init_panel();
}
-23
View File
@@ -1,23 +0,0 @@
#ifndef EPD_DRV_H
#define EPD_DRV_H
#include <stdint.h>
#include <stddef.h>
#define EPD_WIDTH 200
#define EPD_HEIGHT 200
#define EPD_BUFFER_SIZE ((EPD_WIDTH * EPD_HEIGHT) / 8)
#define EPD_LINE_BYTES (EPD_WIDTH / 8)
#define EPD_COLUMN_BYTES EPD_HEIGHT
int epd_init(void);
void epd_full_refresh(const uint8_t *image);
void epd_clear(void);
void epd_set_base_image(const uint8_t *image);
void epd_partial_region(const uint8_t *image, size_t len,
int x_start, int x_end,
int y_start, int y_end);
void epd_deep_sleep(void);
void epd_wakeup(void);
#endif
-467
View File
@@ -1,467 +0,0 @@
#include "fonts.h"
const uint8_t font_digits[10][120] = {
{
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF0,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF0,
},
{
0x00, 0x00, 0x00,
0x0F, 0xFC, 0x00,
0x0F, 0xFC, 0x00,
0x0F, 0xFC, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x1C, 0x00,
0x00, 0x00, 0x00,
},
{
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x0F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF0,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF0,
},
{
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF0,
},
{
0x00, 0x00, 0x00,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x00,
},
{
0x0F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF0,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF0,
},
{
0x0F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF0,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1C, 0x00, 0x00,
0x1F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF0,
},
{
0x0F, 0xFF, 0xF0,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x00,
},
{
0x0F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF0,
},
{
0x0F, 0xFF, 0xF0,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1C, 0x00, 0x38,
0x1F, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x00, 0x00, 0x38,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF8,
0x0F, 0xFF, 0xF0,
},
};
const uint8_t font_colon[40] = {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1C,
0x1C,
0x1C,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1C,
0x1C,
0x1C,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
};
-18
View File
@@ -1,18 +0,0 @@
#ifndef FONTS_H
#define FONTS_H
#include <stdint.h>
#include <stddef.h>
#define FONT_DIGIT_W 24
#define FONT_DIGIT_H 40
#define FONT_DIGIT_BYTES (FONT_DIGIT_W * FONT_DIGIT_H / 8)
#define FONT_COLON_W 8
#define FONT_COLON_H 40
#define FONT_COLON_BYTES (FONT_COLON_W * FONT_COLON_H / 8)
extern const uint8_t font_digits[10][FONT_DIGIT_BYTES];
extern const uint8_t font_colon[FONT_COLON_BYTES];
#endif
-52
View File
@@ -1,52 +0,0 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include "epd_drv.h"
#include "watchface.h"
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
int main(void)
{
int ret;
LOG_INF("nrfwarch starting");
ret = epd_init();
if (ret) {
LOG_ERR("EPD init failed: %d", ret);
return ret;
}
wf_clear();
wf_draw_time(12, 0);
wf_draw_battery_indicator(75);
LOG_INF("Rendering initial watch face");
epd_full_refresh(wf_get_buffer());
LOG_INF("Initial render complete");
int hours = 12;
int minutes = 0;
while (1) {
k_sleep(K_SECONDS(60));
minutes++;
if (minutes >= 60) {
minutes = 0;
hours++;
if (hours >= 24) {
hours = 0;
}
}
wf_clear();
wf_draw_time(hours, minutes);
wf_draw_battery_indicator(75);
epd_set_base_image(wf_get_buffer());
LOG_INF("Updated: %02d:%02d", hours, minutes);
}
}
-115
View File
@@ -1,115 +0,0 @@
#include "watchface.h"
#include "fonts.h"
#include <string.h>
static uint8_t framebuffer[WF_BUF_SIZE];
void wf_clear(void)
{
memset(framebuffer, 0xFF, WF_BUF_SIZE);
}
void wf_set_pixel(int x, int y, int black)
{
if (x < 0 || x >= WF_WIDTH || y < 0 || y >= WF_HEIGHT) {
return;
}
int ram_row = (WF_HEIGHT - 1) - y;
int byte_idx = ram_row * WF_LINE_BYTES + (x / 8);
int bit = 7 - (x % 8);
if (black) {
framebuffer[byte_idx] &= ~(1 << bit);
} else {
framebuffer[byte_idx] |= (1 << bit);
}
}
int wf_get_pixel(int x, int y)
{
if (x < 0 || x >= WF_WIDTH || y < 0 || y >= WF_HEIGHT) {
return -1;
}
int ram_row = (WF_HEIGHT - 1) - y;
int byte_idx = ram_row * WF_LINE_BYTES + (x / 8);
int bit = 7 - (x % 8);
return (framebuffer[byte_idx] >> bit) & 1;
}
void wf_draw_glyph(const uint8_t *glyph, int glyph_w, int glyph_h,
int dst_x, int dst_y)
{
int glyph_line_bytes = glyph_w / 8;
for (int row = 0; row < glyph_h; row++) {
for (int byte_col = 0; byte_col < glyph_line_bytes; byte_col++) {
uint8_t val = glyph[row * glyph_line_bytes + byte_col];
for (int bit = 0; bit < 8; bit++) {
if (!(val & (1 << bit))) {
continue;
}
int px = dst_x + byte_col * 8 + (7 - bit);
int py = dst_y + row;
wf_set_pixel(px, py, 1);
}
}
}
}
#define TIME_DIGIT_W FONT_DIGIT_W
#define TIME_DIGIT_H FONT_DIGIT_H
#define TIME_COLON_W FONT_COLON_W
#define TIME_COLON_H FONT_COLON_H
#define TIME_GAP 4
#define TIME_TOTAL_W (4 * TIME_DIGIT_W + TIME_COLON_W + 4 * TIME_GAP)
#define TIME_X_START ((WF_WIDTH - TIME_TOTAL_W) / 2)
#define TIME_Y_START 40
void wf_draw_time(int hours, int minutes)
{
int h1 = (hours / 10) % 10;
int h2 = hours % 10;
int m1 = (minutes / 10) % 10;
int m2 = minutes % 10;
int x = TIME_X_START;
wf_draw_glyph(font_digits[h1], TIME_DIGIT_W, TIME_DIGIT_H, x, TIME_Y_START);
x += TIME_DIGIT_W + TIME_GAP;
wf_draw_glyph(font_digits[h2], TIME_DIGIT_W, TIME_DIGIT_H, x, TIME_Y_START);
x += TIME_DIGIT_W + TIME_GAP;
wf_draw_glyph(font_colon, TIME_COLON_W, TIME_COLON_H, x, TIME_Y_START);
x += TIME_COLON_W + TIME_GAP;
wf_draw_glyph(font_digits[m1], TIME_DIGIT_W, TIME_DIGIT_H, x, TIME_Y_START);
x += TIME_DIGIT_W + TIME_GAP;
wf_draw_glyph(font_digits[m2], TIME_DIGIT_W, TIME_DIGIT_H, x, TIME_Y_START);
}
void wf_draw_battery_indicator(int percent)
{
int bar_w = 30;
int bar_h = 8;
int x_start = WF_WIDTH - bar_w - 10;
int y_start = 10;
for (int x = 0; x < bar_w; x++) {
for (int y = 0; y < bar_h; y++) {
int is_border = (x == 0 || x == bar_w - 1 ||
y == 0 || y == bar_h - 1);
int fill_end = 1 + (percent * (bar_w - 2)) / 100;
int is_filled = (x >= 1 && x < fill_end &&
y >= 1 && y < bar_h - 1);
wf_set_pixel(x_start + x, y_start + y, is_border || is_filled);
}
}
}
uint8_t *wf_get_buffer(void)
{
return framebuffer;
}
-20
View File
@@ -1,20 +0,0 @@
#ifndef WATCHFACE_H
#define WATCHFACE_H
#include <stdint.h>
#define WF_WIDTH 200
#define WF_HEIGHT 200
#define WF_BUF_SIZE ((WF_WIDTH * WF_HEIGHT) / 8)
#define WF_LINE_BYTES (WF_WIDTH / 8)
void wf_clear(void);
void wf_set_pixel(int x, int y, int black);
int wf_get_pixel(int x, int y);
void wf_draw_glyph(const uint8_t *glyph, int glyph_w, int glyph_h,
int dst_x, int dst_y);
void wf_draw_time(int hours, int minutes);
void wf_draw_battery_indicator(int percent);
uint8_t *wf_get_buffer(void);
#endif
-13
View File
@@ -1,13 +0,0 @@
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(nrfwarch_test)
target_sources(app PRIVATE
src/main.c
../src/watchface.c
../src/fonts.c
)
target_include_directories(app PRIVATE ../src)
-2
View File
@@ -1,2 +0,0 @@
CONFIG_ZTEST=y
CONFIG_MAIN_STACK_SIZE=8192
-7
View File
@@ -1,7 +0,0 @@
sample:
name: nrfwarch tests
tests:
test:
build_only: false
platform_allow: qemu_x86
tags: testing
-217
View File
@@ -1,217 +0,0 @@
#include <zephyr/ztest.h>
#include <string.h>
#include "watchface.h"
#include "fonts.h"
ZTEST(nrfwarch, test_wf_clear_sets_all_white)
{
wf_clear();
uint8_t *buf = wf_get_buffer();
for (int i = 0; i < WF_BUF_SIZE; i++) {
zassert_equal(buf[i], 0xFF,
"byte %d should be 0xFF (white), got 0x%02X", i, buf[i]);
}
}
ZTEST(nrfwarch, test_wf_set_pixel_out_of_bounds)
{
wf_clear();
wf_set_pixel(-1, 0, 1);
wf_set_pixel(0, -1, 1);
wf_set_pixel(WF_WIDTH, 0, 1);
wf_set_pixel(0, WF_HEIGHT, 1);
uint8_t *buf = wf_get_buffer();
for (int i = 0; i < WF_BUF_SIZE; i++) {
zassert_equal(buf[i], 0xFF, "out-of-bounds pixel modified buffer at byte %d", i);
}
}
ZTEST(nrfwarch, test_wf_set_pixel_black_top_left)
{
wf_clear();
wf_set_pixel(0, WF_HEIGHT - 1, 1);
uint8_t *buf = wf_get_buffer();
zassert_equal(buf[0], 0x7F,
"top-left pixel (x=0, y=199) should set bit 7 of byte 0, got 0x%02X",
buf[0]);
}
ZTEST(nrfwarch, test_wf_set_pixel_black_bottom_left)
{
wf_clear();
wf_set_pixel(0, 0, 1);
uint8_t *buf = wf_get_buffer();
int last_row = (WF_HEIGHT - 1) * WF_LINE_BYTES;
zassert_equal(buf[last_row], 0x7F,
"bottom-left pixel (x=0, y=0) should set bit 7 of last row byte, got 0x%02X",
buf[last_row]);
}
ZTEST(nrfwarch, test_wf_set_then_clear_pixel)
{
wf_clear();
wf_set_pixel(10, 10, 1);
int after_black = wf_get_pixel(10, 10);
zassert_equal(after_black, 0, "pixel should be black (0), got %d", after_black);
wf_set_pixel(10, 10, 0);
int after_white = wf_get_pixel(10, 10);
zassert_equal(after_white, 1, "pixel should be white (1), got %d", after_white);
}
ZTEST(nrfwarch, test_wf_get_pixel_out_of_bounds)
{
zassert_equal(wf_get_pixel(-1, 0), -1);
zassert_equal(wf_get_pixel(0, -1), -1);
zassert_equal(wf_get_pixel(WF_WIDTH, 0), -1);
zassert_equal(wf_get_pixel(0, WF_HEIGHT), -1);
}
ZTEST(nrfwarch, test_font_digits_size)
{
for (int d = 0; d < 10; d++) {
size_t expected = (size_t)(FONT_DIGIT_W * FONT_DIGIT_H / 8);
zassert_equal(sizeof(font_digits[d]), expected,
"digit %d: expected %zu bytes, got %zu",
d, expected, sizeof(font_digits[d]));
}
}
ZTEST(nrfwarch, test_font_colon_size)
{
size_t expected = (size_t)(FONT_COLON_W * FONT_COLON_H / 8);
zassert_equal(sizeof(font_colon), expected,
"colon: expected %zu bytes, got %zu",
expected, sizeof(font_colon));
}
ZTEST(nrfwarch, test_font_digit_not_all_ff)
{
for (int d = 0; d < 10; d++) {
int all_ff = 1;
for (size_t i = 0; i < sizeof(font_digits[d]); i++) {
if (font_digits[d][i] != 0xFF) {
all_ff = 0;
break;
}
}
zassert_false(all_ff, "digit %d is all 0xFF (empty/white)", d);
}
}
ZTEST(nrfwarch, test_font_colon_not_all_ff)
{
int all_ff = 1;
for (size_t i = 0; i < sizeof(font_colon); i++) {
if (font_colon[i] != 0xFF) {
all_ff = 0;
break;
}
}
zassert_false(all_ff, "colon is all 0xFF (empty/white)");
}
ZTEST(nrfwarch, test_wf_draw_time_modifies_buffer)
{
wf_clear();
wf_draw_time(12, 34);
uint8_t *buf = wf_get_buffer();
int any_black = 0;
for (int i = 0; i < WF_BUF_SIZE; i++) {
if (buf[i] != 0xFF) {
any_black = 1;
break;
}
}
zassert_true(any_black, "draw_time produced no visible pixels");
}
ZTEST(nrfwarch, test_wf_draw_time_different_times)
{
wf_clear();
wf_draw_time(11, 11);
uint8_t buf_a[WF_BUF_SIZE];
memcpy(buf_a, wf_get_buffer(), WF_BUF_SIZE);
wf_clear();
wf_draw_time(22, 22);
uint8_t *buf_b = wf_get_buffer();
int differs = 0;
for (int i = 0; i < WF_BUF_SIZE; i++) {
if (buf_a[i] != buf_b[i]) {
differs = 1;
break;
}
}
zassert_true(differs, "different times produce identical buffers");
}
ZTEST(nrfwarch, test_wf_draw_battery_indicator)
{
wf_clear();
wf_draw_battery_indicator(100);
uint8_t *buf = wf_get_buffer();
int any_black = 0;
for (int i = 0; i < WF_BUF_SIZE; i++) {
if (buf[i] != 0xFF) {
any_black = 1;
break;
}
}
zassert_true(any_black, "battery indicator produced no visible pixels");
}
ZTEST(nrfwarch, test_wf_draw_battery_zero_vs_full)
{
wf_clear();
wf_draw_battery_indicator(0);
uint8_t buf_empty[WF_BUF_SIZE];
memcpy(buf_empty, wf_get_buffer(), WF_BUF_SIZE);
wf_clear();
wf_draw_battery_indicator(100);
uint8_t *buf_full = wf_get_buffer();
int differs = 0;
for (int i = 0; i < WF_BUF_SIZE; i++) {
if (buf_empty[i] != buf_full[i]) {
differs = 1;
break;
}
}
zassert_true(differs, "0%% and 100%% battery look identical");
}
ZTEST(nrfwarch, test_epd_buffer_size)
{
zassert_equal(WF_BUF_SIZE, 5000, "200x200 / 8 should be 5000, got %d", WF_BUF_SIZE);
zassert_equal(WF_LINE_BYTES, 25, "200/8 should be 25, got %d", WF_LINE_BYTES);
}
ZTEST(nrfwarch, test_wf_draw_glyph_modifies_pixels)
{
wf_clear();
uint8_t test_glyph[] = { 0xFF };
wf_draw_glyph(test_glyph, 8, 1, 50, 50);
zassert_equal(wf_get_pixel(50, 50), 0, "pixel should be black after drawing all-ones glyph");
}
ZTEST(nrfwarch, test_wf_pixel_symmetry)
{
wf_clear();
wf_set_pixel(100, 100, 1);
zassert_equal(wf_get_pixel(100, 100), 0, "center pixel should be black");
wf_set_pixel(100, 100, 0);
zassert_equal(wf_get_pixel(100, 100), 1, "center pixel should be white after clear");
}
ZTEST_SUITE(nrfwarch, NULL, NULL, NULL, NULL, NULL);
-201
View File
@@ -1,201 +0,0 @@
#!/usr/bin/env python3
"""Generate bitmap digit/colon fonts as C arrays for the e-ink watchface.
Renders digits as stroked outlines onto a pixel grid.
'#' = black (drawn), '.' = white (background).
Bit=0 is black (drawn), bit=1 is white (background) in the output.
"""
DIGIT_W = 24
DIGIT_H = 40
COLON_W = 8
COLON_H = 40
def make_grid(w, h):
return [["." for _ in range(w)] for _ in range(h)]
def fill_rect(grid, x1, y1, x2, y2):
for y in range(max(0, y1), min(len(grid), y2 + 1)):
for x in range(max(0, x1), min(len(grid[0]), x2 + 1)):
grid[y][x] = "#"
def draw_hline(grid, y, x1, x2, thickness=2):
fill_rect(grid, x1, y - thickness // 2, x2, y + thickness // 2)
def draw_vline(grid, x, y1, y2, thickness=2):
fill_rect(grid, x - thickness // 2, y1, x + thickness // 2, y2)
def draw_digit(d):
g = make_grid(DIGIT_W, DIGIT_H)
W = DIGIT_W
H = DIGIT_H
t = 2 # stroke thickness
m = 3 # margin
top = t // 2
bot = H - t // 2 - 1
left = m + t // 2
right = W - m - t // 2 - 1
mid_y = H // 2
if d == 0:
draw_vline(g, left, top + t, bot - t, t)
draw_vline(g, right, top + t, bot - t, t)
draw_hline(g, top, left, right, t)
draw_hline(g, bot, left, right, t)
elif d == 1:
draw_vline(g, W // 2, top, bot, t)
fill_rect(g, left, top, W // 2, top + t)
elif d == 2:
draw_hline(g, top, left, right, t)
draw_vline(g, right, top, mid_y, t)
draw_hline(g, mid_y, left, right, t)
draw_vline(g, left, mid_y, bot, t)
draw_hline(g, bot, left, right, t)
elif d == 3:
draw_hline(g, top, left, right, t)
draw_vline(g, right, top, bot, t)
draw_hline(g, mid_y, left, right, t)
draw_hline(g, bot, left, right, t)
elif d == 4:
draw_vline(g, left, top, mid_y, t)
draw_hline(g, mid_y, left, right, t)
draw_vline(g, right, top, bot, t)
elif d == 5:
draw_hline(g, top, left, right, t)
draw_vline(g, left, top, mid_y, t)
draw_hline(g, mid_y, left, right, t)
draw_vline(g, right, mid_y, bot, t)
draw_hline(g, bot, left, right, t)
elif d == 6:
draw_hline(g, top, left, right, t)
draw_vline(g, left, top, bot, t)
draw_hline(g, mid_y, left, right, t)
draw_vline(g, right, mid_y, bot, t)
draw_hline(g, bot, left, right, t)
elif d == 7:
draw_hline(g, top, left, right, t)
draw_vline(g, right, top, bot, t)
elif d == 8:
draw_hline(g, top, left, right, t)
draw_hline(g, mid_y, left, right, t)
draw_hline(g, bot, left, right, t)
draw_vline(g, left, top, bot, t)
draw_vline(g, right, top, bot, t)
elif d == 9:
draw_hline(g, top, left, right, t)
draw_vline(g, left, top, mid_y, t)
draw_hline(g, mid_y, left, right, t)
draw_vline(g, right, top, bot, t)
draw_hline(g, bot, left, right, t)
return ["".join(row) for row in g]
def draw_colon():
g = make_grid(COLON_W, COLON_H)
cx = COLON_W // 2
t = 2
fill_rect(g, cx - 1, COLON_H // 4 - 1, cx + 1, COLON_H // 4 + 1)
fill_rect(g, cx - 1, 3 * COLON_H // 4 - 1, cx + 1, 3 * COLON_H // 4 + 1)
return ["".join(row) for row in g]
def pattern_to_bytes(pattern, width):
rows = []
for row_str in pattern:
for byte_idx in range(width // 8):
byte_val = 0
for bit in range(8):
col = byte_idx * 8 + bit
if col < len(row_str) and row_str[col] == "#":
byte_val |= 1 << (7 - bit)
rows.append(byte_val)
return rows
def format_c_array(name, data, width):
entry_bytes = width // 8
lines = []
for i in range(0, len(data), entry_bytes):
chunk = data[i : i + entry_bytes]
lines.append("\t" + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
return f"static const uint8_t {name}[] = {{\n" + "\n".join(lines) + "\n};"
def main():
digit_patterns = {str(d): draw_digit(d) for d in range(10)}
colon_pat = draw_colon()
h_lines = [
"#ifndef FONTS_H",
"#define FONTS_H",
"",
"#include <stdint.h>",
"#include <stddef.h>",
"",
f"#define FONT_DIGIT_W {DIGIT_W}",
f"#define FONT_DIGIT_H {DIGIT_H}",
f"#define FONT_DIGIT_BYTES (FONT_DIGIT_W * FONT_DIGIT_H / 8)",
"",
f"#define FONT_COLON_W {COLON_W}",
f"#define FONT_COLON_H {COLON_H}",
f"#define FONT_COLON_BYTES (FONT_COLON_W * FONT_COLON_H / 8)",
"",
"extern const uint8_t font_digits[10][FONT_DIGIT_BYTES];",
"extern const uint8_t font_colon[FONT_COLON_BYTES];",
"",
"#endif",
"",
]
c_lines = [
'#include "fonts.h"',
"",
]
entry_bytes = DIGIT_W // 8
entries = []
for d in range(10):
data = pattern_to_bytes(digit_patterns[str(d)], DIGIT_W)
rows = []
for i in range(0, len(data), entry_bytes):
chunk = data[i : i + entry_bytes]
rows.append("\t\t" + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
entries.append("\t{\n" + "\n".join(rows) + "\n\t},")
total = DIGIT_W * DIGIT_H // 8
c_lines.append(f"const uint8_t font_digits[10][{total}] = {{")
c_lines.extend(entries)
c_lines.append("};")
c_lines.append("")
colon_data = pattern_to_bytes(colon_pat, COLON_W)
colon_total = COLON_W * COLON_H // 8
colon_rows = []
ceb = COLON_W // 8
for i in range(0, len(colon_data), ceb):
chunk = colon_data[i : i + ceb]
colon_rows.append("\t" + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
c_lines.append(f"const uint8_t font_colon[{colon_total}] = {{")
c_lines.extend(colon_rows)
c_lines.append("};")
c_lines.append("")
return "\n".join(h_lines), "\n".join(c_lines)
if __name__ == "__main__":
header, source = main()
with open("src/fonts.h", "w") as f:
f.write(header)
with open("src/fonts.c", "w") as f:
f.write(source)
print(f"Generated fonts.h ({len(header)} bytes) and fonts.c ({len(source)} bytes)")
print(f"Digit size: {DIGIT_W}x{DIGIT_H} = {DIGIT_W * DIGIT_H // 8} bytes each")
print(f"Colon size: {COLON_W}x{COLON_H} = {COLON_W * COLON_H // 8} bytes")