Skip to content

Repository files navigation

DoorBird Doorbell Chime (WT32-ETH01)

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.

Web interface

Background — why this exists

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.

How it works

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' chachapoly is IETF-only and will not work; the AEAD is assembled by hand from the rweather/Crypto primitives in src/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.

Getting the notification key

Decryption needs the NOTIFICATION_ENCRYPTION_KEY from the DoorBird API. You fetch it once.

  1. In the DoorBird app, go to AdministrationUser 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 the INTERCOM_ID you can optionally filter on.

  2. 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"
      }
    }
  3. Paste NOTIFICATION_ENCRYPTION_KEY into 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.cgi returns 401, 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).

Which events exist

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.

Hardware

Supported boards

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=22

Those 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.

Board (WT32-ETH01)

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_MHZ is 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 off PLL_F160M, and the RMII reference clock comes from the board's own oscillator.
  • LOOP_IDLE_MS is 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.

Which I2S DAC

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.

Wiring — PCM5102A (line level)

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 H1LH4L) and are usually already set correctly; board revisions differ, so check yours.

Wiring — MAX98357A (straight to a speaker)

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.

Mixing into an existing line signal

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.

Network and access

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.

Login

Web interface

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.

Overview

  • 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.

Ring tones

Ring tone manager

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.

Volume

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.

Time

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/3 is 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.

Log

Log

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.

MQTT

Optional. With no broker configured the client is inert.

  • <topic>/status — retained online / offline (LWT); base topic defaults to doorbird
  • <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 update (OTA)

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.

Factory reset

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

Troubleshooting

The device drops off the network and only a power cycle brings it back

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:

  1. 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 3V3 also back-feeds the on-board regulator's output.

    Power the board from 5V with a supply that can do ≥500 mA (1 A with a DAC attached) and connect only TXD, RXD and GND from the programmer — never its 3.3 V or 5 V as well, or two sources fight each other. Common ground is required.

  2. 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 triggered in the log, or Startgrund: Unterspannung on the status page, settles it.

  3. 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.

  4. 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.

  5. 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) or Absturz points straight at the hardware.

Nothing rings

  • 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.

It rings but nothing comes out of the speakers

  • 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: SCK must be tied to GND and XSMT to 3V3. A floating SCK is the single most common reason these boards stay silent.
  • Check the software volume is not at the bottom of the range.

JSON API

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.

Partition layout

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.

Build & flash

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
IO0GND 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.

Repository layout

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

Status and caveats

  • The decryption reproduces the official test vector from the DoorBird API documentation; CLAUDE.md documents 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.

License

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).

About

Standalone doorbell chime for DoorBird intercoms, on an ESP32 with wired Ethernet

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages