Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
132ffe352a
|
|||
|
5d3c82a1e0
|
@@ -0,0 +1,5 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,82 +2,94 @@
|
|||||||
|
|
||||||
ESP32 firmware that reads a pulse-generating telephone rotary dial and publishes the dialled number to Home Assistant over MQTT.
|
ESP32 firmware that reads a pulse-generating telephone rotary dial and publishes the dialled number to Home Assistant over MQTT.
|
||||||
|
|
||||||
Built on the [ESP32 Device Framework](../Device-Framework).
|
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
|
## How It Works
|
||||||
|
|
||||||
A classic rotary telephone dial generates a burst of pulses as it returns to rest after each digit — one pulse for `1`, two for `2`, …, nine for `9`, ten for `0`. The firmware counts those pulses on a single GPIO pin and assembles digits into a string. When the user stops dialling (2.5 s silence), the complete number is published to MQTT.
|
A classic rotary telephone dial closes and opens contacts as the dial is wound and released. This firmware expects two contacts:
|
||||||
|
|
||||||
Leading zeros are preserved: dialling `07` sends `"07"`, not `"7"`.
|
| 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
|
## Hardware
|
||||||
|
|
||||||
| Item | Details |
|
| Item | Details |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| MCU | ESP32 (any variant) |
|
| MCU | ESP32 board supported by the configured PlatformIO environments |
|
||||||
| Dial | Pulse-generating rotary telephone dial |
|
| Dial | Pulse-generating rotary telephone dial with off-normal and pulse contacts |
|
||||||
| Pulse pin | GPIO 4 (`DIAL_PIN` in `main.cpp`) |
|
| Off-normal pin | GPIO 4 (`OFF_NORMAL_PIN` in `src/main.cpp`) |
|
||||||
| Wiring | Pulse contact between GPIO 4 and GND. Pin uses `INPUT_PULLUP`. |
|
| 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 |
|
||||||
|
|
||||||
The pulse contact is normally open. As the dial returns it briefly shorts GPIO 4 to GND on each pulse — the rising edge (release) is what the interrupt counts.
|
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.
|
||||||
|
|
||||||
If your dial's contact is normally closed instead, change the interrupt trigger in `setup()` from `RISING` to `FALLING`.
|
## Building and Flashing
|
||||||
|
|
||||||
## Building and flashing
|
Install PlatformIO, then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
# Build the default ESP32 DevKit environment
|
||||||
pio run -e esp32dev
|
pio run -e esp32dev
|
||||||
|
|
||||||
# Flash firmware
|
# Flash firmware
|
||||||
pio run -e esp32dev --target upload
|
pio run -e esp32dev --target upload
|
||||||
|
|
||||||
# Upload filesystem image (logo, etc.) — only needed when data/ changes
|
# Upload the LittleFS image, including data/logo.png
|
||||||
pio run -e esp32dev --target uploadfs
|
pio run -e esp32dev --target uploadfs
|
||||||
|
|
||||||
# Serial monitor (115 200 baud)
|
# Serial monitor at 115200 baud
|
||||||
pio device monitor
|
pio device monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** close the serial monitor before uploading — it holds the serial port exclusively.
|
The configured environments are:
|
||||||
|
|
||||||
Board targets: `esp32dev`, `esp32-s3-devkitc-1`
|
| Environment | Board |
|
||||||
|
|-------------|-------|
|
||||||
|
| `esp32dev` | ESP32 Dev Module |
|
||||||
|
| `esp32-s3-devkitc-1` | ESP32-S3-DevKitC-1 |
|
||||||
|
|
||||||
## First-time Wi-Fi setup
|
Use the matching environment name in the commands above. Close the serial monitor before uploading firmware because it holds the serial port.
|
||||||
|
|
||||||
On first boot the device opens an access point named **Rotary-Dial**. Connect with any phone or laptop; a captive portal will appear.
|
## First-Time Wi-Fi Setup
|
||||||
|
|
||||||
1. Enter your Wi-Fi credentials.
|
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.
|
||||||
2. Optionally set the **Device hostname** (default: `rotary-dial`, max 63 characters).
|
|
||||||
3. Save. The device joins your network and restarts.
|
|
||||||
|
|
||||||
Reachable at `http://rotary-dial.local` or the IP shown in the serial monitor.
|
The portal also exposes a `Device hostname` field. The default hostname is `rotary-dial`.
|
||||||
|
|
||||||
### Factory reset
|
After the device joins Wi-Fi, it starts mDNS and the web UI is reachable at:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://rotary-dial.local
|
||||||
```
|
```
|
||||||
[BOOT] reset count 3
|
|
||||||
[BOOT] reset count 4
|
The serial monitor also prints the assigned IP address.
|
||||||
...
|
|
||||||
[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. The logo in the header is served from `data/logo.png` in the filesystem image.
|
Browse to the device address to configure:
|
||||||
|
|
||||||
Click **Save** to persist settings. Changing the hostname automatically restarts the device. Click **Reset Device** to restart immediately.
|
| 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
|
## HTTP API
|
||||||
|
|
||||||
@@ -85,23 +97,24 @@ Click **Save** to persist settings. Changing the hostname automatically restarts
|
|||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `GET` | `/` | HTML configuration page |
|
| `GET` | `/` | HTML configuration page |
|
||||||
| `GET` | `/logo.png` | Logo image served from LittleFS |
|
| `GET` | `/logo.png` | Logo image served from LittleFS |
|
||||||
| `POST` | `/config` | Save configuration; redirects to `/`. Restarts if hostname changed |
|
| `POST` | `/config` | Save hostname, auth, and MQTT settings; redirects to `/`; restarts if hostname changed |
|
||||||
| `POST` | `/reset` | Restart the device immediately |
|
| `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
|
||||||
|
|
||||||
### Topics
|
MQTT is disabled until it is enabled and configured in the web UI.
|
||||||
|
|
||||||
|
Default topic prefix: `dial`
|
||||||
|
|
||||||
| Direction | Topic | Description |
|
| Direction | Topic | Description |
|
||||||
|-----------|-------|-------------|
|
|-----------|-------|-------------|
|
||||||
| Published | `<prefix>/dialed` | Complete dialled number as a string (not retained) |
|
| Published | `<prefix>/dialed` | Complete dialled number as a non-retained string event |
|
||||||
| Published | `<prefix>/status` | `online` on connect, `offline` as LWT (retained) |
|
| Published | `<prefix>/status` | `online` on connect, `offline` as retained MQTT last will |
|
||||||
|
| Published | `homeassistant/sensor/<object_id>/config` | Retained Home Assistant discovery payload |
|
||||||
|
|
||||||
Default prefix: `dial`
|
The firmware publishes Home Assistant MQTT discovery for a sensor named `Dialed Number`.
|
||||||
|
|
||||||
### Home Assistant
|
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@@ -122,12 +135,34 @@ automation:
|
|||||||
message: "Emergency number dialled!"
|
message: "Emergency number dialled!"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Timing constants
|
## Factory Reset
|
||||||
|
|
||||||
Adjust in `main.cpp` if your dial runs faster or slower than standard:
|
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 |
|
| Constant | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `PULSE_DEBOUNCE_MS` | 15 ms | Minimum time between counted edges |
|
| `PULSE_DEBOUNCE_MS` | 20 ms | Minimum time between counted pulse release edges |
|
||||||
| `INTER_PULSE_MS` | 350 ms | Silence after last pulse = digit complete |
|
| `NUMBER_COMPLETE_MS` | 3000 ms | Idle time after the last completed digit before publishing the number |
|
||||||
| `INTER_DIGIT_MS` | 2500 ms | Silence after last digit = number complete |
|
|
||||||
|
The source also defines `DIGIT_BOUNDARY_MS`, but the current digit boundary is determined by the off-normal contact returning high.
|
||||||
|
|||||||
+13
-2
@@ -1,9 +1,20 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
[env:esp32-s3-devkitc-1]
|
[env:esp32-s3-devkitc-1]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32-s3-devkitc-1
|
board = esp32-s3-devkitc-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
lib_deps =
|
board_build.filesystem = littlefs
|
||||||
|
lib_deps =
|
||||||
tzapu/WiFiManager@^2.0.17
|
tzapu/WiFiManager@^2.0.17
|
||||||
bblanchon/ArduinoJson@^7.0.0
|
bblanchon/ArduinoJson@^7.0.0
|
||||||
knolleary/PubSubClient@^2.8
|
knolleary/PubSubClient@^2.8
|
||||||
@@ -14,7 +25,7 @@ framework = arduino
|
|||||||
platform = espressif32
|
platform = espressif32
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
lib_deps =
|
lib_deps =
|
||||||
tzapu/WiFiManager@^2.0.17
|
tzapu/WiFiManager@^2.0.17
|
||||||
bblanchon/ArduinoJson@^7.4.3
|
bblanchon/ArduinoJson@^7.4.3
|
||||||
knolleary/PubSubClient@^2.8
|
knolleary/PubSubClient@^2.8
|
||||||
|
|||||||
+51
-58
@@ -22,10 +22,11 @@
|
|||||||
// Dial constants
|
// Dial constants
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#define DIAL_PIN 4 // GPIO connected to the pulse contact (to GND)
|
#define OFF_NORMAL_PIN 4 // GPIO connected to the off-normal / active contact
|
||||||
#define PULSE_DEBOUNCE_MS 100 // ignore edges faster than this
|
#define PULSE_PIN 5 // GPIO connected to the pulse contact
|
||||||
#define INTER_PULSE_MS 350 // silence longer than this = digit complete
|
#define PULSE_DEBOUNCE_MS 20 // ignore pulse edges faster than this
|
||||||
#define INTER_DIGIT_MS 2500 // silence longer than this = number complete, publish
|
#define DIGIT_BOUNDARY_MS 300 // pin-4 high gap longer than this separates digits
|
||||||
|
#define NUMBER_COMPLETE_MS 3000 // idle longer than this ends the number and publishes
|
||||||
|
|
||||||
struct MqttConfig {
|
struct MqttConfig {
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
@@ -54,19 +55,14 @@ static unsigned long mqttReconnectAt = 0;
|
|||||||
// Dial state
|
// Dial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static volatile int dialPulses = 0;
|
static String dialNumber = "";
|
||||||
static volatile unsigned long dialLastPulse = 0; // millis() of last counted pulse
|
static unsigned long dialLastDigit = 0; // millis() of last completed digit
|
||||||
static String dialNumber = "";
|
static bool offNormalState = HIGH; // current state of pin 4
|
||||||
static unsigned long dialLastDigit = 0; // millis() of last completed digit
|
static bool pulseState = HIGH; // current state of pin 5
|
||||||
static int dialRawCount = 0; // raw digit count since last publish (odd positions = wind-up spurious)
|
static bool digitActive = false;
|
||||||
|
static int digitPulseCount = 0;
|
||||||
static void IRAM_ATTR onDialPulse() {
|
static unsigned long digitStartedAt = 0;
|
||||||
unsigned long now = millis();
|
static unsigned long lastPulseEdgeAt = 0;
|
||||||
if (now - dialLastPulse >= PULSE_DEBOUNCE_MS) {
|
|
||||||
dialPulses++;
|
|
||||||
dialLastPulse = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Config persistence (JSON via LittleFS)
|
// Config persistence (JSON via LittleFS)
|
||||||
@@ -258,54 +254,49 @@ static void mqttLoop() {
|
|||||||
|
|
||||||
static void dialLoop() {
|
static void dialLoop() {
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
static int lastLoggedPulses = 0;
|
bool currentOffNormal = digitalRead(OFF_NORMAL_PIN);
|
||||||
|
bool currentPulse = digitalRead(PULSE_PIN);
|
||||||
|
|
||||||
// Snapshot volatile state atomically
|
if (currentOffNormal != offNormalState) {
|
||||||
noInterrupts();
|
if (offNormalState == HIGH && currentOffNormal == LOW) {
|
||||||
int pulses = dialPulses;
|
digitActive = true;
|
||||||
unsigned long lastPulse = dialLastPulse;
|
digitPulseCount = 0;
|
||||||
interrupts();
|
digitStartedAt = now;
|
||||||
|
lastPulseEdgeAt = 0;
|
||||||
// Log each new pulse as it arrives (ISR can't use Serial, so we log here)
|
dialLastDigit = now; // pause idle timer on digit start
|
||||||
if (pulses != lastLoggedPulses) {
|
Serial.printf("[DIAL] digit start at %lu ms\n", now);
|
||||||
Serial.printf("[DIAL] pulse #%d\n", pulses);
|
} else if (offNormalState == LOW && currentOffNormal == HIGH) {
|
||||||
lastLoggedPulses = pulses;
|
if (digitActive) {
|
||||||
|
char digit = '0' + (digitPulseCount >= 10 ? 0 : digitPulseCount);
|
||||||
|
dialNumber += digit;
|
||||||
|
dialLastDigit = now;
|
||||||
|
Serial.printf("[DIAL] digit complete pulses=%d -> '%c' number_so_far=%s\n",
|
||||||
|
digitPulseCount, digit, dialNumber.c_str());
|
||||||
|
}
|
||||||
|
digitActive = false;
|
||||||
|
}
|
||||||
|
offNormalState = currentOffNormal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Burst silence exceeded → digit complete
|
if (digitActive && currentOffNormal == LOW) {
|
||||||
if (pulses > 0 && (now - lastPulse) >= INTER_PULSE_MS) {
|
if (currentPulse == HIGH && pulseState == LOW) {
|
||||||
noInterrupts();
|
unsigned long pulseDelta = now - lastPulseEdgeAt;
|
||||||
dialPulses = 0;
|
if (lastPulseEdgeAt == 0 || pulseDelta >= PULSE_DEBOUNCE_MS) {
|
||||||
interrupts();
|
digitPulseCount++;
|
||||||
lastLoggedPulses = 0;
|
lastPulseEdgeAt = now;
|
||||||
|
Serial.printf("[DIAL] pulse #%d at %lu ms\n", digitPulseCount, now);
|
||||||
dialRawCount++;
|
}
|
||||||
// 10 pulses = digit '0'; 1–9 pulses = digits '1'–'9'
|
|
||||||
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;
|
|
||||||
dialLastDigit = now;
|
|
||||||
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
|
pulseState = currentPulse;
|
||||||
if (dialNumber.length() > 0 && (now - dialLastDigit) >= INTER_DIGIT_MS) {
|
|
||||||
|
if (dialNumber.length() > 0 && (now - dialLastDigit) >= NUMBER_COMPLETE_MS) {
|
||||||
Serial.printf("[DIAL] number complete=%s\n", dialNumber.c_str());
|
Serial.printf("[DIAL] number complete=%s\n", dialNumber.c_str());
|
||||||
mqttPublishDialed(dialNumber);
|
mqttPublishDialed(dialNumber);
|
||||||
dialNumber = "";
|
dialNumber = "";
|
||||||
dialRawCount = 0;
|
digitActive = false;
|
||||||
}
|
digitPulseCount = 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,8 +595,10 @@ void setup() {
|
|||||||
delay(3000);
|
delay(3000);
|
||||||
applyFactoryResetIfNeeded(resetCount);
|
applyFactoryResetIfNeeded(resetCount);
|
||||||
|
|
||||||
pinMode(DIAL_PIN, INPUT_PULLUP);
|
pinMode(OFF_NORMAL_PIN, INPUT_PULLUP);
|
||||||
attachInterrupt(digitalPinToInterrupt(DIAL_PIN), onDialPulse, RISING);
|
pinMode(PULSE_PIN, INPUT_PULLUP);
|
||||||
|
offNormalState = digitalRead(OFF_NORMAL_PIN);
|
||||||
|
pulseState = digitalRead(PULSE_PIN);
|
||||||
|
|
||||||
Serial.println("[WIFI] starting WiFiManager");
|
Serial.println("[WIFI] starting WiFiManager");
|
||||||
WiFiManager wm;
|
WiFiManager wm;
|
||||||
|
|||||||
Reference in New Issue
Block a user