153 lines
4.2 KiB
Arduino
153 lines
4.2 KiB
Arduino
/*
|
|
* ================================================================
|
|
* LoRa-УЗЕЛ v2: электроэнергия + реле → LoRa → шлюз
|
|
* Отличия от v1:
|
|
* - пакет info при старте и раз в 10 мин (саморегистрация)
|
|
* - поле "t": "info" | "d" — шлюз раскладывает по топикам nodes/
|
|
* Железо и распиновка — как в v1 (ESP32 DevKit + PZEM + RA-01H):
|
|
* PZEM: RX←GPIO17, TX→GPIO16 (Serial2)
|
|
* LoRa: SCK=18 MISO=19 MOSI=23 NSS=5 RST=14 DIO0=26
|
|
* Входы реле: 32, 33, 25, 27 · Выходы: 4, 13, 21, 22
|
|
* ================================================================
|
|
*/
|
|
|
|
#include <SPI.h>
|
|
#include <LoRa.h>
|
|
#include <PZEM004Tv30.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
#define NODE_ID "eg01"
|
|
#define FW_VER "2.0"
|
|
#define SEND_PERIOD_MS 30000UL
|
|
#define INFO_PERIOD_MS 600000UL
|
|
#define LORA_FREQ 869000000E0
|
|
#define LORA_SF 9
|
|
#define LORA_BW 125E3
|
|
#define LORA_SYNC 0x2B
|
|
#define LORA_TX_DBM 20
|
|
#define LORA_NSS 5
|
|
#define LORA_RST 14
|
|
#define LORA_DIO0 26
|
|
|
|
const uint8_t RELAY_IN[] = {32, 33, 25, 27};
|
|
const uint8_t RELAY_OUT[] = {4, 13, 21, 22};
|
|
const uint8_t N_RELAYS = 4;
|
|
|
|
PZEM004Tv30 pzem(Serial2, 16, 17);
|
|
unsigned long lastSend = 0, lastInfo = 0;
|
|
uint16_t seq = 0;
|
|
|
|
void loraSend(const char *buf, size_t n) {
|
|
LoRa.beginPacket();
|
|
LoRa.write((const uint8_t*)buf, n);
|
|
LoRa.endPacket();
|
|
LoRa.receive();
|
|
}
|
|
|
|
void sendInfo() {
|
|
StaticJsonDocument<224> doc;
|
|
doc["id"] = NODE_ID;
|
|
doc["t"] = "info";
|
|
doc["fw"] = FW_VER;
|
|
doc["hw"] = "lora";
|
|
JsonObject caps = doc.createNestedObject("caps");
|
|
caps["pzem"] = true;
|
|
caps["relays"] = N_RELAYS;
|
|
caps["ctl"] = true;
|
|
char buf[224];
|
|
loraSend(buf, serializeJson(doc, buf));
|
|
Serial.println("TX info");
|
|
}
|
|
|
|
void sendTelemetry() {
|
|
StaticJsonDocument<288> doc;
|
|
doc["id"] = NODE_ID;
|
|
doc["t"] = "d";
|
|
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[288];
|
|
size_t n = serializeJson(doc, buf);
|
|
loraSend(buf, n);
|
|
Serial.printf("TX: %s\n", buf);
|
|
}
|
|
|
|
void handleCommand(const String &payload) {
|
|
StaticJsonDocument<192> doc;
|
|
if (deserializeJson(doc, payload)) return;
|
|
if (String((const char*)doc["id"]) != NODE_ID) 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);
|
|
sendTelemetry();
|
|
}
|
|
} else if (!strcmp(cmd, "poll")) {
|
|
sendTelemetry();
|
|
} else if (!strcmp(cmd, "info")) {
|
|
sendInfo();
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
for (uint8_t i = 0; i < N_RELAYS; i++) {
|
|
pinMode(RELAY_IN[i], INPUT_PULLUP);
|
|
pinMode(RELAY_OUT[i], OUTPUT);
|
|
digitalWrite(RELAY_OUT[i], LOW);
|
|
}
|
|
|
|
LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0);
|
|
if (!LoRa.begin(LORA_FREQ)) { Serial.println("LoRa FAIL"); while (1) delay(1000); }
|
|
LoRa.setSpreadingFactor(LORA_SF);
|
|
LoRa.setSignalBandwidth(LORA_BW);
|
|
LoRa.setSyncWord(LORA_SYNC);
|
|
LoRa.setTxPower(LORA_TX_DBM);
|
|
LoRa.enableCrc();
|
|
LoRa.receive();
|
|
|
|
randomSeed(esp_random());
|
|
lastSend = millis() - random(0, SEND_PERIOD_MS);
|
|
delay(random(0, 3000)); // рассинхронизация info при массовом включении
|
|
sendInfo();
|
|
lastInfo = millis();
|
|
}
|
|
|
|
void loop() {
|
|
int sz = LoRa.parsePacket();
|
|
if (sz > 0) {
|
|
String s;
|
|
while (LoRa.available()) s += (char)LoRa.read();
|
|
handleCommand(s);
|
|
}
|
|
|
|
unsigned long now = millis();
|
|
if (now - lastSend >= SEND_PERIOD_MS + random(0, 2000)) {
|
|
lastSend = now; sendTelemetry();
|
|
}
|
|
if (now - lastInfo >= INFO_PERIOD_MS) {
|
|
lastInfo = now; sendInfo();
|
|
}
|
|
}
|