Files
farmbox/firmware/sensor_node/sensor_node.ino
T

522 lines
19 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}