Range mapped, documentation added
This commit is contained in:
4
LICENSE
4
LICENSE
@@ -209,7 +209,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
M1730-ESP32
|
M1730-ESP32
|
||||||
Copyright (C) 2026 adebaumann
|
Copyright (C) 2026 Adrian A. Baumann
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||||||
|
|
||||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
M1730-ESP32 Copyright (C) 2026 adebaumann
|
M1730-ESP32 Copyright (C) 2026 Adrian A. Baumann
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
|||||||
226
documentation/README.md
Normal file
226
documentation/README.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# 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](#how-it-works)
|
||||||
|
- [Hardware](#hardware)
|
||||||
|
- [Building and flashing](#building-and-flashing)
|
||||||
|
- [First-time Wi-Fi setup](#first-time-wi-fi-setup)
|
||||||
|
- [Web UI](#web-ui)
|
||||||
|
- [MQTT / Home Assistant](#mqtt--home-assistant)
|
||||||
|
- [HTTP API](#http-api)
|
||||||
|
- [Configuration reference](#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. 660
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building and flashing
|
||||||
|
|
||||||
|
The project uses [PlatformIO](https://platformio.org/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
1. Enter your Wi-Fi SSID and password.
|
||||||
|
2. Optionally change the **Device hostname** (default: `m1730`).
|
||||||
|
3. Click **Save**. The device connects to your network and restarts.
|
||||||
|
|
||||||
|
After connecting, the device is reachable at:
|
||||||
|
|
||||||
|
- `http://m1730.local` (mDNS, works on most local networks)
|
||||||
|
- `http://<IP address>` (shown in the serial monitor on boot)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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`.
|
||||||
|
|
||||||
|
```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,
|
||||||
|
"current": 45.0,
|
||||||
|
"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, delete the file or erase flash with `pio run --target erase`.
|
||||||
54
src/main.cpp
54
src/main.cpp
@@ -19,7 +19,8 @@ struct MeterConfig {
|
|||||||
float currentValue;
|
float currentValue;
|
||||||
char name[32] = "";
|
char name[32] = "";
|
||||||
char unit[16] = "";
|
char unit[16] = "";
|
||||||
float range = 100.0;
|
float rangeMin = 0.0;
|
||||||
|
float rangeMax = 100.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MqttConfig {
|
struct MqttConfig {
|
||||||
@@ -78,7 +79,8 @@ static void loadConfig() {
|
|||||||
meters[i].currentValue = m["current"] | 0.0f;
|
meters[i].currentValue = m["current"] | 0.0f;
|
||||||
strlcpy(meters[i].name, m["name"] | "", sizeof(meters[i].name));
|
strlcpy(meters[i].name, m["name"] | "", sizeof(meters[i].name));
|
||||||
strlcpy(meters[i].unit, m["unit"] | "", sizeof(meters[i].unit));
|
strlcpy(meters[i].unit, m["unit"] | "", sizeof(meters[i].unit));
|
||||||
meters[i].range = m["range"] | 100.0f;
|
meters[i].rangeMin = m["rangeMin"] | m["range"] | 0.0f;
|
||||||
|
meters[i].rangeMax = m["rangeMax"] | m["range"] | 100.0f;
|
||||||
}
|
}
|
||||||
Serial.printf("[CFG] loaded hostname=%s meters=%d mqtt_en=%d\n", hostname, meterCount, mqttCfg.enabled);
|
Serial.printf("[CFG] loaded hostname=%s meters=%d mqtt_en=%d\n", hostname, meterCount, mqttCfg.enabled);
|
||||||
}
|
}
|
||||||
@@ -103,7 +105,8 @@ static void saveConfig() {
|
|||||||
m["current"] = meters[i].currentValue;
|
m["current"] = meters[i].currentValue;
|
||||||
m["name"] = meters[i].name;
|
m["name"] = meters[i].name;
|
||||||
m["unit"] = meters[i].unit;
|
m["unit"] = meters[i].unit;
|
||||||
m["range"] = meters[i].range;
|
m["rangeMin"] = meters[i].rangeMin;
|
||||||
|
m["rangeMax"] = meters[i].rangeMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = LittleFS.open("/config.json", "w");
|
File f = LittleFS.open("/config.json", "w");
|
||||||
@@ -140,6 +143,20 @@ static void applyMeters() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Range mapping
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static float pctToPhysical(float pct, int idx) {
|
||||||
|
return pct / 100.0f * (meters[idx].rangeMax - meters[idx].rangeMin) + meters[idx].rangeMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float physicalToPct(float physical, int idx) {
|
||||||
|
float range = meters[idx].rangeMax - meters[idx].rangeMin;
|
||||||
|
if (range == 0) return 0;
|
||||||
|
return constrain((physical - meters[idx].rangeMin) / range * 100.0f, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// MQTT
|
// MQTT
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -166,10 +183,10 @@ static void mqttCallback(char* topic, byte* payload, unsigned int len) {
|
|||||||
String suffix = t.substring(slash);
|
String suffix = t.substring(slash);
|
||||||
|
|
||||||
if (suffix == "/current/set") {
|
if (suffix == "/current/set") {
|
||||||
Serial.printf("[MQTT] set meter%d current=%.1f\n", idx, val);
|
Serial.printf("[MQTT] set meter%d physical=%.1f\n", idx, val);
|
||||||
meters[idx].currentValue = val;
|
meters[idx].currentValue = physicalToPct(val, idx);
|
||||||
if (meters[idx].pin > 0 && meters[idx].maxDuty > 0) {
|
if (meters[idx].pin > 0 && meters[idx].maxDuty > 0) {
|
||||||
float pct = val / 100.0f * meters[idx].maxDuty / 100.0f;
|
float pct = meters[idx].currentValue / 100.0f * meters[idx].maxDuty / 100.0f;
|
||||||
ledcWrite(idx, constrain((int)(pct * 1023), 0, 1023));
|
ledcWrite(idx, constrain((int)(pct * 1023), 0, 1023));
|
||||||
}
|
}
|
||||||
saveConfig();
|
saveConfig();
|
||||||
@@ -181,7 +198,8 @@ static void mqttPublishCurrent(int idx) {
|
|||||||
if (!mqttCfg.enabled || !mqttClient.connected()) return;
|
if (!mqttCfg.enabled || !mqttClient.connected()) return;
|
||||||
char topic[128], val[16];
|
char topic[128], val[16];
|
||||||
snprintf(topic, sizeof(topic), "%s/meter/%d/current", mqttCfg.prefix, idx);
|
snprintf(topic, sizeof(topic), "%s/meter/%d/current", mqttCfg.prefix, idx);
|
||||||
snprintf(val, sizeof(val), "%.1f", meters[idx].currentValue);
|
float physical = pctToPhysical(meters[idx].currentValue, idx);
|
||||||
|
snprintf(val, sizeof(val), "%.1f", physical);
|
||||||
bool ok = mqttClient.publish(topic, val, true);
|
bool ok = mqttClient.publish(topic, val, true);
|
||||||
Serial.printf("[MQTT] publish topic=%s val=%s ok=%d\n", topic, val, ok);
|
Serial.printf("[MQTT] publish topic=%s val=%s ok=%d\n", topic, val, ok);
|
||||||
}
|
}
|
||||||
@@ -206,8 +224,8 @@ static void mqttPublishDiscovery() {
|
|||||||
doc["name"] = name;
|
doc["name"] = name;
|
||||||
doc["state_topic"] = stat;
|
doc["state_topic"] = stat;
|
||||||
doc["command_topic"] = stat + "/set";
|
doc["command_topic"] = stat + "/set";
|
||||||
doc["min"] = 0;
|
doc["min"] = meters[i].rangeMin;
|
||||||
doc["max"] = 100;
|
doc["max"] = meters[i].rangeMax;
|
||||||
doc["step"] = 0.1;
|
doc["step"] = 0.1;
|
||||||
if (strlen(meters[i].unit) > 0)
|
if (strlen(meters[i].unit) > 0)
|
||||||
doc["unit_of_measurement"] = meters[i].unit;
|
doc["unit_of_measurement"] = meters[i].unit;
|
||||||
@@ -343,14 +361,16 @@ static void handleRoot() {
|
|||||||
|
|
||||||
String nameVal = escHtml(meters[i].name);
|
String nameVal = escHtml(meters[i].name);
|
||||||
String unitVal = escHtml(meters[i].unit);
|
String unitVal = escHtml(meters[i].unit);
|
||||||
char rangeStr[8];
|
char rMinStr[8], rMaxStr[8];
|
||||||
dtostrf(meters[i].range, 1, 1, rangeStr);
|
dtostrf(meters[i].rangeMin, 1, 1, rMinStr);
|
||||||
|
dtostrf(meters[i].rangeMax, 1, 1, rMaxStr);
|
||||||
|
|
||||||
meterRows += "<fieldset><legend>Meter " + String(i) + "</legend>";
|
meterRows += "<fieldset><legend>Meter " + String(i) + "</legend>";
|
||||||
meterRows += "<div class=row><label>Pin</label><input name=m" + String(i) + "_pin type=number min=0 max=99 value=" + String(meters[i].pin) + "></div>";
|
meterRows += "<div class=row><label>Pin</label><input name=m" + String(i) + "_pin type=number min=0 max=99 value=" + String(meters[i].pin) + "></div>";
|
||||||
meterRows += "<div class=row><label>Name</label><input name=m" + String(i) + "_name value='" + nameVal + "'></div>";
|
meterRows += "<div class=row><label>Name</label><input name=m" + String(i) + "_name value='" + nameVal + "'></div>";
|
||||||
meterRows += "<div class=row><label>Unit</label><input name=m" + String(i) + "_unit value='" + unitVal + "'></div>";
|
meterRows += "<div class=row><label>Unit</label><input name=m" + String(i) + "_unit value='" + unitVal + "'></div>";
|
||||||
meterRows += "<div class=row><label>Range</label><input name=m" + String(i) + "_range type=number step=any min=0 value=" + String(rangeStr) + "></div>";
|
meterRows += "<div class=row><label>Min</label><input name=m" + String(i) + "_rangeMin type=number step=any value=" + String(rMinStr) + "></div>";
|
||||||
|
meterRows += "<div class=row><label>Max</label><input name=m" + String(i) + "_rangeMax type=number step=any value=" + String(rMaxStr) + "></div>";
|
||||||
meterRows += "<div class=row><label>Max Duty</label><input name=m" + String(i) + "_maxD type=number step=any min=0 max=100 value=" + String(maxStr) + "></div>";
|
meterRows += "<div class=row><label>Max Duty</label><input name=m" + String(i) + "_maxD type=number step=any min=0 max=100 value=" + String(maxStr) + "></div>";
|
||||||
meterRows += "<div class=slider-row>";
|
meterRows += "<div class=slider-row>";
|
||||||
meterRows += "<label>Output</label>";
|
meterRows += "<label>Output</label>";
|
||||||
@@ -489,8 +509,10 @@ static void handleConfig() {
|
|||||||
strlcpy(newMeters[i].name, server.arg(pf + "name").c_str(), sizeof(newMeters[i].name));
|
strlcpy(newMeters[i].name, server.arg(pf + "name").c_str(), sizeof(newMeters[i].name));
|
||||||
if (server.hasArg(pf + "unit"))
|
if (server.hasArg(pf + "unit"))
|
||||||
strlcpy(newMeters[i].unit, server.arg(pf + "unit").c_str(), sizeof(newMeters[i].unit));
|
strlcpy(newMeters[i].unit, server.arg(pf + "unit").c_str(), sizeof(newMeters[i].unit));
|
||||||
if (server.hasArg(pf + "range"))
|
if (server.hasArg(pf + "rangeMin"))
|
||||||
newMeters[i].range = server.arg(pf + "range").toFloat();
|
newMeters[i].rangeMin = server.arg(pf + "rangeMin").toFloat();
|
||||||
|
if (server.hasArg(pf + "rangeMax"))
|
||||||
|
newMeters[i].rangeMax = server.arg(pf + "rangeMax").toFloat();
|
||||||
if (server.hasArg(pf + "maxD"))
|
if (server.hasArg(pf + "maxD"))
|
||||||
newMeters[i].maxDuty = server.arg(pf + "maxD").toFloat();
|
newMeters[i].maxDuty = server.arg(pf + "maxD").toFloat();
|
||||||
if (server.hasArg(pf + "cur"))
|
if (server.hasArg(pf + "cur"))
|
||||||
@@ -500,8 +522,8 @@ static void handleConfig() {
|
|||||||
meterCount = newCount;
|
meterCount = newCount;
|
||||||
memcpy(meters, newMeters, sizeof(meters));
|
memcpy(meters, newMeters, sizeof(meters));
|
||||||
for (int i = 0; i < meterCount; i++)
|
for (int i = 0; i < meterCount; i++)
|
||||||
Serial.printf("[HTTP] meter%d pin=%d name=%s unit=%s range=%.1f maxD=%.1f cur=%.1f\n",
|
Serial.printf("[HTTP] meter%d pin=%d name=%s unit=%s range=[%.1f,%.1f] maxD=%.1f cur=%.1f\n",
|
||||||
i, meters[i].pin, meters[i].name, meters[i].unit, meters[i].range, meters[i].maxDuty, meters[i].currentValue);
|
i, meters[i].pin, meters[i].name, meters[i].unit, meters[i].rangeMin, meters[i].rangeMax, meters[i].maxDuty, meters[i].currentValue);
|
||||||
|
|
||||||
saveConfig();
|
saveConfig();
|
||||||
attachMeters();
|
attachMeters();
|
||||||
|
|||||||
Reference in New Issue
Block a user