MQTT - not working properly yet

This commit is contained in:
2026-06-14 22:08:16 +02:00
parent 42a4c89eee
commit 955fd24df6
2 changed files with 256 additions and 82 deletions

View File

@@ -16,3 +16,4 @@ monitor_speed = 115200
lib_deps = 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

View File

@@ -4,6 +4,8 @@
#include <LittleFS.h> #include <LittleFS.h>
#include <WebServer.h> #include <WebServer.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#define HOSTNAME_DEFAULT "m1730" #define HOSTNAME_DEFAULT "m1730"
#define MAX_METERS 10 #define MAX_METERS 10
@@ -16,11 +18,24 @@ struct MeterConfig {
float currentValue; float currentValue;
}; };
struct MqttConfig {
bool enabled = false;
char host[64] = "";
uint16_t port = 1883;
char user[32] = "";
char pass[32] = "";
char prefix[32] = "m1730";
};
static char hostname[32] = HOSTNAME_DEFAULT; static char hostname[32] = HOSTNAME_DEFAULT;
static int meterCount = 0; static int meterCount = 0;
static MeterConfig meters[MAX_METERS] = {}; static MeterConfig meters[MAX_METERS] = {};
static MqttConfig mqttCfg;
static WiFiManagerParameter hostnameParam("hostname", "Device hostname", hostname, 32); static WiFiManagerParameter hostnameParam("hostname", "Device hostname", hostname, 32);
static WebServer server(80); static WebServer server(80);
static WiFiClient wifiClient;
static PubSubClient mqttClient(wifiClient);
static unsigned long mqttReconnectAt = 0;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Config persistence (JSON via LittleFS) // Config persistence (JSON via LittleFS)
@@ -39,6 +54,16 @@ static void loadConfig() {
if (doc["hostname"].is<const char*>()) if (doc["hostname"].is<const char*>())
strlcpy(hostname, doc["hostname"], sizeof(hostname)); strlcpy(hostname, doc["hostname"], sizeof(hostname));
JsonObject mq = doc["mqtt"];
if (!mq.isNull()) {
mqttCfg.enabled = mq["en"] | false;
strlcpy(mqttCfg.host, mq["host"] | "", sizeof(mqttCfg.host));
mqttCfg.port = mq["port"] | 1883;
strlcpy(mqttCfg.user, mq["user"] | "", sizeof(mqttCfg.user));
strlcpy(mqttCfg.pass, mq["pass"] | "", sizeof(mqttCfg.pass));
strlcpy(mqttCfg.prefix, mq["prefix"] | "m1730", sizeof(mqttCfg.prefix));
}
JsonArray arr = doc["meters"].as<JsonArray>(); JsonArray arr = doc["meters"].as<JsonArray>();
meterCount = min((int)arr.size(), MAX_METERS); meterCount = min((int)arr.size(), MAX_METERS);
for (int i = 0; i < meterCount; i++) { for (int i = 0; i < meterCount; i++) {
@@ -53,6 +78,14 @@ static void saveConfig() {
JsonDocument doc; JsonDocument doc;
doc["hostname"] = hostname; doc["hostname"] = hostname;
JsonObject mq = doc["mqtt"].to<JsonObject>();
mq["en"] = mqttCfg.enabled;
mq["host"] = mqttCfg.host;
mq["port"] = mqttCfg.port;
mq["user"] = mqttCfg.user;
mq["pass"] = mqttCfg.pass;
mq["prefix"] = mqttCfg.prefix;
JsonArray arr = doc["meters"].to<JsonArray>(); JsonArray arr = doc["meters"].to<JsonArray>();
for (int i = 0; i < meterCount; i++) { for (int i = 0; i < meterCount; i++) {
JsonObject m = arr.add<JsonObject>(); JsonObject m = arr.add<JsonObject>();
@@ -90,6 +123,109 @@ static void applyMeters() {
} }
} }
// ---------------------------------------------------------------------------
// MQTT
// ---------------------------------------------------------------------------
static void mqttCallback(char* topic, byte* payload, unsigned int len) {
String valStr;
for (unsigned i = 0; i < len; i++) valStr += (char)payload[i];
float val = valStr.toFloat();
// topic format: <prefix>/meter/<idx>/current/set or .../maxduty/set
String t = String(topic);
String pref = String(mqttCfg.prefix) + "/meter/";
if (!t.startsWith(pref)) return;
t = t.substring(pref.length());
int slash = t.indexOf('/');
if (slash < 0) return;
int idx = t.substring(0, slash).toInt();
if (idx < 0 || idx >= meterCount) return;
String suffix = t.substring(slash);
if (suffix == "/current/set") {
meters[idx].currentValue = val;
if (meters[idx].pin > 0 && meters[idx].maxDuty > 0) {
float pct = val / 100.0f * meters[idx].maxDuty / 100.0f;
ledcWrite(idx, constrain((int)(pct * 1023), 0, 1023));
}
saveConfig();
} else if (suffix == "/maxduty/set") {
meters[idx].maxDuty = constrain(val, 0, 100);
attachMeters();
applyMeters();
saveConfig();
}
}
static void mqttPublishCurrent(int idx) {
if (!mqttCfg.enabled || !mqttClient.connected()) return;
char topic[128], val[16];
snprintf(topic, sizeof(topic), "%s/meter/%d/current", mqttCfg.prefix, idx);
snprintf(val, sizeof(val), "%.1f", meters[idx].currentValue);
mqttClient.publish(topic, val, true);
}
static void mqttSubscribe() {
if (!mqttCfg.enabled) return;
for (int i = 0; i < meterCount; i++) {
char t[128];
snprintf(t, sizeof(t), "%s/meter/%d/current/set", mqttCfg.prefix, i);
mqttClient.subscribe(t);
snprintf(t, sizeof(t), "%s/meter/%d/maxduty/set", mqttCfg.prefix, i);
mqttClient.subscribe(t);
}
}
static bool mqttConnect() {
if (!mqttCfg.enabled || strlen(mqttCfg.host) == 0 || strlen(mqttCfg.user) == 0 || strlen(mqttCfg.pass) == 0) return false;
char clientId[32];
{
uint8_t mac[6];
WiFi.macAddress(mac);
snprintf(clientId, sizeof(clientId), "m1730-%02x%02x%02x", mac[3], mac[4], mac[5]);
}
mqttClient.setServer(mqttCfg.host, mqttCfg.port);
mqttClient.setCallback(mqttCallback);
bool ok = mqttClient.connect(clientId, mqttCfg.user, mqttCfg.pass);
if (ok) {
Serial.printf("MQTT connected to %s:%d\n", mqttCfg.host, mqttCfg.port);
mqttSubscribe();
} else {
Serial.printf("MQTT failed rc=%d\n", mqttClient.state());
}
return ok;
}
static void mqttLoop() {
if (!mqttCfg.enabled || strlen(mqttCfg.host) == 0 || strlen(mqttCfg.user) == 0 || strlen(mqttCfg.pass) == 0) return;
if (!mqttClient.connected()) {
unsigned long now = millis();
if (now > mqttReconnectAt) {
// quick TCP probe with short timeout before the blocking MQTT connect
WiFiClient probe;
bool reachable = probe.connect(mqttCfg.host, mqttCfg.port, 1500);
probe.stop();
if (reachable) {
if (mqttConnect()) mqttReconnectAt = 0;
else mqttReconnectAt = now + 30000;
} else {
mqttReconnectAt = now + 30000;
}
}
} else {
mqttClient.loop();
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// mDNS // mDNS
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -103,6 +239,24 @@ static void startMDNS() {
} }
} }
// ---------------------------------------------------------------------------
// Web helpers
// ---------------------------------------------------------------------------
static String escHtml(const String& s) {
String out;
for (unsigned i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&': out += "&amp;"; break;
case '\'': out += "&#39;"; break;
case '"': out += "&quot;"; break;
default: out += c;
}
}
return out;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Web handlers // Web handlers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -131,89 +285,84 @@ static void handleRoot() {
countOpts += ">" + String(i) + "</option>"; countOpts += ">" + String(i) + "</option>";
} }
String html = R"( String mqttSection;
<!DOCTYPE html> {
<html> char portStr[8];
<head> snprintf(portStr, sizeof(portStr), "%u", mqttCfg.port);
<meta charset=utf-8> String checked = mqttCfg.enabled ? " checked" : "";
<meta name=viewport content='width=device-width,initial-scale=1'>
<title>M1730</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,sans-serif;background:#f2f4f8;color:#1a1a2e;display:flex;justify-content:center;align-items:flex-start;min-height:100vh;padding:24px 16px}
.card{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.08);padding:32px;width:100%;max-width:520px}
h1{font-size:24px;margin-bottom:4px}
.sub{color:#666;font-size:14px;margin-bottom:24px}
.info{background:#f8f9fc;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:14px}
.info div{display:flex;justify-content:space-between;padding:4px 0}
.info div span:first-child{color:#666}
label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}
input,select{width:100%;padding:9px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;outline:none;transition:border-color .2s}
input:focus,select:focus{border-color:#4361ee;box-shadow:0 0 0 3px rgba(67,97,238,.15)}
fieldset{border:1px solid #e5e7eb;border-radius:10px;padding:16px;margin-bottom:16px}
legend{font-weight:600;font-size:14px;padding:0 6px;color:#4361ee}
fieldset .row{display:flex;gap:8px;align-items:center;margin-bottom:10px}
fieldset .row label{width:60px;margin-bottom:0;flex-shrink:0}
.slider-row{display:flex;gap:8px;align-items:center}
.slider-row label{width:60px;margin-bottom:0;flex-shrink:0}
.slider-row input[type=range]{flex:1;padding:0;border:none;box-shadow:none;accent-color:#4361ee}
.slider-row input[type=range]:focus{box-shadow:none}
.val{font-size:14px;font-weight:600;min-width:48px;text-align:right;color:#4361ee}
.count-row{display:flex;gap:12px;align-items:flex-end;margin-bottom:20px}
.count-row > div{flex:1}
.count-row label{margin-bottom:4px}
.btn{width:100%;margin-top:8px;padding:12px;background:#4361ee;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:background .2s}
.btn:hover{background:#3651d4}
.mac{font-size:12px;color:#999;text-align:center;margin-top:20px}
</style>
</head>
<body>
<div class=card>
<h1>M1730</h1>
<div class=sub>Configuration</div>
<div class=info>
<div><span>Hostname</span><span>)" + String(hostname) + R"(</span></div>
<div><span>IP</span><span>)" + WiFi.localIP().toString() + R"(</span></div>
</div>
<form action=/config method=post> mqttSection +=
<label>Hostname</label> "<fieldset><legend>MQTT</legend>"
<input name=hostname value=')" + String(hostname) + R"('> "<div class=row>"
"<label class=tgl-lbl>Enable</label>"
"<label class=switch><input type=checkbox name=mqtt_en" + checked + "><span class=slid></span></label>"
"</div>";
<div class=count-row> String hostVal = escHtml(mqttCfg.host);
<div> String userVal = escHtml(mqttCfg.user);
<label>Meters</label> String passVal = escHtml(mqttCfg.pass);
<select name=meter_count onchange='this.form.submit()'>)" + countOpts + R"(</select> String prefVal = escHtml(mqttCfg.prefix);
</div>
</div>
)" + meterRows + R"( mqttSection +=
"<div class=row><label>Broker</label><input name=mqtt_host value='" + hostVal + "'></div>"
<button class=btn type=submit>Save</button> "<div class=row><label>Port</label><input name=mqtt_port type=number value=" + String(portStr) + "></div>"
</form> "<div class=row><label>User</label><input name=mqtt_user value='" + userVal + "'></div>"
"<div class=row><label>Pass</label><input name=mqtt_pass type=password value='" + passVal + "'></div>"
<div class=mac>)" + WiFi.macAddress() + R"(</div> "<div class=row><label>Prefix</label><input name=mqtt_prefix value='" + prefVal + "'></div>";
</div> mqttSection += "</fieldset>";
<script>
Array.from(document.querySelectorAll('input[type=range]')).forEach(function(s){
var span = document.getElementById(s.name.replace('_cur',''));
if(span){
s.addEventListener('input',function(){
span.textContent=this.value+'%';
var x=new XMLHttpRequest();
x.open('GET','/set?i='+this.name.match(/\d+/)[0]+'&v='+this.value,true);
x.send();
});
} }
});
document.querySelector('select[name=meter_count]').addEventListener('change',function(){ String html;
this.form.submit(); html.reserve(4096);
}); html += "<!DOCTYPE html><html><head><meta charset=utf-8>";
</script> html += "<meta name=viewport content='width=device-width,initial-scale=1'><title>M1730</title><style>";
</body> html += "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}";
</html> html += "body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen,Ubuntu,sans-serif;background:#f2f4f8;color:#1a1a2e;display:flex;justify-content:center;align-items:flex-start;min-height:100vh;padding:24px 16px}";
)"; html += ".card{background:#fff;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.08);padding:32px;width:100%;max-width:560px}";
html += "h1{font-size:24px;margin-bottom:4px}.sub{color:#666;font-size:14px;margin-bottom:24px}";
html += ".info{background:#f8f9fc;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:14px}";
html += ".info div{display:flex;justify-content:space-between;padding:4px 0}.info div span:first-child{color:#666}";
html += "label{display:block;font-size:14px;font-weight:600;margin-bottom:4px}";
html += "input,select{width:100%;padding:9px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;outline:none;transition:border-color .2s}";
html += "input:focus,select:focus{border-color:#4361ee;box-shadow:0 0 0 3px rgba(67,97,238,.15)}";
html += "fieldset{border:1px solid #e5e7eb;border-radius:10px;padding:16px;margin-bottom:16px}";
html += "legend{font-weight:600;font-size:14px;padding:0 6px;color:#4361ee}";
html += "fieldset .row{display:flex;gap:8px;align-items:center;margin-bottom:8px}";
html += "fieldset .row label{width:70px;margin-bottom:0;flex-shrink:0}.slider-row{display:flex;gap:8px;align-items:center}";
html += ".slider-row label{width:70px;margin-bottom:0;flex-shrink:0}";
html += ".slider-row input[type=range]{flex:1;padding:0;border:none;box-shadow:none;accent-color:#4361ee}";
html += ".slider-row input[type=range]:focus{box-shadow:none}";
html += ".val{font-size:14px;font-weight:600;min-width:48px;text-align:right;color:#4361ee}";
html += ".count-row{display:flex;gap:12px;align-items:flex-end;margin-bottom:20px}.count-row > div{flex:1}";
html += ".count-row label{margin-bottom:4px}";
html += ".btn{width:100%;margin-top:8px;padding:12px;background:#4361ee;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:background .2s}";
html += ".btn:hover{background:#3651d4}.mac{font-size:12px;color:#999;text-align:center;margin-top:20px}";
html += ".tgl-lbl{margin-bottom:0!important;line-height:28px}.switch{position:relative;display:inline-block;width:44px;height:24px;margin-left:auto;flex-shrink:0}";
html += ".switch input{opacity:0;width:0;height:0}.slid{position:absolute;cursor:pointer;inset:0;background:#ccc;border-radius:24px;transition:.3s}";
html += ".slid::before{content:\"\";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s}";
html += ".switch input:checked+.slid{background:#4361ee}.switch input:checked+.slid::before{transform:translateX(20px)}";
html += "</style></head><body><div class=card>";
html += "<h1>M1730</h1><div class=sub>Configuration</div><div class=info>";
html += "<div><span>Hostname</span><span>" + String(hostname) + "</span></div>";
html += "<div><span>IP</span><span>" + WiFi.localIP().toString() + "</span></div></div>";
html += "<form action=/config method=post><label>Hostname</label>";
html += "<input name=hostname value='" + String(hostname) + "'>";
html += "<div class=count-row><div><label>Meters</label>";
html += "<select name=meter_count onchange='this.form.submit()'>" + countOpts + "</select></div></div>";
html += meterRows;
html += mqttSection;
html += "<button class=btn type=submit>Save</button></form>";
html += "<div class=mac>" + WiFi.macAddress() + "</div></div>";
html += "<script>";
html += "Array.from(document.querySelectorAll('input[type=range]')).forEach(function(s){";
html += "var span=document.getElementById(s.name.replace('_cur',''));";
html += "if(span){s.addEventListener('input',function(){";
html += "span.textContent=this.value+'%';";
html += "var x=new XMLHttpRequest();";
html += "x.open('GET','/set?i='+this.name.match(/\\d+/)[0]+'&v='+this.value,true);x.send();";
html += "});}});";
html += "document.querySelector('select[name=meter_count]').addEventListener('change',function(){this.form.submit();});";
html += "</script></body></html>";
server.send(200, "text/html", html); server.send(200, "text/html", html);
} }
@@ -225,6 +374,22 @@ static void handleConfig() {
val.toCharArray(hostname, sizeof(hostname)); val.toCharArray(hostname, sizeof(hostname));
} }
if (server.hasArg("mqtt_en")) {
mqttCfg.enabled = true;
if (server.hasArg("mqtt_host"))
strlcpy(mqttCfg.host, server.arg("mqtt_host").c_str(), sizeof(mqttCfg.host));
if (server.hasArg("mqtt_port"))
mqttCfg.port = server.arg("mqtt_port").toInt();
if (server.hasArg("mqtt_user"))
strlcpy(mqttCfg.user, server.arg("mqtt_user").c_str(), sizeof(mqttCfg.user));
if (server.hasArg("mqtt_pass"))
strlcpy(mqttCfg.pass, server.arg("mqtt_pass").c_str(), sizeof(mqttCfg.pass));
if (server.hasArg("mqtt_prefix"))
strlcpy(mqttCfg.prefix, server.arg("mqtt_prefix").c_str(), sizeof(mqttCfg.prefix));
} else {
mqttCfg.enabled = false;
}
int newCount = meterCount; int newCount = meterCount;
if (server.hasArg("meter_count")) if (server.hasArg("meter_count"))
newCount = constrain(server.arg("meter_count").toInt(), 1, MAX_METERS); newCount = constrain(server.arg("meter_count").toInt(), 1, MAX_METERS);
@@ -247,6 +412,10 @@ static void handleConfig() {
attachMeters(); attachMeters();
applyMeters(); applyMeters();
// let mqttLoop handle connection to avoid blocking the HTTP handler
if (mqttClient.connected()) mqttClient.disconnect();
mqttReconnectAt = 0;
server.sendHeader("Location", "/"); server.sendHeader("Location", "/");
server.send(303); server.send(303);
} }
@@ -263,6 +432,7 @@ static void handleSet() {
ledcWrite(idx, duty); ledcWrite(idx, duty);
} }
saveConfig(); saveConfig();
mqttPublishCurrent(idx);
server.send(200); server.send(200);
} }
@@ -283,6 +453,7 @@ void setup() {
LittleFS.begin(true); LittleFS.begin(true);
loadConfig(); loadConfig();
attachMeters(); attachMeters();
applyMeters(); applyMeters();
@@ -304,6 +475,7 @@ void setup() {
saveConfig(); saveConfig();
startServer(); startServer();
startMDNS(); startMDNS();
mqttReconnectAt = 0; // handshake deferred to mqttLoop in loop()
Serial.printf("Board address: http://%s.local or http://%s\n", Serial.printf("Board address: http://%s.local or http://%s\n",
hostname, WiFi.localIP().toString().c_str()); hostname, WiFi.localIP().toString().c_str());
@@ -311,4 +483,5 @@ void setup() {
void loop() { void loop() {
server.handleClient(); server.handleClient();
mqttLoop();
} }