Files
firmware/justfile
T
swaits aca4c3cffd feat(t114): add Heltec Mesh Node T114 board support
Adds nRF52840 + SX1262 + 1.14" 240×135 ST7789 color TFT support. First
color display board in the project; introduces a BoardDisplay trait so
each board renders to its native pixel format and resolution without
aliasing or color-quantization round-trips.

Architecture:
- New BoardDisplay trait (src/display.rs) bundles render_*, present(),
  set_bright/set_dim. Replaces the old DrawTarget<Color=BinaryColor>
  + DisplayBrightness combo as the LoRaBoard::DisplayDriver bound.
- Mono OLED drivers (Sh1106, Ssd1306Async) get adapter impls that
  delegate to the existing display::render module — no behavior change
  for any existing board.
- T114 ST7789 driver (src/driver/st7789.rs) wraps lcd-async (async fork
  of mipidsi) with a 64,800-byte raw byte framebuffer in .bss. Renders
  via lcd_async::raw_framebuf::RawFrameBuf<Rgb565, _>, presents via a
  single async DMA blit (panel.show_raw_data(...).await) — the await
  yields the executor for the ~65 ms SPI transfer so the radio task
  isn't starved during repaints.
- PWM backlight on PWM0 / P0.15 (active-LOW, inverted polarity) for
  smooth dim/bright transitions instead of GPIO toggle.
- Native 240×135 Rgb565 renderer (src/display/render_color.rs) with
  splash + dashboard + sparkline.
- Shared QR-code blit (src/display/qr.rs), generic over PixelColor,
  re-centers the 64×64 bitmap which has cells in cols 0..50 and ~14 px
  of right-side padding — both mono and color splash now show the QR
  with 7 px quiet zones on all four sides (was visibly left-shifted).
- Shared content types (BoardInfo, DashboardCtx) decouple what to draw
  from how each board renders it.
- create_display() now takes the board's full DisplayParts bundle
  (was just the I2C bus) so non-I2C displays can carry SPI + control
  pins through.

Hardware:
- VEXT_ENABLE on P0.21 driven HIGH at boot — powers the SX1262 + TFT
  rail. Without this, lora-phy init succeeds at the SPI level but
  TX_DONE never fires.
- VTFT_CTRL on P0.03, active LOW; backlight on P0.15, active LOW.
- TFT SPI2 (SCK P1.08 / MOSI P1.09) at 8 MHz over EasyDMA. Async
  ExclusiveDevice from embedded-hal-bus (with the 'async' feature).
- lcd-async builder at display_size(135, 240), display_offset(52, 40),
  Deg90 rotation, ColorInversion::Inverted — standard 1.14" ST7789
  die mapping.
- Linker: shared ld/nrf52840-memory.x (S140 SoftDevice preserved at
  0x01000-0x26000 per project policy).
2026-04-30 03:11:36 -06:00

261 lines
12 KiB
Makefile

# Run every recipe with mise's tool versions active — so `just` invocations
# of cargo, espflash, rust-objcopy, etc. always resolve to the pinned
# versions in mise.toml, even if the user has a different version elsewhere
# on PATH (e.g. a stray `cargo install espflash`).
set shell := ["mise", "exec", "--", "sh", "-c"]
builds_dir := "builds"
version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml`
# All known boards. ADD A NEW BOARD HERE and in the `_info` case below.
#
# Board metadata ("feature target chip") lives in the `_info` recipe —
# Xtensa targets are built with `+esp` + `-Zbuild-std=core,alloc` (the
# self-contained esp toolchain ships its own rust-src; the public
# nightly drifts ahead and breaks core-stdlib compilation).
boards := "heltec_mesh_node_t114 heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 lilygo_tbeam lilygo_tbeam_supreme rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1"
# Install all required tools and toolchains
setup:
mise trust --yes
mise install
@# ESP Xtensa toolchain — installed via the `esp-install` mise task
@# (which pins versions to match mise.toml's [env] paths).
@if ! rustup toolchain list | grep -q "^esp"; then \
echo "ESP toolchain not found, installing via mise task (this may take a while)..."; \
mise run esp-install || { echo "error: esp-install task failed" >&2; exit 1; }; \
fi
# Build release firmware for all boards, installing toolchains as needed
build-all:
@trap 'exit 130' INT; \
for board in {{boards}}; do \
if just _can_build $board; then \
echo "── building $board ──"; \
just build $board || exit $?; \
else \
echo "── skipping $board (toolchain install failed) ──"; \
fi; \
done
# Check all boards compile, installing toolchains as needed
check-all:
@trap 'exit 130' INT; \
for board in {{boards}}; do \
if just _can_build $board; then \
echo "── checking $board ──"; \
just check $board || exit $?; \
else \
echo "── skipping $board (toolchain install failed) ──"; \
fi; \
done
# Check a single board compiles (clippy with warnings as errors)
check board:
@just _cargo {{board}} "clippy --release" "-- -D warnings"
# Build release firmware and copy to builds/ with a readable name.
# `extra_features` (comma-separated) is appended to the board feature,
# e.g. `just build wio_tracker_l1 release debug-checkpoint`.
build board profile="release" extra_features="":
@just _cargo {{board}} "build --{{profile}}" "" "{{extra_features}}"
@just _copy_firmware {{board}} {{profile}}
# Build and flash a board (espflash for Xtensa, UF2 DFU for ARM).
# `extra_features` is passed through to the build, e.g.
# `just flash wio_tracker_l1 debug-checkpoint`.
flash board extra_features="":
@just _ensure_tools
@just build {{board}} release "{{extra_features}}"
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
case "$target" in \
xtensa-*) \
port=$(just _find_port "303a:1001" "1209:5741" "10c4:ea60" "1a86:7522" "1a86:7523"); \
if [ -n "$port" ]; then \
echo "Using $port"; \
espflash flash -p "$port" "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"; \
else \
echo "No port found, falling back to espflash auto-detection..." >&2; \
espflash flash "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"; \
fi ;; \
*) just _flash_uf2 {{board}} ;; \
esac
# Flash ARM board via debug probe (requires J-Link or similar).
# `extra_features` is passed through to the build.
# Sets DEFMT_LOG at build time so defmt::info!/warn!/error! are actually
# compiled in (default filter is ERROR-only — strips info logs).
# `embassy_usb=error` silences the benign Linux modem-manager
# `SET_INTERFACE: alt setting out of range` WARN that fires once per
# enumeration; it's a normal STALL response (correct USB behavior),
# the WARN is just embassy_usb noise. Other crates inherit the `info`
# default so chart traces, our own info!/warn!/error! all still fire.
flash-probe board extra_features="":
@just _ensure_tools
@DEFMT_LOG=info,embassy_usb=error just build {{board}} release "{{extra_features}}"
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"
# UF2-flash via the bootloader, then probe-rs attach for RTT log streaming.
# Required on boards (e.g. RAK4631) where the bootloader gates the app
# behind a "valid app flag" in its settings page that only the bootloader
# itself can set — `probe-rs run`'s direct SWD writes bypass that flag and
# leave the bootloader stuck in DFU mode. UF2 path goes through the
# bootloader, sets the flag, then we attach for RTT (no reset, no reflash).
flash-attach board extra_features="":
@just _ensure_tools
@DEFMT_LOG=info,embassy_usb=error just build {{board}} release "{{extra_features}}"
@just _flash_uf2 {{board}}
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
sleep 1; \
probe-rs attach --chip $chip --no-catch-reset \
"{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"
# Attach probe-rs for live RTT/defmt streaming WITHOUT flashing or
# resetting the chip. Logs are tee'd to a UTC-timestamped file in /tmp
# so soak captures don't clobber each other across runs. Use this with
# the dongle already running the desired firmware (flash via `just flash`
# or `just flash-attach` first).
attach board:
@just _ensure_tools
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
log="/tmp/donglora-soak-$(date -u +%Y%m%dT%H%M%SZ).log"; \
echo "Logging to $log"; \
probe-rs attach --chip $chip --no-catch-reset \
"{{builds_dir}}/donglora-{{board}}-v{{version}}.elf" 2>&1 \
| tee "$log"
# Show binary size for a release build
size board:
@just _cargo {{board}} "size --release"
# Run host-side protocol unit tests
test:
DONGLORA_HOST_TEST=1 cargo test
# Check for outdated dependencies
outdated:
cargo outdated
# ── Private helpers ───────────────────────────────────────────────────
# Install mise-managed tools if any are missing (fast no-op when current)
[private]
_ensure_tools:
@mise trust --yes . 2>/dev/null; mise install --quiet
# Run a cargo command for a board, optionally with extra Cargo features
# (comma-separated, e.g. "debug-checkpoint").
# Xtensa: esp toolchain (cargo + rustc + rust-src together) + -Zbuild-std=core
[private]
_cargo board cmd extra_args="" extra_features="":
@just _ensure_tools
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
[ -n "{{extra_features}}" ] && feat="$feat,{{extra_features}}"; \
extra=""; buildstd=""; \
case "$target" in xtensa-*) \
just _require_esp_toolchain; \
extra="+esp"; buildstd="-Zbuild-std=core,alloc";; \
esac; \
cargo $extra {{cmd}} --target "$target" --features "$feat" $buildstd {{extra_args}}
# Ensure a board's toolchain is available, auto-installing if needed
[private]
_can_build board:
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
case "$target" in \
xtensa-*) just _require_esp_toolchain ;; \
*) rustup target list --installed | grep -q "^$target$" || rustup target add "$target" ;; \
esac
# Ensure the ESP Xtensa toolchain is installed. The actual install is
# delegated to the `esp-install` mise task so the version pin lives in
# mise.toml alongside the matching [env] PATH/LIBCLANG_PATH entries.
# The esp toolchain ships its own cargo, rustc, and rust-src — no extra
# components are needed on top.
[private]
_require_esp_toolchain:
@just _ensure_tools
@if ! rustup toolchain list | grep -q "^esp"; then \
echo "ESP toolchain not found, installing via mise task (this may take a while)..." >&2; \
mise run esp-install || { echo "error: esp-install task failed" >&2; exit 1; }; \
fi
# Find a serial port matching any of the given VID:PID pairs (checked in order)
[private]
_find_port +vid_pids:
@for vidpid in {{vid_pids}}; do \
vid="${vidpid%%:*}"; pid="${vidpid##*:}"; \
for dev in /dev/ttyACM* /dev/ttyUSB*; do \
[ -e "$dev" ] || continue; \
info=$(udevadm info --query=property --name="$dev" 2>/dev/null) || continue; \
dev_vid=$(echo "$info" | sed -n 's/^ID_VENDOR_ID=//p' | tr '[:upper:]' '[:lower:]'); \
dev_pid=$(echo "$info" | sed -n 's/^ID_MODEL_ID=//p' | tr '[:upper:]' '[:lower:]'); \
if [ "$dev_vid" = "$vid" ] && [ "$dev_pid" = "$pid" ]; then \
echo "$dev"; exit 0; \
fi; \
done; \
done
[private]
_copy_firmware board profile:
@mkdir -p {{builds_dir}}
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
src="target/$target/{{profile}}/donglora"; \
dst="{{builds_dir}}/donglora-{{board}}-v{{version}}"; \
if [ -f "$src" ]; then \
cp "$src" "$dst.elf"; echo "→ $dst.elf"; \
case "$target" in \
xtensa-*) \
espflash save-image --chip "$chip" --merge --skip-padding "$src" "$dst.bin"; \
echo "→ $dst.bin"; \
;; \
thumbv6m-*) \
rust-objcopy -O ihex "$src" "$dst.hex"; \
cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family rp2040; \
rm "$dst.hex"; \
echo "→ $dst.uf2"; \
;; \
thumbv7em-*) \
rust-objcopy -O ihex "$src" "$dst.hex"; \
cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family nrf52840; \
rm "$dst.hex"; \
echo "→ $dst.uf2"; \
;; \
esac; \
fi
# Copy UF2 to a mounted UF2 bootloader drive
[private]
_flash_uf2 board:
@uf2="{{builds_dir}}/donglora-{{board}}-v{{version}}.uf2"; \
if [ ! -f "$uf2" ]; then \
echo "error: $uf2 not found — run 'just build {{board}}' first" >&2; exit 1; \
fi; \
mnt="$(find /run/media -maxdepth 3 -name INFO_UF2.TXT -printf '%h\n' -quit 2>/dev/null)"; \
if [ -z "$mnt" ]; then \
echo "error: no UF2 drive found — double-tap reset to enter bootloader" >&2; exit 1; \
fi; \
echo "Copying to $mnt ..."; \
cp "$uf2" "$mnt/"; \
sync; \
echo "Done — board will reboot automatically"
# Single source of truth for per-board metadata: "feature target chip".
# ADD A NEW BOARD HERE and in the `boards` list near the top of this file.
[private]
_info name:
@case "{{name}}" in \
heltec_mesh_node_t114) echo "heltec_mesh_node_t114 thumbv7em-none-eabihf nRF52840_xxAA" ;; \
heltec_v3) echo "heltec_v3 xtensa-esp32s3-none-elf esp32s3" ;; \
heltec_v3_uart) echo "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3" ;; \
heltec_v4) echo "heltec_v4 xtensa-esp32s3-none-elf esp32s3" ;; \
elecrow_thinknode_m2) echo "elecrow_thinknode_m2 xtensa-esp32s3-none-elf esp32s3" ;; \
lilygo_tbeam) echo "lilygo_tbeam xtensa-esp32-none-elf esp32" ;; \
lilygo_tbeam_supreme) echo "lilygo_tbeam_supreme xtensa-esp32s3-none-elf esp32s3" ;; \
rak_wisblock_4631) echo "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA" ;; \
waveshare_rp2040_lora) echo "waveshare_rp2040_lora thumbv6m-none-eabi rp2040" ;; \
wio_tracker_l1) echo "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA" ;; \
*) echo "Unknown board: {{name}}" >&2; exit 1 ;; \
esac