M1730-ESP32
ESP32 firmware that drives one or more M1730 analog panel meters (moving-coil ammeters) via PWM, with a web configuration UI and Home Assistant integration over MQTT.
Table of contents
- How it works
- Hardware
- Building and flashing
- First-time Wi-Fi setup
- Web UI
- MQTT / Home Assistant
- HTTP API
- Configuration reference
How it works
Each meter needle is controlled by a PWM signal on a GPIO pin. The firmware maps a 0–100 % value to a PWM duty cycle, scaled by a per-meter Max Duty percentage that you calibrate for full-scale deflection. All settings are stored in LittleFS (/config.json) so they survive reboots.
needle position = (currentValue / 100) × (maxDuty / 100) × 1023 ticks
PWM runs at 5 kHz with 10-bit resolution (0–1023).
Physical range mapping
Each meter has configurable Min / Max values that define its physical range (e.g. 0–50 A). The firmware normalises to 0–100 % internally, but MQTT publishes and receives in physical units — Home Assistant never has to deal with percentages.
Hardware
| Item | Details |
|---|---|
| MCU | ESP32-S3 DevKitC-1 |
| Meter | M1730 moving-coil panel ammeter (or any PWM-driveable analog meter) |
| Max meters | 8 simultaneous (ESP32 LEDC channels 0–7) |
| PWM freq | 5 kHz |
| PWM resolution | 10-bit |
Connect the meter coil (via a current-limiting resistor sized for full-scale) between a GPIO pin and GND. Find the correct resistor value by raising Max Duty slowly until the needle reaches full scale.
Here's a list of suggested current-limiting resistors:
| Meter full-scale (I_fs) | Typical R_coil | V across coil @ FS | Series R (calc) | Nearest E12 (≤calc) | Pin current @ 100% duty |
|---|---|---|---|---|---|
| 50 µA | 1–5 kΩ | ~0.2 V | 62 kΩ | 56 kΩ | 54 µA |
| 100 µA | 0.5–2 kΩ | ~0.15 V | 31 kΩ | 27 kΩ | 114 µA |
| 200 µA | 0.5–1 kΩ | ~0.15 V | 15.7 kΩ | 15 kΩ | 203 µA |
| 500 µA | 100–500 Ω | ~0.15 V | 6.3 kΩ | 5.6 kΩ | 551 µA |
| 1 mA | 50–200 Ω | ~0.15 V | 3.15 kΩ | 2.7 kΩ | 1.14 mA |
| 5 mA | 20–80 Ω | ~0.2 V | 620 Ω | 560 Ω | 5.34 mA |
| 10 mA | 5–40 Ω | ~0.2 V | 310 Ω | 270 Ω | 11.4 mA |
Building and flashing
The project uses PlatformIO.
# Build
pio run
# Flash
pio run --target upload
# Open serial monitor (115200 baud)
pio device monitor
Board target: esp32-s3-devkitc-1
First-time Wi-Fi setup
On first boot (or when stored Wi-Fi credentials are missing), the device starts an access point named M1730. Connect to it with any phone or laptop — a captive portal will appear automatically.
- Enter your Wi-Fi SSID and password.
- Optionally change the Device hostname (default:
m1730). - Click Save. The device connects to your network and restarts.
After connecting, the device is reachable at:
http://<nhostname>.local(defaulthttp://m1730.local) (mDNS, works on most local networks)http://<IP address>(shown in the serial monitor on boot)
Factory reset (forgot password)
If you lose the web UI password, reset the device 5 times in quick succession (press the EN button or cycle power 5× within a few seconds — before the boot completes). The firmware resets all settings to factory defaults except meter pin assignments and calibration (maxDuty), which are preserved. On acknowledgement, all connected meters sweep 0→100→0.
Serial output shows the progress:
[BOOT] reset count 1/5
[BOOT] reset count 2/5
...
[BOOT] reset count 5/5, triggering factory reset
[BOOT] factory reset triggered!
...
[BOOT] factory reset complete, restarting...
Web UI
Browse to the device address to open the configuration page.
Info panel
Shows the current hostname and IP address.
Hostname
Sets the mDNS name (<hostname>.local) and the MQTT device name. Saved across reboots.
Meters
Use the Meters dropdown to add or remove meters (1–10). Each meter has:
| Field | Description |
|---|---|
| Pin | GPIO pin number connected to the meter coil via current limiting resistor |
| Name | Label shown in Home Assistant and the web UI |
| Unit | Optional unit string shown in Home Assistant (e.g. W, A, °C) |
| Min / Max | Physical range of the meter (e.g. 0–50 A, 0–3000 W). MQTT publishes and receives values in this range; HA discovery uses these as the number entity min/max |
| Max Duty | PWM duty at full scale, as a percentage (0–100). Calibrate this so the needle just reaches full deflection |
| Output slider | Moves the needle live (0–100 % of the configured range). Also sent to MQTT |
Click Save to persist all settings. Changing the meter count also triggers an immediate save and meter re-attach.
MQTT / Home Assistant
Enabling MQTT
In the MQTT section of the web UI:
| Field | Description |
|---|---|
| Enable | Toggle MQTT on/off |
| Broker | Hostname or IP of your MQTT broker |
| Port | Default 1883 |
| User / Pass | Broker credentials |
| Prefix | Topic prefix (default m1730) |
Home Assistant auto-discovery
On connect, the device publishes discovery payloads to homeassistant/number/…/config. Each meter appears in HA as a Number entity with min/max taken from the meter's configured physical range and a step of 0.1. The entities are grouped under a single HA device named after the hostname.
Value mapping
Internally the firmware works with a 0–100 % duty value. MQTT publishes and receives physical values — the percentage is transparently mapped to the meter's Min–Max range:
physicalValue = percentage / 100 × (rangeMax - rangeMin) + rangeMin
For example, with Min=0 and Max=50, the slider at 50 % publishes 25.0 to MQTT, and a command of 25.0 on the /set topic moves the slider to 50 %.
Topics
| Direction | Topic | Description |
|---|---|---|
| Published | <prefix>/meter/<n>/current |
Current meter value in physical units (retained) |
| Subscribed | <prefix>/meter/<n>/current/set |
Set meter value in physical units |
| Published | <prefix>/status |
online: true on connect, online: false as LWT |
<n> is the zero-based meter index.
Reconnection
The firmware probes the broker TCP port before attempting a full MQTT connect. If the broker is unreachable, it retries every 30 seconds without blocking the web server.
Example HA automation
automation:
- alias: Show solar power on meter
trigger:
platform: state
entity_id: sensor.solar_power_w
action:
service: number.set_value
target:
entity_id: number.m1730_solar
data:
value: "{{ trigger.to_state.state | float | round(1) }}"
# Values are in physical units — if the meter Min=0, Max=3000, the HA
# number entity directly accepts watts, no conversion needed.
HTTP API
GET /set?i=<index>&v=<value>
Immediately moves meter <index> to <value> (0–100), updates PWM, persists the value to flash, and publishes to MQTT. Used by the live slider on the web UI.
| Parameter | Type | Description |
|---|---|---|
i |
integer | Meter index (0-based) |
v |
float | Value 0–100 |
Returns 200 OK on success, 400 on bad input.
GET /
Returns the full HTML configuration page.
POST /config
Saves all configuration from the HTML form and re-applies PWM to all meters. Responds with the updated configuration page.
Configuration reference
Config is stored as JSON in LittleFS at /config.json.
{
"hostname": "m1730",
"mqtt": {
"enabled": true,
"host": "192.168.1.10",
"port": 1883,
"user": "ha",
"pass": "secret",
"prefix": "m1730"
},
"meters": [
{
"pin": 4,
"maxD": 72.5,
"name": "Solar",
"unit": "W",
"rangeMin": 0.0,
"rangeMax": 3000.0
}
]
}
The file is written by the web UI and should not need manual editing. To reset to factory defaults (keeping pin/calibration), reset the device 5 times in quick succession. To erase everything including flash, use pio run --target erase.