A nightstand noise generator based on M5Stack Atom Echo and integrating with Home Assistant
1# Firmware build entry points.
2#
3# Wraps cargo with the env tweaks needed on hosts where libxml2 has bumped
4# past .so.2 (Ubuntu Questing 25.10+ ships .so.16). The .lib/libxml2.so.2
5# symlink in this directory points at the system library so esp-clang loads.
6#
7# Cargo handles its own incremental rebuilds, so these are all phony.
8
9LIB_DIR := $(CURDIR)/.lib
10LIBXML2_COMPAT := $(LIB_DIR)/libxml2.so.2
11SYSTEM_LIBXML2 := $(firstword $(wildcard /usr/lib/x86_64-linux-gnu/libxml2.so.16 /usr/lib/libxml2.so.16))
12
13export LD_LIBRARY_PATH := $(LIB_DIR):$(LD_LIBRARY_PATH)
14
15# Inject the absolute path to partitions.csv via a generated overlay file.
16# ESP-IDF resolves relative CONFIG_PARTITION_TABLE_FILENAME against an
17# internal CMake source dir under target/, not against this directory, so
18# baking a relative path into the committed sdkconfig.defaults would fail.
19# Generating the overlay here keeps the committed config portable.
20GEN_DIR := $(CURDIR)/target/gen
21GEN_PARTITIONS_SDKCONFIG := $(GEN_DIR)/sdkconfig.defaults.partitions
22PARTITIONS_CSV := $(CURDIR)/partitions.csv
23
24# esp-idf-sys reads ESP_IDF_SDKCONFIG_DEFAULTS as the list of project sdkconfig
25# defaults files (it then internally builds its own SDKCONFIG_DEFAULTS env var
26# for cmake by prepending its generated defaults).
27export ESP_IDF_SDKCONFIG_DEFAULTS := $(CURDIR)/sdkconfig.defaults;$(GEN_PARTITIONS_SDKCONFIG)
28
29CARGO ?= cargo
30BIN := target/xtensa-esp32-espidf/release/sound-machine
31BOOTLOADER_BIN := target/xtensa-esp32-espidf/release/bootloader.bin
32# First /dev/ttyUSB* / /dev/ttyACM* found, used for headless flashing.
33# Override on the command line: `make flash PORT=/dev/ttyUSB1`
34PORT ?= $(firstword $(wildcard /dev/ttyUSB* /dev/ttyACM*))
35
36# Firmware version, parsed from Cargo.toml. Used for the OTA filename and
37# the latest_version MQTT publish.
38VERSION := $(shell sed -nE '0,/^version *= *"([^"]+)"/{s//\1/p}' Cargo.toml)
39OTA_BIN := sound-machine-$(VERSION).bin
40
41.PHONY: build check flash flash-monitor monitor clean help ota-publish $(LIBXML2_COMPAT)
42
43build: $(LIBXML2_COMPAT) $(GEN_PARTITIONS_SDKCONFIG)
44 $(CARGO) build --release
45
46check: $(LIBXML2_COMPAT) $(GEN_PARTITIONS_SDKCONFIG)
47 $(CARGO) check
48
49# Headless flash — builds, then writes to the device with no interactive
50# monitor. Safe to run from any shell, including non-TTY contexts. Auto-
51# detects the first ttyUSB*/ttyACM*; override with PORT=/dev/...
52flash: build
53 @if [ -z "$(PORT)" ]; then \
54 echo "no serial port found (looked for /dev/ttyUSB* and /dev/ttyACM*)"; \
55 exit 1; \
56 fi
57 espflash flash --port $(PORT) \
58 --bootloader $(BOOTLOADER_BIN) \
59 --partition-table $(PARTITIONS_CSV) \
60 --erase-parts otadata \
61 $(BIN)
62
63# Interactive flash + monitor — builds, flashes, attaches the serial monitor.
64# Needs a TTY (the monitor writes to terminal and reads keyboard input).
65flash-monitor: $(LIBXML2_COMPAT) $(GEN_PARTITIONS_SDKCONFIG)
66 $(CARGO) run --release
67
68monitor:
69 espflash monitor
70
71# Publish a new firmware version. Reads OTA_LOCAL_DIR / OTA_URL_BASE / MQTT_URL
72# from the environment (sourced via direnv from .envrc.private). Builds the
73# release binary, generates the flat .bin via espflash save-image, copies it
74# into the static-HTTP serve directory, then publishes the new latest_version
75# retained to the shared MQTT topic. Both nightstands' HA update cards light
76# up the moment the publish lands.
77ota-publish: build
78 @if [ -z "$(OTA_LOCAL_DIR)" ]; then \
79 echo "OTA_LOCAL_DIR is not set (see .envrc.private.example)"; exit 1; \
80 fi
81 @if [ -z "$(OTA_URL_BASE)" ]; then \
82 echo "OTA_URL_BASE is not set (see .envrc.private.example)"; exit 1; \
83 fi
84 @if [ -z "$(MQTT_URL)" ]; then \
85 echo "MQTT_URL is not set (see .envrc.private.example)"; exit 1; \
86 fi
87 @if [ -z "$(VERSION)" ]; then \
88 echo "could not parse version from Cargo.toml"; exit 1; \
89 fi
90 @echo "publishing v$(VERSION) → $(OTA_URL_BASE)/$(OTA_BIN)"
91 espflash save-image --chip esp32 --flash-size 4mb $(BIN) $(OTA_LOCAL_DIR)/$(OTA_BIN)
92 mosquitto_pub \
93 -L "$(MQTT_URL)/sound-machine/firmware/latest" \
94 -r \
95 -m "$(VERSION)"
96 @echo "done. devices will see the update in HA within a few seconds."
97
98clean:
99 $(CARGO) clean
100
101# The compat symlink. Created on demand if the system has libxml2.so.16.
102# Skipped silently if it already exists or the system has a newer libxml2.
103$(LIBXML2_COMPAT):
104 @if [ ! -e "$@" ]; then \
105 if [ -n "$(SYSTEM_LIBXML2)" ]; then \
106 mkdir -p $(LIB_DIR); \
107 ln -sf $(SYSTEM_LIBXML2) $@; \
108 echo "linked $@ -> $(SYSTEM_LIBXML2)"; \
109 else \
110 echo "warning: no system libxml2.so.16 found; esp-clang may fail to load"; \
111 fi; \
112 fi
113
114# Generated sdkconfig overlay carrying the absolute path to partitions.csv.
115# Regenerated whenever the CSV or this Makefile changes.
116$(GEN_PARTITIONS_SDKCONFIG): $(PARTITIONS_CSV) $(MAKEFILE_LIST)
117 @mkdir -p $(GEN_DIR)
118 @printf 'CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="%s"\nCONFIG_PARTITION_TABLE_FILENAME="%s"\n' \
119 '$(PARTITIONS_CSV)' '$(PARTITIONS_CSV)' > $@
120 @echo "generated $@"
121
122help:
123 @echo "firmware targets:"
124 @echo " build cargo build --release (default)"
125 @echo " check cargo check"
126 @echo " flash headless: build + flash, no monitor (works in any shell)"
127 @echo " flash-monitor interactive: build + flash + serial monitor (needs a TTY)"
128 @echo " monitor espflash monitor only"
129 @echo " ota-publish save .bin to \$$OTA_LOCAL_DIR + publish latest_version to MQTT"
130 @echo " clean cargo clean"