diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index ab209e7..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -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) diff --git a/README.md b/README.md deleted file mode 100644 index 29bcf77..0000000 --- a/README.md +++ /dev/null @@ -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 diff --git a/boards/xiao_nrf54l15_nrf54l15_cpuapp.conf b/boards/xiao_nrf54l15_nrf54l15_cpuapp.conf deleted file mode 100644 index 6c6bdcf..0000000 --- a/boards/xiao_nrf54l15_nrf54l15_cpuapp.conf +++ /dev/null @@ -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 diff --git a/boards/xiao_nrf54l15_nrf54l15_cpuapp.overlay b/boards/xiao_nrf54l15_nrf54l15_cpuapp.overlay deleted file mode 100644 index 0ab5588..0000000 --- a/boards/xiao_nrf54l15_nrf54l15_cpuapp.overlay +++ /dev/null @@ -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 = ; -}; diff --git a/developmentlog.md b/developmentlog.md deleted file mode 100644 index cff03b2..0000000 --- a/developmentlog.md +++ /dev/null @@ -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) diff --git a/dts/bindings/nrfwarch,epd-ctrl.yaml b/dts/bindings/nrfwarch,epd-ctrl.yaml deleted file mode 100644 index b27e2b9..0000000 --- a/dts/bindings/nrfwarch,epd-ctrl.yaml +++ /dev/null @@ -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 diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 3f06416..0000000 --- a/flake.lock +++ /dev/null @@ -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 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 1fd6887..0000000 --- a/flake.nix +++ /dev/null @@ -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 - ) - '' - ); - }; - }; - } - ); -} diff --git a/prj.conf b/prj.conf deleted file mode 100644 index 9c61e3a..0000000 --- a/prj.conf +++ /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 diff --git a/sample.yaml b/sample.yaml deleted file mode 100644 index 859eefa..0000000 --- a/sample.yaml +++ /dev/null @@ -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 diff --git a/sim/CMakeLists.txt b/sim/CMakeLists.txt deleted file mode 100644 index eacde5c..0000000 --- a/sim/CMakeLists.txt +++ /dev/null @@ -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) diff --git a/sim/prj.conf b/sim/prj.conf deleted file mode 100644 index c8063ff..0000000 --- a/sim/prj.conf +++ /dev/null @@ -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 diff --git a/sim/src/epd_sim.c b/sim/src/epd_sim.c deleted file mode 100644 index 6cc0a77..0000000 --- a/sim/src/epd_sim.c +++ /dev/null @@ -1,69 +0,0 @@ -#include "epd_drv.h" - -#include -#include - -#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"); -} diff --git a/sim/src/main.c b/sim/src/main.c deleted file mode 100644 index e908c01..0000000 --- a/sim/src/main.c +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include - -#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; - } - } - } -} diff --git a/sim/view.py b/sim/view.py deleted file mode 100644 index df36f76..0000000 --- a/sim/view.py +++ /dev/null @@ -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() diff --git a/src/epd_drv.c b/src/epd_drv.c deleted file mode 100644 index 26c9b8d..0000000 --- a/src/epd_drv.c +++ /dev/null @@ -1,246 +0,0 @@ -#include "epd_drv.h" - -#include -#include -#include -#include -#include -#include - -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(); -} diff --git a/src/epd_drv.h b/src/epd_drv.h deleted file mode 100644 index dd8895d..0000000 --- a/src/epd_drv.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef EPD_DRV_H -#define EPD_DRV_H - -#include -#include - -#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 diff --git a/src/fonts.c b/src/fonts.c deleted file mode 100644 index 4a07384..0000000 --- a/src/fonts.c +++ /dev/null @@ -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, -}; diff --git a/src/fonts.h b/src/fonts.h deleted file mode 100644 index 44d686b..0000000 --- a/src/fonts.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FONTS_H -#define FONTS_H - -#include -#include - -#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 diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 8c0908e..0000000 --- a/src/main.c +++ /dev/null @@ -1,52 +0,0 @@ -#include -#include - -#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); - } -} diff --git a/src/watchface.c b/src/watchface.c deleted file mode 100644 index d683437..0000000 --- a/src/watchface.c +++ /dev/null @@ -1,115 +0,0 @@ -#include "watchface.h" -#include "fonts.h" -#include - -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; -} diff --git a/src/watchface.h b/src/watchface.h deleted file mode 100644 index 4669da2..0000000 --- a/src/watchface.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef WATCHFACE_H -#define WATCHFACE_H - -#include - -#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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt deleted file mode 100644 index 470c5f5..0000000 --- a/tests/CMakeLists.txt +++ /dev/null @@ -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) diff --git a/tests/prj.conf b/tests/prj.conf deleted file mode 100644 index cdca1e9..0000000 --- a/tests/prj.conf +++ /dev/null @@ -1,2 +0,0 @@ -CONFIG_ZTEST=y -CONFIG_MAIN_STACK_SIZE=8192 diff --git a/tests/sample.yaml b/tests/sample.yaml deleted file mode 100644 index 9e959cc..0000000 --- a/tests/sample.yaml +++ /dev/null @@ -1,7 +0,0 @@ -sample: - name: nrfwarch tests -tests: - test: - build_only: false - platform_allow: qemu_x86 - tags: testing diff --git a/tests/src/main.c b/tests/src/main.c deleted file mode 100644 index efbb3e1..0000000 --- a/tests/src/main.c +++ /dev/null @@ -1,217 +0,0 @@ -#include -#include -#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); diff --git a/tools/gen_fonts.py b/tools/gen_fonts.py deleted file mode 100644 index 44a080d..0000000 --- a/tools/gen_fonts.py +++ /dev/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 ", - "#include ", - "", - 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")