Основные изменения: Добавлено хранилище для имени файла: cpp RTC_DATA_ATTR char fileName[32] = "data"; // Имя файла по умолчанию Изменена работа с файлами: Все файловые операции теперь используют шаблон: /[fileName].csv Автоматическое создание файла при первом запуске Поддержка уникальных имен для разных устройств Добавлен веб-интерфейс для управления именем: Новая форма в веб-интерфейсе: html
Обработчик /setname для сохранения нового имени Отображение текущего имени в веб-интерфейсе Скачивание данных: Файл скачивается с текущим именем устройства Автоматическое имя файла при скачивании: [fileName].csv Особенности реализации: Имя сохраняется в энергонезависимой памяти (RTC_DATA_ATTR) Ограничение длины имени: 31 символ Автоматическое создание нового файла при изменении имени Обратная совместимость (по умолчанию используется "data.csv") При скачивании файл сохраняется с текущим именем устройства Работа системы полностью сохраняет предыдущую функциональность, добавляя гибкую настройку имени устройства/файла через веб-интерфейс. #include #include #include #include #include #include #include // Конфигурация пинов #define ONE_WIRE_BUS 5 // Пин для датчиков температуры #define BUTTON_PIN 32 // Пин кнопки для пробуждения #define POWER_CTRL_PIN 33 // Пин управления питанием датчиков (опционально) // Настройки Wi-Fi const char* AP_SSID = "TempLogger_1"; const char* AP_PASSWORD = "securepass123"; // Интервалы сна (в микросекундах) const uint64_t SLEEP_INTERVALS[] = { 2 * 60 * 1000000ULL, // 2 минуты 15 * 60 * 1000000ULL, // 15 минут 60 * 60 * 1000000ULL, // 1 час 24 * 60 * 60 * 1000000ULL // 1 день }; RTC_DATA_ATTR uint8_t currentIntervalIdx = 1; // По умолчанию 15 минут // Глобальные объекты OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); WebServer server(80); // Переменные, сохраняемые между циклами сна RTC_DATA_ATTR time_t baseEpoch = 0; // Базовое время (Unix epoch) RTC_DATA_ATTR uint64_t totalSleepMicros = 0; // Общее время в сне RTC_DATA_ATTR uint32_t bootCount = 0; // Счетчик запусков RTC_DATA_ATTR bool firstBoot = true; // Флаг первого запуска RTC_DATA_ATTR char fileName[32] = "data"; // Имя файла для данных // Структура для хранения данных датчика struct SensorData { DeviceAddress address; float temperature; bool valid = false; }; void setup() { Serial.begin(115200); delay(1000); // Даем время для инициализации Serial pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(POWER_CTRL_PIN, OUTPUT); digitalWrite(POWER_CTRL_PIN, HIGH); // Включаем питание датчиков // Инициализация SPIFFS с проверкой if (!initFileSystem()) { Serial.println("Критическая ошибка: Не удалось инициализировать файловую систему!"); goToDeepSleep(SLEEP_INTERVALS[currentIntervalIdx]); return; } // Определяем причину пробуждения esp_sleep_wakeup_cause_t wakeCause = esp_sleep_get_wakeup_cause(); if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) { Serial.println("Пробуждение по таймеру"); totalSleepMicros += SLEEP_INTERVALS[currentIntervalIdx]; if (!collectAndSaveData()) { Serial.println("Ошибка сбора данных!"); } goToDeepSleep(SLEEP_INTERVALS[currentIntervalIdx]); } else if (wakeCause == ESP_SLEEP_WAKEUP_EXT0) { Serial.println("Пробуждение по кнопке"); startWebInterface(); } else { Serial.println("Первичный запуск системы"); bootCount++; firstBoot = true; startWebInterface(); } } void loop() { // Обработка веб-интерфейса, если активен if (WiFi.softAPgetStationNum() > 0) { server.handleClient(); // Если нет подключенных клиентов более 5 минут - спать static unsigned long lastClientTime = millis(); if (millis() - lastClientTime > 5 * 60 * 1000) { goToDeepSleep(SLEEP_INTERVALS[currentIntervalIdx]); } } } // ================== ФУНКЦИИ ДЛЯ РАБОТЫ С ФАЙЛАМИ ================== bool initFileSystem() { Serial.println("Инициализация SPIFFS..."); if (!SPIFFS.begin(true)) { Serial.println("Ошибка монтирования SPIFFS, пробуем форматировать..."); if (!SPIFFS.format()) { Serial.println("Ошибка форматирования SPIFFS!"); return false; } if (!SPIFFS.begin(true)) { Serial.println("Повторная ошибка монтирования SPIFFS!"); return false; } } // Создаем файл данных с заголовком, если не существует String fullFileName = String("/") + fileName + ".csv"; if (!SPIFFS.exists(fullFileName)) { Serial.println("Создание нового файла данных..."); File file = SPIFFS.open(fullFileName, FILE_WRITE); if (!file) { Serial.println("Ошибка создания файла данных!"); return false; } if (!file.println("timestamp,free_ram,sensor_count,sensor_address,temperature")) { Serial.println("Ошибка записи заголовка!"); file.close(); return false; } file.close(); } return true; } bool saveDataToFile(const String& data) { String fullFileName = String("/") + fileName + ".csv"; File file = SPIFFS.open(fullFileName, FILE_APPEND); if (!file) { Serial.println("Ошибка открытия файла для записи!"); return false; } if (!file.println(data)) { Serial.println("Ошибка записи данных!"); file.close(); return false; } file.close(); return true; } // ================== ФУНКЦИИ ДЛЯ РАБОТЫ С ДАТЧИКАМИ ================== bool initSensors() { Serial.println("Инициализация датчиков температуры..."); sensors.begin(); int deviceCount = sensors.getDeviceCount(); if (deviceCount == 0) { Serial.println("Датчики не обнаружены!"); return false; } Serial.print("Найдено датчиков: "); Serial.println(deviceCount); return true; } bool readSensors(SensorData* data, int maxSensors) { if (!initSensors()) return false; // Запрашиваем температуру со всех датчиков sensors.requestTemperatures(); DeviceAddress addr; int validCount = 0; for (int i = 0; i < maxSensors && i < sensors.getDeviceCount(); i++) { if (sensors.getAddress(addr, i)) { float tempC = sensors.getTempC(addr); if (tempC != DEVICE_DISCONNECTED_C) { memcpy(data[validCount].address, addr, 8); data[validCount].temperature = tempC; data[validCount].valid = true; validCount++; } } } return validCount > 0; } // ================== ОСНОВНАЯ ЛОГИКА ================== bool collectAndSaveData() { Serial.println("=== Начало сбора данных ==="); // Максимальное количество датчиков const int MAX_SENSORS = 20; SensorData sensorData[MAX_SENSORS]; // Читаем данные с датчиков if (!readSensors(sensorData, MAX_SENSORS)) { Serial.println("Нет данных для сохранения!"); return false; } // Формируем строку данных String dataString = getCurrentDateTime() + ","; dataString += String(esp_get_free_heap_size()) + ","; // Считаем валидные датчики int validCount = 0; for (int i = 0; i < MAX_SENSORS; i++) { if (sensorData[i].valid) validCount++; } dataString += String(validCount); // Добавляем данные каждого датчика for (int i = 0; i < MAX_SENSORS; i++) { if (sensorData[i].valid) { dataString += "," + addressToString(sensorData[i].address); dataString += "," + String(sensorData[i].temperature, 2); } } Serial.println("Данные для сохранения:"); Serial.println(dataString); // Сохраняем в файл if (!saveDataToFile(dataString)) { return false; } Serial.println("Данные успешно сохранены!"); return true; } // ================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ================== String addressToString(const DeviceAddress addr) { String str; for (uint8_t i = 0; i < 8; i++) { if (addr[i] < 16) str += "0"; str += String(addr[i], HEX); } return str; } String getCurrentDateTime() { time_t current = baseEpoch + (totalSleepMicros / 1000000); struct tm *timeinfo = gmtime(¤t); char buffer[30]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeinfo); return String(buffer); } time_t parseDateTime(String datetime) { if (datetime.length() < 16) return 0; int year = datetime.substring(0,4).toInt(); int month = datetime.substring(5,7).toInt(); int day = datetime.substring(8,10).toInt(); int hour = datetime.substring(11,13).toInt(); int minute = datetime.substring(14,16).toInt(); tmElements_t tm; tm.Year = year - 1970; tm.Month = month; tm.Day = day; tm.Hour = hour; tm.Minute = minute; tm.Second = 0; return makeTime(tm); } // ================== ВЕБ-ИНТЕРФЕЙС ================== void startWebInterface() { Serial.println("Запуск веб-интерфейса..."); WiFi.softAP(AP_SSID, AP_PASSWORD); IPAddress ip = WiFi.softAPIP(); Serial.print("AP запущен. IP: "); Serial.println(ip); // Настройка обработчиков веб-сервера server.on("/", HTTP_GET, handleRoot); server.on("/settime", HTTP_POST, handleSetTime); server.on("/setinterval", HTTP_POST, handleSetInterval); server.on("/setname", HTTP_POST, handleSetName); // Новый обработчик server.on("/download", HTTP_GET, handleDownload); server.on("/sleep", HTTP_GET, handleSleep); server.begin(); Serial.println("HTTP сервер запущен"); } void handleRoot() { String html = ""; html += ""; html += ""; html += "

Температурный даталоггер

"; html += "

Текущее время: " + getCurrentDateTime() + "

"; html += "

Свободная память: " + String(esp_get_free_heap_size()) + " байт

"; html += "

Имя файла данных: " + String(fileName) + ".csv

"; // Отображение текущего имени html += "

Скачать данные

"; // Форма для установки времени html += "
"; html += ""; html += "
"; // Форма для установки интервала html += "
"; html += "
"; // Новая форма для изменения имени файла html += "
"; html += ""; html += "
"; html += "

Перейти в спящий режим

"; html += ""; server.send(200, "text/html", html); } void handleSetTime() { if (server.hasArg("time")) { time_t newTime = parseDateTime(server.arg("time")); if (newTime > 0) { baseEpoch = newTime; totalSleepMicros = 0; server.send(200, "text/plain", "Время успешно установлено!"); } else { server.send(400, "text/plain", "Неверный формат времени!"); } } else { server.send(400, "text/plain", "Время не указано!"); } } void handleSetInterval() { if (server.hasArg("interval")) { uint8_t newInterval = server.arg("interval").toInt(); if (newInterval >= 0 && newInterval <= 3) { currentIntervalIdx = newInterval; server.send(200, "text/plain", "Интервал изменен!"); } else { server.send(400, "text/plain", "Неверный интервал!"); } } else { server.send(400, "text/plain", "Интервал не указан!"); } } // Новый обработчик для изменения имени void handleSetName() { if (server.hasArg("name")) { String newName = server.arg("name"); if (newName.length() > 0 && newName.length() < 32) { newName.toCharArray(fileName, 32); server.send(200, "text/plain", "Имя устройства обновлено!"); } else { server.send(400, "text/plain", "Имя должно содержать от 1 до 31 символа!"); } } else { server.send(400, "text/plain", "Имя не указано!"); } } void handleDownload() { String fullFileName = String("/") + fileName + ".csv"; if (!SPIFFS.exists(fullFileName)) { server.send(404, "text/plain", "Файл данных не найден!"); return; } File file = SPIFFS.open(fullFileName, "r"); if (!file) { server.send(500, "text/plain", "Ошибка открытия файла!"); return; } // Используем имя файла для скачивания String downloadName = fileName + ".csv"; server.sendHeader("Content-Type", "text/csv"); server.sendHeader("Content-Disposition", "attachment; filename=" + downloadName); server.streamFile(file, "text/csv"); file.close(); } void handleSleep() { server.send(200, "text/plain", "Переход в спящий режим..."); delay(1000); goToDeepSleep(SLEEP_INTERVALS[currentIntervalIdx]); } // ================== УПРАВЛЕНИЕ ПИТАНИЕМ ================== void goToDeepSleep(uint64_t interval) { Serial.println("Подготовка к глубокому сну..."); // Отключаем все перед сном server.stop(); WiFi.softAPdisconnect(true); digitalWrite(POWER_CTRL_PIN, LOW); // Отключаем питание датчиков // Настраиваем пробуждение esp_sleep_enable_ext0_wakeup(GPIO_NUM_32, LOW); esp_sleep_enable_timer_wakeup(interval); Serial.print("Переход в сон на "); Serial.print(interval / 1000000); Serial.println(" секунд"); delay(100); esp_deep_sleep_start(); }