Files
2026-07-12 15:43:31 +02:00

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.