Files
egorin/lora/node_lan_energy.ino
T

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(); }
}