# 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 | `/dialed` | Complete dialled number as a non-retained string event | | Published | `/status` | `online` on connect, `offline` as retained MQTT last will | | Published | `homeassistant/sensor//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.