A wired-Ethernet ESP32 that listens for DoorBird notification broadcasts on the LAN, decrypts them, and plays a chime through an I2S DAC. Events are optionally republished to MQTT. It is configured through a small web interface and needs no server, broker or bridge to work.
It is a passive listener — it does not connect to the DoorBird's video/audio stream and does not poll it. It only receives the UDP broadcast the DoorBird already sends to the whole network when the bell is pressed or motion is detected.
The doorbell used to be a chain of services:
DoorBird ──UDP──▶ Node-RED (Raspberry Pi) ──HTTP──▶ web service (mini PC)
│
PipeWire
▼
ceiling speakers
Node-RED on a Raspberry Pi parsed the notification packet and fired an HTTP request at a small web service on a mini PC, which handed the sound to PipeWire, which played it on the ceiling speakers in the living/dining area. The request named the sink, so that part at least could not drift.
It worked, but the doorbell only rang if both machines were up and Node-RED had not been left in a half-deployed state. A doorbell that depends on two general purpose computers, each rebooting for updates on its own schedule, is a doorbell that eventually does not ring — and you find out when somebody has been standing outside for a minute.
So the whole chain got replaced by one microcontroller:
DoorBird ──UDP──▶ WT32-ETH01 ──I2S──▶ DAC ──line level──┐
├──▶ power amp ──▶ ceiling speakers
Snapcast client ──line level─────┘
The board decrypts the packet itself and plays a WAV from its own flash. Its analogue output is mixed into the Snapcast multiroom stream ahead of the power amp, so the chime comes out of the same ceiling speakers as the music without any of the software in between. What is left in the failure path is the DoorBird, a network switch, and a board with no moving parts and no operating system to update.
Two consequences of mixing at line level are worth knowing up front: the chime is added to whatever is playing rather than replacing it (there is no ducking), and the chime keeps working when the Snapcast client is off, because it does not travel through it.
Why not put a second amplifier on the speakers? Because two power amplifier outputs must never share a speaker. The idle amp's output stage is a low-impedance voltage source and acts as a near short across the active one. With a class-D chip like the MAX98357A it is worse still: its output is bridge-tied, so neither terminal is ground and there is no safe common point. Mix at line level, or switch the speaker between amps with a relay.
When an event occurs, the DoorBird sends encrypted UDP broadcasts on ports 6524 and 35344 (plus a keep-alive every ~7 s, which is ignored). This firmware implements the current "v2" notification scheme:
- Packet layout (46 bytes):
IDENT(3) = DE AD BE·VERSION(1) = 0x02·NONCE(8)·CIPHERTEXT(34) - Decryption: ChaCha20-Poly1305, the original libsodium construction
(
crypto_aead_chacha20poly1305, 8-byte nonce, no additional data) — not the IETF/RFC-8439 variant. mbedtls'chachapolyis IETF-only and will not work; the AEAD is assembled by hand from therweather/Cryptoprimitives insrc/DoorbirdCrypto.cpp. - Decrypted payload (18 bytes):
INTERCOM_ID(6)·EVENT(8, space-padded)·TIMESTAMP(4, big-endian)
The deprecated v1 scheme (Argon2i + ChaCha20) is intentionally not implemented; it can be switched off in the DoorBird app.
Decryption needs the NOTIFICATION_ENCRYPTION_KEY from the DoorBird API. You fetch it once.
-
In the DoorBird app, go to Administration → User and pick (or create) a user. Give it API permission — the setting is called "API-Operator" on most firmware versions. Note its user name and password. The user name looks like
abcdef0001; its first six characters are theINTERCOM_IDyou can optionally filter on. -
Ask the device for a session:
curl -u USER:PASSWORD http://<doorbird-ip>/bha-api/getsession.cgi{ "BHA": { "RETURNCODE": "1", "SESSIONID": "ISXA9DkAJTNIS9pkdRfP", "NOTIFICATION_ENCRYPTION_KEY": "BHYGHyRKtGzBjku2t2jX2UKidXYQ3VqmfbKoCtxXJ6O4lgSzpgIwZ6onrSh" } } -
Paste
NOTIFICATION_ENCRYPTION_KEYinto the DoorBird card of the web interface.
Notes:
- The key stays valid until that user's password changes — it is not tied to the session ID and does not expire on its own.
- Only the first 32 bytes are used, as raw ASCII. Do not base64- or hex-decode it, and do not trim it — paste the string exactly as returned.
- If
getsession.cgireturns401, the user exists but lacks API permission. - The key is stored in NVS on the device and is never sent back by the web interface (the field shows it because it is not a password field; treat a shared screenshot accordingly).
EVENT is the doorbell button number as a string (1 for the first call button, 2 for the
second, …) or the literal motion for the motion detector. The Gong bei Ereignissen field
takes a comma-separated list; blank means "every event except motion". motion never rings
the chime regardless of what you put in that field.
One firmware, two hardware variants. The board-specific parts are small enough to live in
#if CONFIG_IDF_TARGET_ESP32P4 branches in src/config.h — everything else is identical.
| WT32-ETH01 | Waveshare ESP32-P4 Ethernet | |
|---|---|---|
| MCU | ESP32-D0WD (Xtensa) | ESP32-P4 (RISC-V) |
| PHY | LAN8720, external 50 MHz oscillator | on-board, already described by the core variant |
| Flash | 4 MB | 16 MB |
| App slot used | 75 % of 1.5 MB | 22 % of 4 MB |
| Ring tone space | 896 KB (~27 s) | ~7.9 MB (~4 min) |
| Audio | external I2S DAC | external I2S DAC, pins set in platformio.ini |
| PlatformIO env | wt32eth01 |
p4eth |
| Tested by the author | yes | no |
The Ethernet side needs no configuration at all: the stock esp32p4 variant already defines
the PHY type, address, MDC/MDIO, power pin and clock mode, so EthLink calls the no-argument
ETH.begin(). Those values are identical to the ones in the core's waveshare_p4_poe_eth
variant, which is why the same build should suit the PoE and non-PoE boards.
The I2S pins are the one thing you must set yourself. Waveshare ships several P4 boards with
Ethernet and they do not break the same GPIOs out to the header, so hard-coding a guess would be
worse than useless. They live in platformio.ini under [p4common]:
-DI2S_BCLK_PIN=20
-DI2S_LRC_PIN=21
-DI2S_DOUT_PIN=22Those are placeholders, not measured. Ethernet already occupies GPIO 28, 29, 30, 31, 34, 35, 49, 50, 51 and 52 — pick from what is left on your board and check the header before wiring. Removing the flags makes the build fail with a clear message rather than silently using wrong pins.
The P4 variant has never run on hardware. It compiles and the Ethernet wiring comes from the Arduino core rather than from guesswork, but nobody has booted it. Treat it as a starting point. PlatformIO also has no board definition for these boards, so a generic P4 target (
esp32-p4_r3) is used.If you have the PoE version, note that it carries an ES8311 audio codec on GPIO 10/11/12/13 with its amplifier on
PA_POWER(GPIO 53). A codec is not a DAC — it needs I2C register setup before it makes any sound, and that is not implemented here. An external I2S DAC works on either board.
A WT32-ETH01 (ESP32-D0WD + LAN8720 PHY, 4 MB flash). Wired Ethernet is the point: no WiFi credentials to expire, no roaming, no radio next to the audio wiring. The firmware switches the radio off at boot.
The RMII bus to the PHY occupies GPIO 0, 16, 18, 19, 21, 22, 23, 25, 26 and 27 — including the pins a plain ESP32 board would use for I2S. What is left on the header is GPIO 4, 14, 17, 32, 33 as free outputs, GPIO 35/36/39 as inputs, and the strapping pins GPIO 2/5/12/15, which are better left alone.
The board has no USB bridge: flashing needs an external 3.3 V USB-TTL adapter. See Build & flash.
Power: budget ≥1 A at 5 V. The PHY draws noticeably more than a bare ESP32, and an amplifier peaks on top of that. A weak supply shows up as a PHY that never links up.
The board runs warm — 45–50 °C on the module is normal and well inside the ESP32's ratings, but
two settings in src/config.h shave a few degrees off it because nothing here needs the speed:
CPU_FREQ_MHZis 80 instead of the default 240, which saves roughly 100–130 mW. It is the lowest value that keeps the CPU on the PLL and therefore the APB clock — and every peripheral's timing — unchanged. Audio and Ethernet are unaffected either way: I2S clocks offPLL_F160M, and the RMII reference clock comes from the board's own oscillator.LOOP_IDLE_MSis 1, so the polling loop yields and the core can halt between interrupts instead of spinning. Worth about as much again, at the price of up to 1 ms before a datagram is picked up.
If you want it cooler still, look at the supply before the software: feeding 3.3 V directly skips the on-board regulator, which otherwise burns 1.7 V times the whole board current right next to everything else. Standoffs and an open or vented case beat any of this.
Both of these are wired the same way and need no firmware change — pick by where the audio goes.
| PCM5102A | MAX98357A | |
|---|---|---|
| Output | line level, ~2.1 Vrms | 3.2 W class-D, straight to a speaker |
| Use it when | feeding a mixer, an amplifier input or an aux input | driving a small speaker on its own |
| Needs MCLK | no (internal PLL) | no |
| Volume control | none — the firmware attenuates in software | none, GAIN is a hardware strap |
For the setup described above — mixing into an existing line signal — it is the PCM5102A. The cheap "GY-PCM5102" breakout boards work fine. The MAX98357A is the right part only if the chime gets its own speaker; it cannot feed a line input.
Do not use the ESP32's built-in DAC. It is 8 bit and noisy.
| PCM5102A | WT32-ETH01 | Note |
|---|---|---|
| VIN | 3V3 |
3.3 V; some boards accept 5 V, check the silkscreen |
| GND | GND |
|
| BCK | IO14 |
bit clock |
| LCK | IO32 |
word select — silkscreen says CFG |
| DIN | IO33 |
data — silkscreen says 485_EN |
| SCK | GND |
must be tied low, this enables the internal PLL |
| FLT | GND |
normal latency filter |
| DEMP | GND |
de-emphasis off |
| XSMT | 3V3 |
un-mute; low keeps the output silent |
| FMT | GND |
I2S format |
SCK floating is the classic reason a PCM5102A stays silent — the chip then waits for a master
clock that the ESP32 never sends. On the GY-PCM5102 boards FLT/DEMP/XSMT/FMT are solder
jumpers on the back (often labelled H1L…H4L) and are usually already set correctly; board
revisions differ, so check yours.
| MAX98357A | WT32-ETH01 | Note |
|---|---|---|
| VIN | 5V |
|
| GND | GND |
|
| BCLK | IO14 |
|
| LRC | IO32 |
silkscreen CFG |
| DIN | IO33 |
silkscreen 485_EN |
| GAIN | float | 9 dB; strap to change it |
| SD | float | both channels, output enabled |
The MAX98357A is an I2S (Philips) slave. Mono clips are duplicated to both channels by the
firmware, so a clip plays regardless of how SD selects the channel.
GPIO4 is left over for an optional status LED (STATUS_PIN in src/config.h) — it is lit
while the Ethernet link is up. All pins are defined in src/config.h.
Two line-level sources into one amplifier input is a passive resistor mixer: one resistor from each source, joined at the amplifier input.
Snapcast client out ──[ 10 kΩ ]──┬──▶ power amp input
DAC out ─────────────[ 10 kΩ ]──┘
└──[ 100 kΩ ]── GND (optional, defines the DC path)
Costs about 6 dB of level and needs both sources to have low-impedance outputs and the amplifier input to be high impedance, which is true of essentially any consumer gear. There is no ducking: the chime is added on top of whatever is playing.
The PCM5102A's 2.1 Vrms is hot for a consumer line input (typically 0.3–1 Vrms). Either drop the software volume to roughly 30 %, or — better, because digital attenuation costs resolution — add a divider, e.g. 10 kΩ in series and 4.7 kΩ to ground, and leave the software volume near 100 %.
If you would rather have the chime replace the music than sit on top of it, switch the amplifier
input with a small relay instead of mixing, driven from GPIO4.
The device is wired-only. It takes its address from DHCP and announces itself as
doorbird-chime, so it is reachable at http://doorbird-chime.local/ (mDNS) or at whatever
address the DHCP server handed out.
There is no AP fallback and no captive portal: a device that cannot reach the network is reachable only over serial. That is the trade for a wired device.
The whole interface is one page behind one password (default: doorbird). Logging in sets an
HttpOnly; SameSite=Strict session cookie that expires after 30 minutes idle, and five wrong
attempts lock logins out for a minute. The page nags until the default password is replaced.
There is no TLS — the password and the session cookie travel in the clear over the LAN. Treat this as "keeps the household out", not as a boundary against someone already on your network.
Everything is on a single page at http://doorbird-chime.local/, and each card saves on its own.
The UI text is German; code, comments and this document are English.
- Status — link state, MQTT, uptime, selected chime and volume, plus a Gong testen button
- Letztes Ereignis — what came in last and whether it rang
- Klingeltöne — volume and the ring tone manager (below)
- DoorBird — notification key, optional intercom ID filter, which events ring
- MQTT — broker, port, credentials, base topic (blank broker = disabled)
- Zeit — NTP server and time zone
- Protokoll — the in-RAM event log
- Zugang — the admin password
- Firmware — OTA upload, reboot, factory reset
Settings are stored in NVS and applied immediately; only a firmware update reboots the device.
Clips live in /chimes on the LittleFS partition and are managed from the browser: upload a WAV,
preview it, pick the active one, delete the rest. The active file name is stored in NVS, and if
it ever goes missing the firmware falls back to whatever else is there rather than going silent.
- Format: 16-bit PCM WAV, mono or stereo, any sample rate. Anything the player cannot read back is rejected at upload time rather than failing silently at the next doorbell press.
- Names: letters, digits, dot, underscore, dash; spaces become underscores; must end in
.wav. Directory parts in the uploaded name are stripped. - Budget: the 896 KB partition leaves 875 KB after LittleFS metadata, and each file is rounded up to a 4 KB block. At 16 kHz/16-bit mono (~32 KB per second) that is about 27 seconds of audio in total. The card shows the real numbers.
- The last remaining clip cannot be deleted.
A doorbell chime has almost no energy above 4 kHz, so 16 kHz mono is transparent for this kind of material and a quarter the size of 44.1 kHz stereo:
ffmpeg -i input.mp3 -ac 1 -ar 16000 -sample_fmt s16 data/chimes/output.wav
Mono matters if you feed a MAX98357A: with one speaker you would otherwise hear only one channel
of a stereo mix, depending on the SD strap.
Neither DAC has a volume register, so the slider scales the samples in software before they go
out over I2S. That costs roughly one bit of resolution per halving, which is why the slider stops
at 5 % and why very quiet settings sound grainier than turning down an analogue amplifier would.
If you need it permanently quieter, attenuate in the analogue domain (divider, or the MAX98357A's
GAIN strap) and leave the slider near 100 %.
Releasing the slider saves the value and plays the selected clip once, so you can set it by ear.
Two separate things, and only one of them needs a server:
- The time zone is a static rule, so it applies with or without NTP. DoorBird events carry an
absolute UTC timestamp in the packet, which means Letztes Ereignis shows correct local time
even on a device whose own clock was never set. The field takes a POSIX TZ string; the default
CET-1CEST,M3.5.0,M10.5.0/3is Central European Time with EU daylight saving. - The clock comes from SNTP, and only the log depends on it. Leave the server blank and everything keeps working — log lines just carry uptime instead of a time of day.
A router on the LAN is usually the better NTP server: it answers faster than pool.ntp.org and
keeps working when the internet does not. The client lives in lwIP and polls in the background,
so it never blocks the doorbell.
The last 32 events from an in-RAM ring buffer: boot and reset reason, link changes, received DoorBird events, logins and failed logins, configuration changes, uploads, OTA. It is gone after a restart.
Each entry stores both an uptime and a wall clock. Lines written before the first successful NTP sync — everything during boot, typically — keep the uptime; the rest show a time of day.
With -DDEBUG the same lines also go to the serial console.
Optional. With no broker configured the client is inert.
<topic>/status— retainedonline/offline(LWT); base topic defaults todoorbird<topic>/event— one JSON message per event:
{"intercom_id":"ghikzi","event":"1","timestamp":1699550033,"time":"2023-11-09T17:13:53Z"}Every event is published, including motion and events that do not ring the chime — the filter
only decides whether the speaker makes a noise.
The client id is doorbird-chime-<last 3 bytes of the MAC>, so several devices can share one
broker. It stays the same across reboots, which is what retained sessions and the LWT rely on.
Firmware → Firmware hochladen … takes a firmware.bin from .pio/build/wt32eth01/ and shows
upload progress. From the shell it needs the session cookie and the CSRF header:
curl -c jar -H "X-DB-CSRF: 1" -d "password=PASSWORD" http://doorbird-chime.local/api/login
curl -b jar -H "X-DB-CSRF: 1" -F "firmware=@.pio/build/wt32eth01/firmware.bin" \
http://doorbird-chime.local/api/firmware
Your settings survive the update — password, notification key, MQTT credentials, volume and chime selection are read, migrated and rewritten rather than reset.
The filesystem image cannot be flashed over the air, only individual ring tones can (which is
a different mechanism, see above). Replacing the whole /chimes content means pio run -t uploadfs over serial, and that erases anything uploaded through the web interface.
Because nothing else discards the configuration, Firmware → Werkseinstellungen is the only way back to the default password without a serial cable. It clears the stored settings — password, notification key, filters, MQTT, volume, chime selection — drops all sessions and reboots. The uploaded ring tones live on the filesystem and stay.
If you are locked out entirely, the remaining option is erasing flash over the adapter:
pio run -t erase && pio run -t upload && pio run -t uploadfs
Serial log shows:
E (699375) esp.emac: emac_esp32_transmit(229): insufficient TX buffer size
What it means. That message is not from this firmware, it is the ESP32 Ethernet MAC driver.
The transmit ring is 10 descriptors of 512 bytes (CONFIG_ETH_DMA_TX_BUFFER_NUM), about three
full-size frames. Before queueing a frame the driver walks the chain and gives up the moment a
descriptor is still owned by the DMA. So the message says: frames are being queued but the
hardware is not putting them on the wire. The PHY usually still reports link up, so
ARDUINO_EVENT_ETH_DISCONNECTED never fires and nothing notices.
Where to look, roughly in order of likelihood:
-
Powering the board from a USB-TTL adapter's 3.3 V pin. This is the most common cause and it is worth ruling out first. The ESP32 with the radio off draws ~40–50 mA, the LAN8720 adds ~60 mA in 100BASE-TX, plus the oscillator: call it 120–140 mA continuous. A programmer's 3.3 V rail does not deliver that — an FT232RL is specified for 50 mA, a CP2102 for about 100 mA including its own consumption, and many CH340 boards have no usable 3.3 V rail at all. The rail sags, and the PHY and its oscillator are the first things to fail while the ESP32 core keeps running happily — which is exactly "unreachable but still alive". Feeding
3V3also back-feeds the on-board regulator's output.Power the board from
5Vwith a supply that can do ≥500 mA (1 A with a DAC attached) and connect onlyTXD,RXDandGNDfrom the programmer — never its 3.3 V or 5 V as well, or two sources fight each other. Common ground is required. -
Supply in general. Same failure, other origin: a thin USB cable, a shared rail with the audio stage, a supply that sags on peaks.
Brownout detector was triggeredin the log, or Startgrund: Unterspannung on the status page, settles it. -
The 50 MHz oscillator. On the WT32-ETH01 it feeds GPIO0 and is gated by the PHY power pin GPIO16. If it stops or becomes unstable, the transmit DMA stalls exactly like this while the link stays nominally up.
-
Switch port. A port forced to a fixed speed/duplex against the PHY's auto-negotiation causes deferrals that can wedge the ring. Let both ends auto-negotiate.
-
Cabling. Long or unshielded runs next to mains or the speaker cable.
Two tiles on the status page tell you which of these you are looking at:
- Pakete — the datagram counter, with a red pill once nothing has arrived for over a minute. The DoorBird repeats its keep-alive every ~7 seconds, so silence there is real.
- Startgrund — why the current boot happened.
Unterspannung(brownout) orAbsturzpoints straight at the hardware.
- Does the Pakete counter on the status page go up? If not, the broadcasts are not reaching the device — check that it is on the same broadcast domain as the DoorBird, and that no switch or AP is filtering broadcast traffic.
- Counter rising but no event in the log? The key is wrong, or the packets are v1. Check the
Schlüssel pill and re-fetch the key with
getsession.cgi. - Event logged with kein Gong? The event is not in your Gong bei Ereignissen list, or it is
motion, which never rings.
- Press Gong testen: if the log shows the clip playing, the firmware side is fine and the problem is wiring or levels.
- On a PCM5102A:
SCKmust be tied toGNDandXSMTto3V3. A floatingSCKis the single most common reason these boards stay silent. - Check the software volume is not at the bottom of the range.
The web page is a client of this API; nothing is server-rendered. Every endpoint answers JSON.
Everything except /api/session needs the session cookie, and every POST additionally needs the
header X-DB-CSRF: 1 — a browser cannot set a custom header on a cross-site form post, which
together with SameSite=Strict is the CSRF defence. The header's value is irrelevant.
| Endpoint | Method | Purpose |
|---|---|---|
/ |
GET | the single page |
/api/session |
GET | {"authenticated":bool} — the only endpoint that needs no session |
/api/login |
POST | password=…; sets the session cookie |
/api/logout |
POST | clears it |
/api/status |
GET | link, IP, MAC, MQTT, uptime, heap, selected chime, volume, packet counter, reset reason, device time, last event |
/api/config |
GET | current settings; passwords are write-only and never returned |
/api/config |
POST | partial update — only the fields present in the request change |
/api/log |
GET | the in-RAM ring buffer, oldest first; t is uptime, w a wall clock or 0 |
/api/chimes |
GET | clip list, selected file, volume, partition usage |
/api/chimes/select |
POST | name=… |
/api/chimes/delete |
POST | name=…; refuses to remove the last one |
/api/chimes/upload |
POST | multipart, field chime; validated and discarded if unplayable |
/api/test |
POST | play the selected clip, or name=… to preview a specific one |
/api/reboot |
POST | restart |
/api/factory-reset |
POST | clear the stored configuration, then restart |
/api/firmware |
POST | multipart, field firmware; reboots on success |
POST /api/config accepts adminPass, notifyKey, intercom, chimeEvents, mqttServer,
mqttPort, mqttUser, mqttPass, mqttTopic, volume, ntpServer and timezone. An empty adminPass or mqttPass
means "leave unchanged", so the UI never has to send the current one back.
Each board has its own table: partitions_chimes.csv (4 MB, WT32-ETH01) and
partitions_chimes_16mb.csv (16 MB, P4).
partitions_chimes.csv replaces the stock min_spiffs.csv: two 1.5 MB app slots (so OTA still
works) and a 896 KB data partition. The stock table leaves only 128 KB for data — about four
seconds of audio, which makes a ring tone chooser pointless. The firmware is under 1 MB, so the
smaller app slots still leave ~40 % headroom.
Changing the partition table needs a serial flash. OTA writes into the other app slot, it cannot repartition.
PlatformIO, but not the official espressif32 platform: that one is still on arduino-esp32
2.0.17 / IDF 4.4. platformio.ini pins a pioarduino
release instead, currently arduino-esp32 3.3.11 on IDF 5.5. The first build after cloning pulls
a toolchain and framework of a few hundred MB.
The board has no USB bridge, so the first flash needs an external 3.3 V USB-TTL adapter:
| Adapter | WT32-ETH01 |
|---|---|
| TXD | RXD0 |
| RXD | TXD0 |
| GND | GND |
| — | IO0 → GND while powering up |
Pull IO0 to GND, then apply power: the board comes up in the ROM bootloader. Remove the
jumper and power-cycle afterwards. Do not power the board from the adapter's 3.3 V pin, it will
not supply the PHY.
pio run -e wt32eth01 # build (WT32-ETH01)
pio run -e wt32eth01_debug # build with -DDEBUG serial logging
pio run -e p4eth # build (Waveshare ESP32-P4, untested)
pio run -e wt32eth01 -t upload # flash firmware over serial
pio run -e wt32eth01 -t uploadfs # flash data/ (the seed ring tones)
pio device monitor # logs
pio test -e native # host tests for the NVS migration logic
python3 test/ui/check_sync.py # verify the UI mock still matches the API
With more than one env defined, -e is no longer optional for upload and uploadfs — without
it PlatformIO picks default_envs, which is the WT32-ETH01.
Once the device is on the network, further firmware updates go over OTA and need no adapter.
| Path | |
|---|---|
src/ |
firmware; header-mostly, one class per subsystem |
src/DoorbirdCrypto.{h,cpp} |
the ChaCha20-Poly1305 decryption — the one part that must be byte-exact |
src/web_ui.h |
the whole web page as a single PROGMEM string |
data/chimes/ |
seed ring tones packed into the filesystem image |
test/ |
host tests (pio test -e native) and the Arduino/NVS fakes they run against |
partitions_chimes.csv |
custom partition table |
CLAUDE.md |
notes on why the code looks the way it does, and the traps |
- The decryption reproduces the official test vector from the DoorBird API documentation;
CLAUDE.mddocuments the vector and a one-liner that checks it against libsodium. - The NVS migration is covered by host tests (
pio test -e native) — that is the logic that silently costs you your password if it breaks. - The web interface was exercised in a headless browser against
test/ui/mock.py, which also renders the screenshots above. - Plain HTTP only. This belongs on a trusted network segment.
- The v1 (Argon2i) notification scheme is not implemented.
- Not verified on hardware by the author of these docs: I2S output, the OTA flash itself, and LittleFS behaviour across arduino-esp32 versions.
MIT — see LICENSE. That covers the firmware, the tests and data/chimes/chime.wav,
which is synthesised, not sampled from anywhere.
The notification key, intercom ID and packet contents shown in this README, in the screenshots and in the test fixtures are the published test vector from the DoorBird API documentation, not anyone's real credentials. Network addresses in the screenshots are invented.
Dependencies keep their own licenses: 256dpi/MQTT (MIT), rweather/Crypto (MIT), Unity (MIT),
and the arduino-esp32 core (LGPL-2.1-or-later / Apache-2.0).




