169 lines
6.0 KiB
Markdown
169 lines
6.0 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```text
|
|
[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.
|