# Firmware build entry points. # # Wraps cargo with the env tweaks needed on hosts where libxml2 has bumped # past .so.2 (Ubuntu Questing 25.10+ ships .so.16). The .lib/libxml2.so.2 # symlink in this directory points at the system library so esp-clang loads. # # Cargo handles its own incremental rebuilds, so these are all phony. LIB_DIR := $(CURDIR)/.lib LIBXML2_COMPAT := $(LIB_DIR)/libxml2.so.2 SYSTEM_LIBXML2 := $(firstword $(wildcard /usr/lib/x86_64-linux-gnu/libxml2.so.16 /usr/lib/libxml2.so.16)) export LD_LIBRARY_PATH := $(LIB_DIR):$(LD_LIBRARY_PATH) # Inject the absolute path to partitions.csv via a generated overlay file. # ESP-IDF resolves relative CONFIG_PARTITION_TABLE_FILENAME against an # internal CMake source dir under target/, not against this directory, so # baking a relative path into the committed sdkconfig.defaults would fail. # Generating the overlay here keeps the committed config portable. GEN_DIR := $(CURDIR)/target/gen GEN_PARTITIONS_SDKCONFIG := $(GEN_DIR)/sdkconfig.defaults.partitions PARTITIONS_CSV := $(CURDIR)/partitions.csv # esp-idf-sys reads ESP_IDF_SDKCONFIG_DEFAULTS as the list of project sdkconfig # defaults files (it then internally builds its own SDKCONFIG_DEFAULTS env var # for cmake by prepending its generated defaults). export ESP_IDF_SDKCONFIG_DEFAULTS := $(CURDIR)/sdkconfig.defaults;$(GEN_PARTITIONS_SDKCONFIG) CARGO ?= cargo BIN := target/xtensa-esp32-espidf/release/sound-machine BOOTLOADER_BIN := target/xtensa-esp32-espidf/release/bootloader.bin # First /dev/ttyUSB* / /dev/ttyACM* found, used for headless flashing. # Override on the command line: `make flash PORT=/dev/ttyUSB1` PORT ?= $(firstword $(wildcard /dev/ttyUSB* /dev/ttyACM*)) # Firmware version, parsed from Cargo.toml. Used for the OTA filename and # the latest_version MQTT publish. VERSION := $(shell sed -nE '0,/^version *= *"([^"]+)"/{s//\1/p}' Cargo.toml) OTA_BIN := sound-machine-$(VERSION).bin .PHONY: build check flash flash-monitor monitor clean help ota-publish $(LIBXML2_COMPAT) build: $(LIBXML2_COMPAT) $(GEN_PARTITIONS_SDKCONFIG) $(CARGO) build --release check: $(LIBXML2_COMPAT) $(GEN_PARTITIONS_SDKCONFIG) $(CARGO) check # Headless flash — builds, then writes to the device with no interactive # monitor. Safe to run from any shell, including non-TTY contexts. Auto- # detects the first ttyUSB*/ttyACM*; override with PORT=/dev/... flash: build @if [ -z "$(PORT)" ]; then \ echo "no serial port found (looked for /dev/ttyUSB* and /dev/ttyACM*)"; \ exit 1; \ fi espflash flash --port $(PORT) \ --bootloader $(BOOTLOADER_BIN) \ --partition-table $(PARTITIONS_CSV) \ --erase-parts otadata \ $(BIN) # Interactive flash + monitor — builds, flashes, attaches the serial monitor. # Needs a TTY (the monitor writes to terminal and reads keyboard input). flash-monitor: $(LIBXML2_COMPAT) $(GEN_PARTITIONS_SDKCONFIG) $(CARGO) run --release monitor: espflash monitor # Publish a new firmware version. Reads OTA_LOCAL_DIR / OTA_URL_BASE / MQTT_URL # from the environment (sourced via direnv from .envrc.private). Builds the # release binary, generates the flat .bin via espflash save-image, copies it # into the static-HTTP serve directory, then publishes the new latest_version # retained to the shared MQTT topic. Both nightstands' HA update cards light # up the moment the publish lands. ota-publish: build @if [ -z "$(OTA_LOCAL_DIR)" ]; then \ echo "OTA_LOCAL_DIR is not set (see .envrc.private.example)"; exit 1; \ fi @if [ -z "$(OTA_URL_BASE)" ]; then \ echo "OTA_URL_BASE is not set (see .envrc.private.example)"; exit 1; \ fi @if [ -z "$(MQTT_URL)" ]; then \ echo "MQTT_URL is not set (see .envrc.private.example)"; exit 1; \ fi @if [ -z "$(VERSION)" ]; then \ echo "could not parse version from Cargo.toml"; exit 1; \ fi @echo "publishing v$(VERSION) → $(OTA_URL_BASE)/$(OTA_BIN)" espflash save-image --chip esp32 --flash-size 4mb $(BIN) $(OTA_LOCAL_DIR)/$(OTA_BIN) mosquitto_pub \ -L "$(MQTT_URL)/sound-machine/firmware/latest" \ -r \ -m "$(VERSION)" @echo "done. devices will see the update in HA within a few seconds." clean: $(CARGO) clean # The compat symlink. Created on demand if the system has libxml2.so.16. # Skipped silently if it already exists or the system has a newer libxml2. $(LIBXML2_COMPAT): @if [ ! -e "$@" ]; then \ if [ -n "$(SYSTEM_LIBXML2)" ]; then \ mkdir -p $(LIB_DIR); \ ln -sf $(SYSTEM_LIBXML2) $@; \ echo "linked $@ -> $(SYSTEM_LIBXML2)"; \ else \ echo "warning: no system libxml2.so.16 found; esp-clang may fail to load"; \ fi; \ fi # Generated sdkconfig overlay carrying the absolute path to partitions.csv. # Regenerated whenever the CSV or this Makefile changes. $(GEN_PARTITIONS_SDKCONFIG): $(PARTITIONS_CSV) $(MAKEFILE_LIST) @mkdir -p $(GEN_DIR) @printf 'CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="%s"\nCONFIG_PARTITION_TABLE_FILENAME="%s"\n' \ '$(PARTITIONS_CSV)' '$(PARTITIONS_CSV)' > $@ @echo "generated $@" help: @echo "firmware targets:" @echo " build cargo build --release (default)" @echo " check cargo check" @echo " flash headless: build + flash, no monitor (works in any shell)" @echo " flash-monitor interactive: build + flash + serial monitor (needs a TTY)" @echo " monitor espflash monitor only" @echo " ota-publish save .bin to \$$OTA_LOCAL_DIR + publish latest_version to MQTT" @echo " clean cargo clean"