redo
This commit is contained in:
@@ -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)
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
''
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,2 +0,0 @@
|
||||
CONFIG_ZTEST=y
|
||||
CONFIG_MAIN_STACK_SIZE=8192
|
||||
@@ -1,7 +0,0 @@
|
||||
sample:
|
||||
name: nrfwarch tests
|
||||
tests:
|
||||
test:
|
||||
build_only: false
|
||||
platform_allow: qemu_x86
|
||||
tags: testing
|
||||
@@ -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);
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user