165 lines
4.9 KiB
Arduino
165 lines
4.9 KiB
Arduino
/*
|
|
* ================================================================
|
|
* ШЛЮЗ v2: LoRa ↔ Ethernet (W5500) ↔ MQTT
|
|
* Проекты: Егорин / ORBIT · протокол v2 (nodes/...)
|
|
* ================================================================
|
|
* Железо:
|
|
* - ESP32 DevKit V1
|
|
* - LoRa RA-01H (SX1276, 869 МГц)
|
|
* - Ethernet W5500 mini (SPI)
|
|
* Оба модуля на общей шине VSPI, разные CS:
|
|
* SCK=18 MISO=19 MOSI=23
|
|
* LoRa: NSS=5 RST=14 DIO0=26
|
|
* W5500: CS=4
|
|
*
|
|
* Ретранслирует LoRa-узлы в единое пространство топиков:
|
|
* пакет {"t":"info",...} → nodes/<id>/info (retained)
|
|
* пакет {"t":"d",...} → nodes/<id>/data (+rssi/snr/gw)
|
|
* nodes/<id>/cmd (MQTT) → LoRa вниз, адресно по id
|
|
*
|
|
* Онлайн/оффлайн LoRa-узлов решает сервер по last_seen
|
|
* (у радиоузлов нет LWT).
|
|
*
|
|
* Библиотеки: LoRa (Sandeep Mistry), Ethernet (Arduino),
|
|
* PubSubClient, ArduinoJson
|
|
* ================================================================
|
|
*/
|
|
|
|
#include <SPI.h>
|
|
#include <Ethernet.h>
|
|
#include <PubSubClient.h>
|
|
#include <LoRa.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
// ================== НАСТРОЙКИ ==================
|
|
#define GW_ID "gw1"
|
|
#define MQTT_HOST "192.168.1.200"
|
|
#define MQTT_PORT 1883
|
|
#define MQTT_USER ""
|
|
#define MQTT_PASSWD ""
|
|
|
|
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x10, 0x01}; // уникальный на шлюз
|
|
|
|
// LoRa — параметры должны совпадать с узлами
|
|
#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
|
|
#define W5500_CS 4
|
|
|
|
EthernetClient ethClient;
|
|
PubSubClient mqtt(ethClient);
|
|
|
|
// ================== СЕТЬ ==================
|
|
void netBegin() {
|
|
Ethernet.init(W5500_CS);
|
|
Serial.print("DHCP... ");
|
|
while (Ethernet.begin(mac) == 0) {
|
|
Serial.println("fail, retry 5s");
|
|
delay(5000);
|
|
}
|
|
Serial.println(Ethernet.localIP());
|
|
}
|
|
|
|
void mqttReconnect() {
|
|
while (!mqtt.connected()) {
|
|
if (mqtt.connect("lora-" GW_ID, MQTT_USER, MQTT_PASSWD,
|
|
"gateways/" GW_ID "/status", 1, true, "offline")) {
|
|
mqtt.publish("gateways/" GW_ID "/status", "online", true);
|
|
mqtt.subscribe("nodes/+/cmd");
|
|
Serial.println("MQTT OK");
|
|
} else {
|
|
Serial.printf("MQTT rc=%d, retry 5s\n", mqtt.state());
|
|
delay(5000);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================== MQTT → LoRa (вниз) ==================
|
|
void mqttCallback(char *topic, byte *payload, unsigned int len) {
|
|
String t(topic);
|
|
int a = t.indexOf('/') + 1;
|
|
int b = t.indexOf('/', a);
|
|
String nodeId = t.substring(a, b);
|
|
|
|
// LAN-узлы обрабатывают свои cmd сами; шлюз ретранслирует всё,
|
|
// радиоузлы фильтруют по своему id, чужое игнорируют.
|
|
StaticJsonDocument<192> doc;
|
|
if (deserializeJson(doc, payload, len)) return;
|
|
doc["id"] = nodeId;
|
|
|
|
char buf[192];
|
|
size_t n = serializeJson(doc, buf);
|
|
LoRa.beginPacket();
|
|
LoRa.write((const uint8_t*)buf, n);
|
|
LoRa.endPacket();
|
|
LoRa.receive();
|
|
Serial.printf("DOWN [%s]: %s\n", nodeId.c_str(), buf);
|
|
}
|
|
|
|
// ================== LoRa → MQTT (вверх) ==================
|
|
void handleLoRaPacket() {
|
|
String s;
|
|
while (LoRa.available()) s += (char)LoRa.read();
|
|
|
|
StaticJsonDocument<384> doc;
|
|
if (deserializeJson(doc, s)) { Serial.println("Bad pkt: " + s); return; }
|
|
const char *id = doc["id"];
|
|
if (!id) return;
|
|
|
|
const char *type = doc["t"] | "d";
|
|
bool isInfo = !strcmp(type, "info");
|
|
|
|
doc["gw"] = GW_ID;
|
|
if (!isInfo) {
|
|
doc["rssi"] = LoRa.packetRssi();
|
|
doc["snr"] = serialized(String(LoRa.packetSnr(), 1));
|
|
}
|
|
doc.remove("t");
|
|
|
|
char buf[384];
|
|
size_t n = serializeJson(doc, buf);
|
|
|
|
char topic[64];
|
|
snprintf(topic, sizeof(topic), "nodes/%s/%s", id, isInfo ? "info" : "data");
|
|
mqtt.publish(topic, (const uint8_t*)buf, n, isInfo); // info — retained
|
|
Serial.printf("UP %s: %s\n", topic, buf);
|
|
}
|
|
|
|
// ================== SETUP / LOOP ==================
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
SPI.begin(18, 19, 23);
|
|
|
|
netBegin();
|
|
mqtt.setServer(MQTT_HOST, MQTT_PORT);
|
|
mqtt.setCallback(mqttCallback);
|
|
mqtt.setBufferSize(512);
|
|
|
|
LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0);
|
|
if (!LoRa.begin(LORA_FREQ)) {
|
|
Serial.println("LoRa init FAIL");
|
|
while (true) delay(1000);
|
|
}
|
|
LoRa.setSpreadingFactor(LORA_SF);
|
|
LoRa.setSignalBandwidth(LORA_BW);
|
|
LoRa.setSyncWord(LORA_SYNC);
|
|
LoRa.setTxPower(LORA_TX_DBM);
|
|
LoRa.enableCrc();
|
|
LoRa.receive();
|
|
|
|
Serial.println("Gateway " GW_ID " ready (ETH)");
|
|
}
|
|
|
|
void loop() {
|
|
Ethernet.maintain(); // продление DHCP-аренды
|
|
if (!mqtt.connected()) mqttReconnect();
|
|
mqtt.loop();
|
|
|
|
if (LoRa.parsePacket() > 0) handleLoRaPacket();
|
|
}
|