Files
cacto bb85c617a4 Добавлен локальный веб-интерфейс и распиновка в README
Плата поднимает страницу + JSON API (/api/state, /api/command) на порту 80
для наблюдения и управления актуаторами независимо от MQTT/Laravel.
Команда с веб-UI публикует тот же ack в devices/{id}/ack, что и MQTT-команда,
поэтому device shadow платформы не расходится с реальным состоянием.
Включается/выключается флагом ENABLE_WEB_UI в config.h.

В README добавлены ASCII-схема подключения DHT22/реле и раздел про сборку
через arduino-cli (включая обходной путь для проблемы с правами на
~/Documents/Arduino на macOS).
2026-08-11 19:27:41 +05:00

100 lines
2.5 KiB
C++

#include "mqtt_link.h"
#include <ArduinoJson.h>
#include <PubSubClient.h>
#include <WiFi.h>
#include "config.h"
#include "devices.h"
#include "time_sync.h"
static WiFiClient wifiClient;
static PubSubClient mqttClient(wifiClient);
// devices/{device_id}/commands -> device_id, или "" если топик не подходит.
static String deviceIdFromCommandTopic(const String& topic) {
if (!topic.startsWith("devices/") || !topic.endsWith("/commands")) {
return "";
}
return topic.substring(8, topic.length() - 9); // strip "devices/" and "/commands"
}
void mqttPublishAck(const String& deviceId, JsonDocument& state) {
JsonDocument ackDoc;
ackDoc["state"] = state.as<JsonObject>();
char buf[256];
size_t len = serializeJson(ackDoc, buf, sizeof(buf));
String topic = "devices/" + deviceId + "/ack";
mqttClient.publish(topic.c_str(), (const uint8_t*)buf, len, false);
}
static void onMessage(char* topic, byte* payload, unsigned int length) {
String deviceId = deviceIdFromCommandTopic(String(topic));
if (deviceId.isEmpty()) {
return;
}
JsonDocument cmd;
DeserializationError err = deserializeJson(cmd, payload, length);
if (err) {
Serial.print("[mqtt] invalid command payload on ");
Serial.println(topic);
return;
}
String action = cmd["action"] | "";
double level = cmd["level"] | 0.0;
if (action.isEmpty()) {
Serial.print("[mqtt] command missing action on ");
Serial.println(topic);
return;
}
JsonDocument state;
if (devicesApplyCommand(deviceId, action, level, state)) {
mqttPublishAck(deviceId, state);
}
}
void mqttBegin() {
mqttClient.setServer(MQTT_BROKER_HOST, MQTT_BROKER_PORT);
mqttClient.setCallback(onMessage);
}
void mqttEnsureConnected() {
if (mqttClient.connected()) {
return;
}
Serial.print("[mqtt] connecting to ");
Serial.print(MQTT_BROKER_HOST);
if (mqttClient.connect(MQTT_CLIENT_ID)) {
Serial.println(" connected");
mqttClient.subscribe("devices/+/commands", 1);
} else {
Serial.print(" failed, rc=");
Serial.println(mqttClient.state());
}
}
void mqttLoop() {
mqttClient.loop();
}
void mqttPublishTelemetry(const String& deviceId, const String& sensorType, double value) {
JsonDocument doc;
doc["device_id"] = deviceId;
doc["zone_id"] = ZONE_ID;
doc["sensor_type"] = sensorType;
doc["value"] = value;
doc["timestamp"] = nowRFC3339();
char buf[256];
size_t len = serializeJson(doc, buf, sizeof(buf));
String topic = "devices/" + deviceId + "/telemetry";
mqttClient.publish(topic.c_str(), (const uint8_t*)buf, len, false);
}