Initial commit: FarmBox edge stack (ESP32 firmware + MQTT bridge + sync agent)

This commit is contained in:
admin
2026-04-30 10:54:42 +03:00
commit ab52b7a54a
10 changed files with 1397 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* FarmBox LoRa Gateway
* ESP32 или Raspberry Pi + LoRa HAT
* Принимает пакеты от всех датчиков → публикует в MQTT
*
* Для ESP32: подключить к WiFi → слать в Mosquitto
* Для RPi: запустить как Python скрипт
*/
#include <Arduino.h>
#include <SPI.h>
#include <LoRa.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
// ─── WiFi (локальная сеть фермы) ──────────────────────
const char* WIFI_SSID = "FARM_WIFI"; // ← изменить
const char* WIFI_PASS = "farm_password"; // ← изменить
// ─── MQTT (Mosquitto на Raspberry Pi / Mini PC) ────────
const char* MQTT_HOST = "192.168.1.100"; // ← IP FarmBox
const int MQTT_PORT = 1883;
// ─── LoRa пины (TTGO LoRa32) ──────────────────────────
#define LORA_SCK 5
#define LORA_MISO 19
#define LORA_MOSI 27
#define LORA_SS 18
#define LORA_RST 23
#define LORA_DI0 26
#define LORA_BAND 868E6
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
void setup() {
Serial.begin(115200);
Serial.println("FarmBox LoRa Gateway v1.0");
// WiFi
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.print("WiFi...");
while (WiFi.status() != WL_CONNECTED) {
delay(500); Serial.print(".");
}
Serial.printf("\nWiFi OK: %s\n", WiFi.localIP().toString().c_str());
// MQTT
mqtt.setServer(MQTT_HOST, MQTT_PORT);
connectMqtt();
// LoRa
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
LoRa.setPins(LORA_SS, LORA_RST, LORA_DI0);
if (!LoRa.begin(LORA_BAND)) {
Serial.println("LoRa ОШИБКА!");
while (true);
}
LoRa.setSpreadingFactor(9);
LoRa.setSignalBandwidth(125E3);
LoRa.enableCrc();
Serial.println("LoRa Gateway готов. Слушаю...");
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
int packetSize = LoRa.parsePacket();
if (packetSize) {
String received = "";
while (LoRa.available()) received += (char)LoRa.read();
int rssi = LoRa.packetRssi();
float snr = LoRa.packetSnr();
Serial.printf("[RX] RSSI:%d SNR:%.1f | %s\n", rssi, snr, received.c_str());
// Парсим JSON и добавляем метаданные шлюза
StaticJsonDocument<512> doc;
if (deserializeJson(doc, received) == DeserializationError::Ok) {
doc["gw_rssi"] = rssi;
doc["gw_snr"] = snr;
String enhanced;
serializeJson(doc, enhanced);
// Публикуем в MQTT топик farm/lora/{device_id}
const char* deviceId = doc["id"] | "unknown";
String topic = "farm/lora/" + String(deviceId);
mqtt.publish(topic.c_str(), enhanced.c_str());
Serial.printf("[MQTT] → %s\n", topic.c_str());
}
}
}
void connectMqtt() {
while (!mqtt.connected()) {
Serial.print("MQTT...");
if (mqtt.connect("farmbox-gateway")) {
Serial.println("OK");
// Публикуем статус шлюза
mqtt.publish("farm/status/gateway", "{\"status\":\"online\"}");
} else {
Serial.printf("FAIL rc=%d, retry 5s\n", mqtt.state());
delay(5000);
}
}
}
+521
View File
@@ -0,0 +1,521 @@
/**
* FarmBox Sensor Node v2.0
* Lilygo TTGO LoRa32 V2.1_1.6 — 868 МГц
*
* Железо на плате:
* - ESP32 (WiFi + Bluetooth)
* - SX1276 LoRa 868 МГц (SMA антенна)
* - OLED SSD1306 0.96" (128×64)
* - SD-карта (SPI)
*
* Внешние датчики (докупить):
* - SCT-013-030: токовый трансформатор (30А) — моточасы + ток
* - DS18B20: температура подшипника
*
* Установка библиотек в Arduino IDE (Tools → Manage Libraries):
* - "LoRa" by Sandeep Mistry
* - "Adafruit SSD1306" by Adafruit
* - "Adafruit GFX Library" by Adafruit
* - "OneWire" by Jim Studt
* - "DallasTemperature" by Miles Burton
* - "ArduinoJson" by Benoit Blanchon
*
* Board в Arduino IDE:
* Tools → Board → ESP32 Arduino → "TTGO LoRa32-OLED"
* (или ESP32 Dev Module если нет в списке)
*
* ─────────────────────────────────────────────
* НАСТРОЙ ЭТИ ПАРАМЕТРЫ ПОД КАЖДУЮ УСТАНОВКУ:
* ─────────────────────────────────────────────
*/
// === КОНФИГУРАЦИЯ УСТРОЙСТВА (МЕНЯТЬ ДЛЯ КАЖДОЙ УВВ) ===
const char* DEVICE_ID = "uvv_01"; // уникальный ID (uvv_01..uvv_13)
const char* DEVICE_NAME = "УВВ-75-2 №1"; // отображается на дисплее
const int EQUIPMENT_ID = 101; // ID оборудования в farm-cmms
const float CURRENT_RATED = 7.5; // номинальный ток двигателя (А)
// =======================================================
// ─── Пины Lilygo LoRa32 V2.1_1.6 ─────────────────────
// LoRa SX1276
#define LORA_SCK 5
#define LORA_MISO 19
#define LORA_MOSI 27
#define LORA_SS 18
#define LORA_RST 23
#define LORA_DIO0 26
// OLED SSD1306
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST 16 // на V2.1_1.6 есть аппаратный сброс OLED
// SD карта
#define SD_MOSI 15
#define SD_MISO 2
#define SD_SCK 14
#define SD_CS 13
// Встроенный светодиод
#define LED_PIN 25
// ─── Внешние датчики ──────────────────────────────────
#define SCT_PIN 34 // токовый трансформатор (только ADC1!)
#define DS18B20_PIN 17 // температура (можно любой GPIO)
// ─── LoRa 868 МГц ─────────────────────────────────────
#define LORA_BAND 868E6
#define LORA_SF 9 // Spreading Factor: 7=быстро/близко, 12=медленно/далеко
#define LORA_BW 125E3 // Bandwidth: 125 кГц стандарт
#define LORA_CR 5 // Coding Rate 4/5
// ─── Интервалы ────────────────────────────────────────
#define SEND_INTERVAL_MS 60000 // отправка по LoRa раз в 1 мин
#define DISPLAY_UPDATE_MS 1000 // обновление дисплея раз в 1 сек
#define TEMP_READ_MS 5000 // температура раз в 5 сек
#define SD_SAVE_MS 30000 // запись на SD раз в 30 сек
// ─── Библиотеки ───────────────────────────────────────
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <LoRa.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoJson.h>
#include <SD.h>
#include <FS.h>
// ─── Объекты ──────────────────────────────────────────
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
OneWire oneWire(DS18B20_PIN);
DallasTemperature tempSensor(&oneWire);
// ─── Состояние системы ────────────────────────────────
struct State {
float current = 0; // ток двигателя (А)
float temperature = 0; // температура подшипника (°C)
uint32_t motoHoursMs = 0; // накопленные миллисекунды работы
bool motorOn = false; // двигатель работает?
int packetsSent = 0; // отправлено пакетов
int lastRSSI = 0; // RSSI последнего пакета
bool loraOk = false; // LoRa инициализирован?
bool sdOk = false; // SD карта доступна?
bool tempOk = false; // DS18B20 найден?
uint8_t displayPage = 0; // текущая страница дисплея
} state;
// ─── Таймеры ──────────────────────────────────────────
uint32_t lastSendMs = 0;
uint32_t lastDisplayMs = 0;
uint32_t lastTempMs = 0;
uint32_t lastSdMs = 0;
uint32_t motorStartMs = 0; // когда двигатель запустился
// ─── Буфер для SD (накапливаем пока нет LoRa связи) ──
#define SD_BUFFER_MAX 120 // максимум строк в буфере
int sdBufferCount = 0;
// ══════════════════════════════════════════════════════
// SETUP
// ══════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("\n=== FarmBox Sensor Node v2.0 ===");
Serial.printf("Device: %s | %s\n", DEVICE_ID, DEVICE_NAME);
pinMode(LED_PIN, OUTPUT);
blink(2, 100);
initOLED();
showBootScreen("Инициализация...");
delay(500);
initLoRa();
initSD();
initTempSensor();
// Загружаем сохранённые моточасы с SD карты
loadMotoHours();
showBootScreen("Готов!");
delay(800);
Serial.println("=== Запуск основного цикла ===");
}
// ══════════════════════════════════════════════════════
// LOOP
// ══════════════════════════════════════════════════════
void loop() {
uint32_t now = millis();
// ── Читаем ток и считаем моточасы ─────────────────
state.current = readCurrent();
bool running = state.current > 0.8; // > 0.8А = двигатель работает
if (running) {
if (!state.motorOn) {
motorStartMs = now;
state.motorOn = true;
Serial.println("▶ Двигатель запустился");
}
state.motoHoursMs += now - motorStartMs;
motorStartMs = now;
} else {
if (state.motorOn) {
state.motorOn = false;
Serial.printf("⏹ Двигатель остановился. Итого: %lu ч\n", getMotoHours());
saveMotoHours(); // сохраняем на SD при каждой остановке
}
}
// ── Температура ────────────────────────────────────
if (now - lastTempMs >= TEMP_READ_MS) {
tempSensor.requestTemperatures();
float t = tempSensor.getTempCByIndex(0);
if (t != DEVICE_DISCONNECTED_C && t > -50) {
state.temperature = t;
state.tempOk = true;
}
lastTempMs = now;
}
// ── Отправка по LoRa ───────────────────────────────
if (now - lastSendMs >= SEND_INTERVAL_MS) {
sendLoRaPacket();
lastSendMs = now;
}
// ── Запись на SD ───────────────────────────────────
if (now - lastSdMs >= SD_SAVE_MS) {
saveToSD();
lastSdMs = now;
}
// ── Обновление дисплея ────────────────────────────
if (now - lastDisplayMs >= DISPLAY_UPDATE_MS) {
updateDisplay();
lastDisplayMs = now;
}
delay(50);
}
// ══════════════════════════════════════════════════════
// LORA
// ══════════════════════════════════════════════════════
void sendLoRaPacket() {
if (!state.loraOk) {
Serial.println("[LoRa] Не инициализирован, пропускаем отправку");
return;
}
StaticJsonDocument<256> doc;
doc["id"] = DEVICE_ID;
doc["eq"] = EQUIPMENT_ID;
doc["mh"] = getMotoHours();
doc["temp"] = roundf(state.temperature * 10) / 10.0;
doc["curr"] = roundf(state.current * 10) / 10.0;
doc["run"] = state.motorOn ? 1 : 0;
doc["pkt"] = state.packetsSent;
String json;
serializeJson(doc, json);
LoRa.beginPacket();
LoRa.print(json);
int result = LoRa.endPacket();
if (result) {
state.packetsSent++;
state.lastRSSI = LoRa.packetRssi();
Serial.printf("[LoRa TX] ✅ #%d %s\n", state.packetsSent, json.c_str());
blink(1, 50);
} else {
Serial.println("[LoRa TX] ❌ Ошибка отправки");
}
}
// ══════════════════════════════════════════════════════
// OLED ДИСПЛЕЙ
// ══════════════════════════════════════════════════════
void updateDisplay() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// ── Заголовок ─────────────────────────────────────
display.setTextSize(1);
display.setCursor(0, 0);
display.println(DEVICE_NAME);
// Разделитель
display.drawFastHLine(0, 10, 128, SSD1306_WHITE);
// ── Основные данные ───────────────────────────────
display.setTextSize(1);
// Ток
display.setCursor(0, 14);
display.print("Ток: ");
display.setTextSize(2);
display.setCursor(30, 12);
char buf[16];
sprintf(buf, "%.1fA", state.current);
display.print(buf);
display.setTextSize(1);
if (state.motorOn) {
display.setCursor(105, 14);
display.print("ON");
}
// Температура
display.setCursor(0, 32);
display.print("Темп: ");
if (state.tempOk) {
sprintf(buf, "%.1f C", state.temperature);
display.print(buf);
// Предупреждение о перегреве
if (state.temperature > 80) {
display.setCursor(100, 32);
display.print("!");
}
} else {
display.print("-- C");
}
// Моточасы
display.setCursor(0, 44);
display.print("Мч: ");
sprintf(buf, "%lu ч", getMotoHours());
display.print(buf);
// ── Строка статуса внизу ──────────────────────────
display.drawFastHLine(0, 54, 128, SSD1306_WHITE);
display.setCursor(0, 56);
// LoRa статус
if (state.loraOk) {
sprintf(buf, "LoRa #%d", state.packetsSent);
display.print(buf);
} else {
display.print("LoRa ERR");
}
// SD статус
display.setCursor(80, 56);
display.print(state.sdOk ? "SD OK" : "NO SD");
display.display();
}
void showBootScreen(const char* msg) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("FarmBox v2.0");
display.drawFastHLine(0, 10, 128, SSD1306_WHITE);
display.setCursor(0, 16);
display.println(DEVICE_ID);
display.println(DEVICE_NAME);
display.setCursor(0, 48);
display.println(msg);
display.display();
}
// ══════════════════════════════════════════════════════
// ДАТЧИК ТОКА SCT-013
// ══════════════════════════════════════════════════════
float readCurrent() {
// SCT-013-030: 30А / 1В, нагрузочный резистор 68 Ом
// ESP32 ADC: 12 бит, 3.3В, смещение на 1.65В через делитель
const int SAMPLES = 1000;
const float ADC_VREF = 3.3;
const float SCT_RATIO = 30.0; // 30А/В для SCT-013-030
const float BURDEN_OHMS = 68.0; // нагрузочный резистор
float sum = 0;
for (int i = 0; i < SAMPLES; i++) {
int raw = analogRead(SCT_PIN);
float v = (raw / 4095.0) * ADC_VREF;
float centered = v - (ADC_VREF / 2.0);
sum += centered * centered;
delayMicroseconds(150);
}
float rmsV = sqrt(sum / SAMPLES);
float current = (rmsV / BURDEN_OHMS) * SCT_RATIO * 1000.0;
// Убираем шум
if (current < 0.5) current = 0;
return current;
}
// ══════════════════════════════════════════════════════
// МОТОЧАСЫ
// ══════════════════════════════════════════════════════
uint32_t getMotoHours() {
return state.motoHoursMs / 3600000UL;
}
uint32_t getMotoHoursRaw() {
return state.motoHoursMs;
}
void saveMotoHours() {
if (!state.sdOk) return;
File f = SD.open("/motohours.txt", FILE_WRITE);
if (f) {
f.println(state.motoHoursMs);
f.close();
Serial.printf("[SD] Моточасы сохранены: %lu мс (%lu ч)\n",
state.motoHoursMs, getMotoHours());
}
}
void loadMotoHours() {
if (!state.sdOk) return;
if (!SD.exists("/motohours.txt")) return;
File f = SD.open("/motohours.txt");
if (f) {
String line = f.readStringUntil('\n');
state.motoHoursMs = line.toInt();
f.close();
Serial.printf("[SD] Загружены моточасы: %lu ч\n", getMotoHours());
}
}
// ══════════════════════════════════════════════════════
// SD КАРТА — буфер данных
// ══════════════════════════════════════════════════════
void saveToSD() {
if (!state.sdOk) return;
// Имя файла по device_id
char filename[32];
sprintf(filename, "/%s.csv", DEVICE_ID);
bool exists = SD.exists(filename);
File f = SD.open(filename, FILE_APPEND);
if (!f) return;
// Заголовок если файл новый
if (!exists) {
f.println("timestamp_ms,motohours,temperature,current,motor_on");
}
// Строка данных
char line[128];
sprintf(line, "%lu,%lu,%.1f,%.1f,%d",
millis(),
getMotoHours(),
state.temperature,
state.current,
state.motorOn ? 1 : 0
);
f.println(line);
f.close();
sdBufferCount++;
// Каждые 100 записей — сохраняем моточасы
if (sdBufferCount % 100 == 0) saveMotoHours();
Serial.printf("[SD] Записано: %s | строк: %d\n", line, sdBufferCount);
}
// ══════════════════════════════════════════════════════
// ИНИЦИАЛИЗАЦИЯ
// ══════════════════════════════════════════════════════
void initOLED() {
// Сброс OLED
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW);
delay(20);
digitalWrite(OLED_RST, HIGH);
Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("[OLED] ❌ Не найден! Проверь подключение SDA/SCL");
// Продолжаем без дисплея
return;
}
display.clearDisplay();
display.display();
Serial.println("[OLED] ✅ OK");
}
void initLoRa() {
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
showBootScreen("LoRa 868 МГц...");
if (!LoRa.begin(LORA_BAND)) {
Serial.println("[LoRa] ❌ Ошибка инициализации!");
showBootScreen("LoRa ERROR!");
state.loraOk = false;
// Не вешаемся — работаем на SD
return;
}
LoRa.setSpreadingFactor(LORA_SF);
LoRa.setSignalBandwidth(LORA_BW);
LoRa.setCodingRate4(LORA_CR);
LoRa.enableCrc();
// Мощность передатчика: 2-20 dBm
// Внутри помещения хватит 14, на улицу можно 20
LoRa.setTxPower(14);
state.loraOk = true;
Serial.printf("[LoRa] ✅ OK — 868 МГц, SF%d, BW%.0fkHz\n",
LORA_SF, LORA_BW / 1000);
}
void initSD() {
SPIClass sdSPI(HSPI);
sdSPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
showBootScreen("SD карта...");
if (!SD.begin(SD_CS, sdSPI)) {
Serial.println("[SD] ⚠️ Карта не найдена — работаем без SD");
state.sdOk = false;
return;
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("[SD] ✅ OK — %llu МБ\n", cardSize);
state.sdOk = true;
}
void initTempSensor() {
showBootScreen("DS18B20...");
tempSensor.begin();
int count = tempSensor.getDeviceCount();
if (count > 0) {
state.tempOk = true;
Serial.printf("[DS18B20] ✅ Найдено датчиков: %d\n", count);
} else {
Serial.println("[DS18B20] ⚠️ Датчик не найден");
state.tempOk = false;
}
}
// ── Мигание LED ───────────────────────────────────────
void blink(int times, int ms) {
for (int i = 0; i < times; i++) {
digitalWrite(LED_PIN, HIGH);
delay(ms);
digitalWrite(LED_PIN, LOW);
delay(ms);
}
}