6.0 KiB
Rotary Dial
ESP32 firmware that reads a pulse-generating telephone rotary dial and publishes the dialled number to Home Assistant over MQTT.
The project is a standalone PlatformIO/Arduino firmware using WiFiManager for first-time Wi-Fi setup, LittleFS for local configuration storage, a small built-in web UI, and PubSubClient for MQTT.
How It Works
A classic rotary telephone dial closes and opens contacts as the dial is wound and released. This firmware expects two contacts:
| Contact | GPIO | Purpose |
|---|---|---|
| Off-normal / active contact | GPIO 4 | Goes active while a digit is being dialled |
| Pulse contact | GPIO 5 | Generates the digit pulse train |
Both pins use INPUT_PULLUP, so each contact should switch the GPIO to GND when active.
When the off-normal contact goes low, the firmware starts a digit. While that contact remains low, it counts rising edges on the pulse contact. When the off-normal contact returns high, the digit is complete:
| Pulses | Digit |
|---|---|
| 1-9 | 1-9 |
| 10 or more | 0 |
Digits are appended to a string. After 3 seconds without another completed digit, the full number is published to MQTT. Leading zeros are preserved, so dialling 07 publishes "07".
Hardware
| Item | Details |
|---|---|
| MCU | ESP32 board supported by the configured PlatformIO environments |
| Dial | Pulse-generating rotary telephone dial with off-normal and pulse contacts |
| Off-normal pin | GPIO 4 (OFF_NORMAL_PIN in src/main.cpp) |
| Pulse pin | GPIO 5 (PULSE_PIN in src/main.cpp) |
| Wiring | Each contact connects its GPIO to GND when active; firmware enables internal pull-ups |
If your dial uses inverted contact logic, adjust the state checks in dialLoop() rather than only changing an interrupt trigger. The current implementation polls both pins and does not use GPIO interrupts.
Building and Flashing
Install PlatformIO, then run:
# Build the default ESP32 DevKit environment
pio run -e esp32dev
# Flash firmware
pio run -e esp32dev --target upload
# Upload the LittleFS image, including data/logo.png
pio run -e esp32dev --target uploadfs
# Serial monitor at 115200 baud
pio device monitor
The configured environments are:
| Environment | Board |
|---|---|
esp32dev |
ESP32 Dev Module |
esp32-s3-devkitc-1 |
ESP32-S3-DevKitC-1 |
Use the matching environment name in the commands above. Close the serial monitor before uploading firmware because it holds the serial port.
First-Time Wi-Fi Setup
On first boot, WiFiManager opens an access point named Rotary-Dial. Connect with a phone or laptop and use the captive portal to enter Wi-Fi credentials.
The portal also exposes a Device hostname field. The default hostname is rotary-dial.
After the device joins Wi-Fi, it starts mDNS and the web UI is reachable at:
http://rotary-dial.local
The serial monitor also prints the assigned IP address.
Web UI
Browse to the device address to configure:
| Setting | Notes |
|---|---|
| Hostname | Saved to LittleFS; changing it restarts the device |
| Security | Optional HTTP basic auth, username admin |
| MQTT | Broker host, port, username, password, topic prefix, and enable switch |
Passwords are only replaced when a new non-empty password is submitted. Leaving an existing password field blank keeps the stored value.
The header logo is served from /logo.png, which comes from data/logo.png in the uploaded LittleFS image.
HTTP API
| Method | Path | Description |
|---|---|---|
GET |
/ |
HTML configuration page |
GET |
/logo.png |
Logo image served from LittleFS |
POST |
/config |
Save hostname, auth, and MQTT settings; redirects to /; restarts if hostname changed |
POST |
/reset |
Restart the device immediately |
When HTTP auth is enabled, the username is admin and the password is the value configured in the web UI.
MQTT
MQTT is disabled until it is enabled and configured in the web UI.
Default topic prefix: dial
| Direction | Topic | Description |
|---|---|---|
| Published | <prefix>/dialed |
Complete dialled number as a non-retained string event |
| Published | <prefix>/status |
online on connect, offline as retained MQTT last will |
| Published | homeassistant/sensor/<object_id>/config |
Retained Home Assistant discovery payload |
The firmware publishes Home Assistant MQTT discovery for a sensor named Dialed Number.
Example automation:
automation:
- alias: Act on dialled number
trigger:
platform: mqtt
topic: dial/dialed
action:
- choose:
- conditions:
- condition: template
value_template: "{{ trigger.payload == '112' }}"
sequence:
- service: notify.mobile_app
data:
message: "Emergency number dialled!"
Factory Reset
During boot, the firmware increments a reset counter in LittleFS and waits 3 seconds. Rapidly power-cycle or press EN before that window expires to accumulate reset counts.
| Rapid boot count | Effect |
|---|---|
| 5-9 | Clears hostname, HTTP auth, and MQTT config |
| 10+ | Clears hostname, HTTP auth, MQTT config, and Wi-Fi credentials |
After the 3-second window, the counter is cleared if no reset threshold is reached.
Example serial output:
[BOOT] reset count 3
[BOOT] reset count 4
[BOOT] reset count 5, triggering factory reset
[BOOT] factory reset triggered (clearWifi=0)!
[BOOT] factory reset complete, restarting...
Tuning
The dial behavior is controlled by constants in src/main.cpp:
| Constant | Default | Description |
|---|---|---|
PULSE_DEBOUNCE_MS |
20 ms | Minimum time between counted pulse release edges |
NUMBER_COMPLETE_MS |
3000 ms | Idle time after the last completed digit before publishing the number |
The source also defines DIGIT_BOUNDARY_MS, but the current digit boundary is determined by the off-normal contact returning high.