Compare commits

...

3 Commits

2 changed files with 78 additions and 17 deletions
+42 -8
View File
@@ -26,33 +26,67 @@ If your dial's contact is normally closed instead, change the interrupt trigger
## Building and flashing ## Building and flashing
```bash ```bash
# Build (S3 DevKit) # Build
pio run -e esp32-s3-devkitc-1 pio run -e esp32dev
# Flash # Flash firmware
pio run -e esp32-s3-devkitc-1 --target upload pio run -e esp32dev --target upload
# Upload filesystem image (logo, etc.) — only needed when data/ changes
pio run -e esp32dev --target uploadfs
# Serial monitor (115 200 baud) # Serial monitor (115 200 baud)
pio device monitor pio device monitor
``` ```
> **Note:** close the serial monitor before uploading — it holds the serial port exclusively.
Board targets: `esp32dev`, `esp32-s3-devkitc-1`
## First-time Wi-Fi setup ## First-time Wi-Fi setup
On first boot the device opens an access point named **Rotary-Dial**. Connect with any phone or laptop; a captive portal will appear. On first boot the device opens an access point named **Rotary-Dial**. Connect with any phone or laptop; a captive portal will appear.
1. Enter your Wi-Fi credentials. 1. Enter your Wi-Fi credentials.
2. Optionally set the **Device hostname** (default: `rotary-dial`). 2. Optionally set the **Device hostname** (default: `rotary-dial`, max 63 characters).
3. Save. The device joins your network and restarts. 3. Save. The device joins your network and restarts.
Reachable at `http://rotary-dial.local` or the IP shown in the serial monitor. Reachable at `http://rotary-dial.local` or the IP shown in the serial monitor.
### Factory reset ### Factory reset
Power-cycle or press EN **5 times in quick succession** (before boot completes). All settings are cleared and the device restarts into the Wi-Fi portal. Power-cycle (or press EN) rapidly before the 3-second boot window expires. The reset counter accumulates across rapid reboots:
| Cycles | Effect |
|--------|--------|
| 5 9 | Clears hostname, auth, and MQTT config; restarts into normal Wi-Fi |
| 10+ | Clears all of the above **plus** stored Wi-Fi credentials; restarts into captive portal |
Serial output shows the accumulated count:
```
[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...
```
## Web UI ## Web UI
Browse to the device address to configure hostname, authentication, and MQTT. Browse to the device address to configure hostname, authentication, and MQTT. The logo in the header is served from `data/logo.png` in the filesystem image.
Click **Save** to persist settings. Changing the hostname automatically restarts the device. Click **Reset Device** to restart immediately.
## HTTP API
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/` | HTML configuration page |
| `GET` | `/logo.png` | Logo image served from LittleFS |
| `POST` | `/config` | Save configuration; redirects to `/`. Restarts if hostname changed |
| `POST` | `/reset` | Restart the device immediately |
## MQTT ## MQTT
@@ -67,7 +101,7 @@ Default prefix: `dial`
### Home Assistant ### Home Assistant
On MQTT connect the device publishes a discovery payload so HA automatically creates a **sensor** entity (`sensor.<hostname>_dialed_number`) that shows the last dialled number. On MQTT connect the device publishes a discovery payload so HA automatically creates a **sensor** entity that shows the last dialled number.
Example automation: Example automation:
+33 -6
View File
@@ -23,7 +23,7 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#define DIAL_PIN 4 // GPIO connected to the pulse contact (to GND) #define DIAL_PIN 4 // GPIO connected to the pulse contact (to GND)
#define PULSE_DEBOUNCE_MS 15 // ignore edges faster than this #define PULSE_DEBOUNCE_MS 100 // ignore edges faster than this
#define INTER_PULSE_MS 350 // silence longer than this = digit complete #define INTER_PULSE_MS 350 // silence longer than this = digit complete
#define INTER_DIGIT_MS 2500 // silence longer than this = number complete, publish #define INTER_DIGIT_MS 2500 // silence longer than this = number complete, publish
@@ -58,6 +58,7 @@ static volatile int dialPulses = 0;
static volatile unsigned long dialLastPulse = 0; // millis() of last counted pulse static volatile unsigned long dialLastPulse = 0; // millis() of last counted pulse
static String dialNumber = ""; static String dialNumber = "";
static unsigned long dialLastDigit = 0; // millis() of last completed digit static unsigned long dialLastDigit = 0; // millis() of last completed digit
static int dialRawCount = 0; // raw digit count since last publish (odd positions = wind-up spurious)
static void IRAM_ATTR onDialPulse() { static void IRAM_ATTR onDialPulse() {
unsigned long now = millis(); unsigned long now = millis();
@@ -135,11 +136,14 @@ static void saveConfig() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static void mqttPublishDialed(const String& number) { static void mqttPublishDialed(const String& number) {
if (!mqttCfg.enabled || !mqttClient.connected()) return; if (!mqttCfg.enabled || !mqttClient.connected()) {
Serial.printf("[DIAL] MQTT unavailable, number=%s not sent\n", number.c_str());
return;
}
char topic[128]; char topic[128];
snprintf(topic, sizeof(topic), "%s/dialed", mqttCfg.prefix); snprintf(topic, sizeof(topic), "%s/dialed", mqttCfg.prefix);
mqttClient.publish(topic, number.c_str(), false); // not retained — it's an event bool ok = mqttClient.publish(topic, number.c_str(), false); // not retained — it's an event
Serial.printf("[DIAL] published %s -> %s\n", topic, number.c_str()); Serial.printf("[DIAL] publish %s -> %s ok=%d\n", topic, number.c_str(), ok);
} }
static void mqttCallback(char* topic, byte* payload, unsigned int len) { static void mqttCallback(char* topic, byte* payload, unsigned int len) {
@@ -254,6 +258,7 @@ static void mqttLoop() {
static void dialLoop() { static void dialLoop() {
unsigned long now = millis(); unsigned long now = millis();
static int lastLoggedPulses = 0;
// Snapshot volatile state atomically // Snapshot volatile state atomically
noInterrupts(); noInterrupts();
@@ -261,24 +266,46 @@ static void dialLoop() {
unsigned long lastPulse = dialLastPulse; unsigned long lastPulse = dialLastPulse;
interrupts(); interrupts();
// Log each new pulse as it arrives (ISR can't use Serial, so we log here)
if (pulses != lastLoggedPulses) {
Serial.printf("[DIAL] pulse #%d\n", pulses);
lastLoggedPulses = pulses;
}
// Burst silence exceeded → digit complete // Burst silence exceeded → digit complete
if (pulses > 0 && (now - lastPulse) >= INTER_PULSE_MS) { if (pulses > 0 && (now - lastPulse) >= INTER_PULSE_MS) {
noInterrupts(); noInterrupts();
dialPulses = 0; dialPulses = 0;
interrupts(); interrupts();
lastLoggedPulses = 0;
dialRawCount++;
// 10 pulses = digit '0'; 19 pulses = digits '1''9' // 10 pulses = digit '0'; 19 pulses = digits '1''9'
char digit = '0' + (pulses >= 10 ? 0 : pulses); char digit = '0' + (pulses >= 10 ? 0 : pulses);
bool spurious = (dialRawCount % 2 == 1) && (digit == '1');
if (spurious) {
Serial.printf("[DIAL] burst end pulses=%d -> digit='%c' (wind-up spurious, discarded)\n", pulses, digit);
} else {
dialNumber += digit; dialNumber += digit;
dialLastDigit = now; dialLastDigit = now;
Serial.printf("[DIAL] digit=%c number_so_far=%s\n", digit, dialNumber.c_str()); Serial.printf("[DIAL] burst end pulses=%d -> digit='%c' number_so_far=%s\n", pulses, digit, dialNumber.c_str());
}
} }
// Number complete when no further digits arrive within the timeout // Number complete when no further digits arrive within the timeout
if (dialNumber.length() > 0 && (now - dialLastDigit) >= INTER_DIGIT_MS) { if (dialNumber.length() > 0 && (now - dialLastDigit) >= INTER_DIGIT_MS) {
Serial.printf("[DIAL] complete number=%s\n", dialNumber.c_str()); Serial.printf("[DIAL] number complete=%s\n", dialNumber.c_str());
mqttPublishDialed(dialNumber); mqttPublishDialed(dialNumber);
dialNumber = ""; dialNumber = "";
dialRawCount = 0;
}
// If the sequence stalled with no accepted digit (e.g. user wound up then
// stopped), reset the count so the next wind-up is treated as position 1.
if (dialRawCount > 0 && dialNumber.length() == 0 && pulses == 0
&& (now - lastPulse) >= INTER_DIGIT_MS) {
Serial.println("[DIAL] idle reset");
dialRawCount = 0;
} }
} }