192 lines
6.3 KiB
Arduino
192 lines
6.3 KiB
Arduino
/*
|
|
* ================================================================
|
|
* LAN-УЗЕЛ: электроэнергия + реле → Ethernet → MQTT
|
|
* Проекты: Егорин / ORBIT · протокол v2 (nodes/...)
|
|
* ================================================================
|
|
* Железо:
|
|
* - WT32-ETH01 (ESP32 + LAN8720, порт RJ45)
|
|
* - PZEM-004T v3.0 (UART2: RX=IO5, TX=IO17)
|
|
* - Входы состояния реле: IO32, IO33 (внутр. pullup),
|
|
* IO35, IO36 (только вход! внешний pullup 10к на 3.3V)
|
|
* - Выходы управления реле: IO2, IO4, IO14, IO15
|
|
* - Питание платы: 5V (не 3.3 на пин 5V!)
|
|
*
|
|
* Узел регистрируется на сервере сам: при старте публикует
|
|
* retained nodes/<id>/info — провижн-сервис ставит его в
|
|
* «Ожидают добавления», админ подтверждает в интерфейсе.
|
|
*
|
|
* Библиотеки: ArduinoJson, PubSubClient, PZEM004Tv30
|
|
* Плата в IDE: "WT32-ETH01 Ethernet Module" (или ESP32 Dev Module)
|
|
* ================================================================
|
|
*/
|
|
|
|
#include <ETH.h>
|
|
#include <PubSubClient.h>
|
|
#include <PZEM004Tv30.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
// ================== НАСТРОЙКИ УЗЛА ==================
|
|
#define NODE_ID "eg-lan1" // уникальный ID: eg-lan1, orb-lan1...
|
|
#define FW_VER "2.0"
|
|
#define SEND_PERIOD_MS 15000UL // LAN не жалко — можно чаще, чем LoRa
|
|
#define INFO_PERIOD_MS 600000UL // info раз в 10 минут
|
|
|
|
#define MQTT_HOST "192.168.1.200" // Mosquitto (Proxmox srv001)
|
|
#define MQTT_PORT 1883
|
|
#define MQTT_USER ""
|
|
#define MQTT_PASSWD ""
|
|
|
|
// WT32-ETH01: конфигурация LAN8720
|
|
#define ETH_PHY_TYPE ETH_PHY_LAN8720
|
|
#define ETH_PHY_ADDR 1
|
|
#define ETH_PHY_MDC 23
|
|
#define ETH_PHY_MDIO 18
|
|
#define ETH_PHY_POWER 16
|
|
#define ETH_CLK ETH_CLOCK_GPIO0_IN
|
|
|
|
// PZEM на UART2 (WT32-ETH01: свободные IO5/IO17)
|
|
PZEM004Tv30 pzem(Serial2, 5, 17);
|
|
|
|
// Реле
|
|
const uint8_t RELAY_IN[] = {32, 33, 35, 36}; // 35/36 — внешний pullup!
|
|
const uint8_t RELAY_OUT[] = {2, 4, 14, 15};
|
|
const uint8_t N_RELAYS = 4;
|
|
|
|
// Топики
|
|
#define T_DATA "nodes/" NODE_ID "/data"
|
|
#define T_INFO "nodes/" NODE_ID "/info"
|
|
#define T_STATUS "nodes/" NODE_ID "/status"
|
|
#define T_CMD "nodes/" NODE_ID "/cmd"
|
|
|
|
WiFiClient ethClient; // ETH.h использует сетевой стек WiFiClient
|
|
PubSubClient mqtt(ethClient);
|
|
|
|
static bool ethUp = false;
|
|
unsigned long lastSend = 0, lastInfo = 0;
|
|
uint16_t seq = 0;
|
|
|
|
// ================== ETHERNET ==================
|
|
void onEthEvent(WiFiEvent_t event) {
|
|
switch (event) {
|
|
case ARDUINO_EVENT_ETH_GOT_IP:
|
|
ethUp = true;
|
|
Serial.printf("ETH IP: %s\n", ETH.localIP().toString().c_str());
|
|
break;
|
|
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
|
case ARDUINO_EVENT_ETH_STOP:
|
|
ethUp = false;
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
// ================== ПУБЛИКАЦИИ ==================
|
|
void publishInfo() {
|
|
StaticJsonDocument<256> doc;
|
|
doc["id"] = NODE_ID;
|
|
doc["fw"] = FW_VER;
|
|
doc["hw"] = "lan"; // транспорт узла
|
|
doc["ip"] = ETH.localIP().toString();
|
|
doc["mac"] = ETH.macAddress();
|
|
JsonObject caps = doc.createNestedObject("caps");
|
|
caps["pzem"] = true;
|
|
caps["relays"] = N_RELAYS;
|
|
caps["ctl"] = true; // умеет управлять реле
|
|
|
|
char buf[256];
|
|
size_t n = serializeJson(doc, buf);
|
|
mqtt.publish(T_INFO, (const uint8_t*)buf, n, true); // retained
|
|
}
|
|
|
|
void publishTelemetry() {
|
|
StaticJsonDocument<320> doc;
|
|
doc["id"] = NODE_ID;
|
|
doc["seq"] = seq++;
|
|
|
|
float v = pzem.voltage();
|
|
if (!isnan(v)) {
|
|
doc["v"] = serialized(String(v, 1));
|
|
doc["a"] = serialized(String(pzem.current(), 3));
|
|
doc["w"] = serialized(String(pzem.power(), 1));
|
|
doc["kwh"] = serialized(String(pzem.energy(), 3));
|
|
doc["pf"] = serialized(String(pzem.pf(), 2));
|
|
doc["hz"] = serialized(String(pzem.frequency(), 1));
|
|
} else {
|
|
doc["err"] = "pzem";
|
|
}
|
|
|
|
JsonArray r = doc.createNestedArray("r");
|
|
for (uint8_t i = 0; i < N_RELAYS; i++)
|
|
r.add(digitalRead(RELAY_IN[i]) == LOW ? 1 : 0);
|
|
|
|
char buf[320];
|
|
size_t n = serializeJson(doc, buf);
|
|
mqtt.publish(T_DATA, (const uint8_t*)buf, n);
|
|
Serial.printf("PUB %s: %s\n", T_DATA, buf);
|
|
}
|
|
|
|
// ================== КОМАНДЫ ==================
|
|
// nodes/<id>/cmd: {"cmd":"relay","n":0,"s":1} | {"cmd":"poll"}
|
|
void mqttCallback(char *topic, byte *payload, unsigned int len) {
|
|
StaticJsonDocument<192> doc;
|
|
if (deserializeJson(doc, payload, len)) return;
|
|
const char *cmd = doc["cmd"];
|
|
if (!cmd) return;
|
|
|
|
if (!strcmp(cmd, "relay")) {
|
|
int n = doc["n"] | -1;
|
|
int s = doc["s"] | 0;
|
|
if (n >= 0 && n < N_RELAYS) {
|
|
digitalWrite(RELAY_OUT[n], s ? HIGH : LOW);
|
|
delay(200);
|
|
publishTelemetry(); // подтверждаем фактом
|
|
}
|
|
} else if (!strcmp(cmd, "poll")) {
|
|
publishTelemetry();
|
|
}
|
|
}
|
|
|
|
void mqttReconnect() {
|
|
while (ethUp && !mqtt.connected()) {
|
|
if (mqtt.connect(NODE_ID, MQTT_USER, MQTT_PASSWD,
|
|
T_STATUS, 1, true, "offline")) {
|
|
mqtt.publish(T_STATUS, "online", true); // LWT-пара
|
|
mqtt.subscribe(T_CMD);
|
|
publishInfo(); // саморегистрация
|
|
Serial.println("MQTT OK");
|
|
} else {
|
|
Serial.printf("MQTT rc=%d, retry 5s\n", mqtt.state());
|
|
delay(5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================== SETUP / LOOP ==================
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
|
|
for (uint8_t i = 0; i < N_RELAYS; i++) {
|
|
pinMode(RELAY_IN[i], (RELAY_IN[i] >= 34) ? INPUT : INPUT_PULLUP);
|
|
pinMode(RELAY_OUT[i], OUTPUT);
|
|
digitalWrite(RELAY_OUT[i], LOW);
|
|
}
|
|
|
|
WiFi.onEvent(onEthEvent);
|
|
ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC,
|
|
ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK);
|
|
|
|
mqtt.setServer(MQTT_HOST, MQTT_PORT);
|
|
mqtt.setCallback(mqttCallback);
|
|
mqtt.setBufferSize(512);
|
|
}
|
|
|
|
void loop() {
|
|
if (!ethUp) { delay(200); return; }
|
|
if (!mqtt.connected()) mqttReconnect();
|
|
mqtt.loop();
|
|
|
|
unsigned long now = millis();
|
|
if (now - lastSend >= SEND_PERIOD_MS) { lastSend = now; publishTelemetry(); }
|
|
if (now - lastInfo >= INFO_PERIOD_MS) { lastInfo = now; publishInfo(); }
|
|
}
|