/** * 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 #include #include #include #include #include #include #include #include #include #include // ─── Объекты ────────────────────────────────────────── 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); } }