В прошлых статьях рассмотрел передачу данных в облако. В принципе, нет никакого смысла передавать дату и время в облако, поскольку при передаче данных ThinSpeak присваивает время при получении, хотя API позволяет передавать и эти данные.
Допустим есть необходимость записывать во флэш память лог работы счетчика воды. Самый правильный и дорогой вариант — воспользоваться модулем RTC. Описание какой модуль лучше использовать в качестве RTC и datalogger в этой статье.
Если не хочется тратить на модуль RTC пол-бакса, можно воспользоваться NTP сервером для получения точного времени для корректировки внутреннего счетчика времени ESP8266/ESP32.
Избавляемся от RTC
Чтобы быстрее написать нужный код выбрал библиотеку NTPClientLib, поддерживающую микроконтроллеры ESP8266 и ESP32. В примере используется WiFiManager, чтобы убрать жесткое задание логина и пароля для WiFi. Код этого WiFi.ino файла здесь.
Вынесу работу с NTP в отдельный NTP.ino файл (Ctrl-Shft-N).
#include <NtpClientLib.h> #include <ESP8266WiFi.h> #define ONBOARDLED 2 // Built in LED on ESP-12/ESP-07 int8_t timeZone = 1; int8_t minutesTimeZone = 0; bool wifiFirstConnected = false; void onSTAConnected (WiFiEventStationModeConnected ipInfo) { Serial.printf ("Connected to %s\r\n", ipInfo.ssid.c_str ()); } // Start NTP only after IP network is connected void onSTAGotIP (WiFiEventStationModeGotIP ipInfo) { Serial.printf ("Got IP: %s\r\n", ipInfo.ip.toString ().c_str ()); Serial.printf ("Connected: %s\r\n", WiFi.status () == WL_CONNECTED ? "yes" : "no"); digitalWrite (ONBOARDLED, LOW); // Turn on LED wifiFirstConnected = true; } // Manage network disconnection void onSTADisconnected (WiFiEventStationModeDisconnected event_info) { Serial.printf ("Disconnected from SSID: %s\n", event_info.ssid.c_str ()); Serial.printf ("Reason: %d\n", event_info.reason); digitalWrite (ONBOARDLED, HIGH); // Turn off LED //NTP.stop(); // NTP sync can be disabled to avoid sync errors } void processSyncEvent (NTPSyncEvent_t ntpEvent) { if (ntpEvent) { Serial.print ("Time Sync error: "); if (ntpEvent == noResponse) Serial.println ("NTP server not reachable"); else if (ntpEvent == invalidAddress) Serial.println ("Invalid NTP server address"); } else { Serial.print ("Got NTP time: "); Serial.println (NTP.getTimeDateString (NTP.getLastNTPSync ())); } } boolean syncEventTriggered = false; // True if a time even has been triggered NTPSyncEvent_t ntpEvent; // Last triggered event void setupNTP() { static WiFiEventHandler e1, e2, e3; pinMode (ONBOARDLED, OUTPUT); // Onboard LED digitalWrite (ONBOARDLED, HIGH); // Switch off LED NTP.onNTPSyncEvent ([](NTPSyncEvent_t event) { ntpEvent = event; syncEventTriggered = true; }); e1 = WiFi.onStationModeGotIP (onSTAGotIP);// As soon WiFi is connected, start NTP Client e2 = WiFi.onStationModeDisconnected (onSTADisconnected); e3 = WiFi.onStationModeConnected (onSTAConnected); } void loopNTP() { if (wifiFirstConnected) { wifiFirstConnected = false; NTP.begin ("pool.ntp.org", timeZone, true, minutesTimeZone); NTP.setInterval (300); if (NTP.setTimeZone(3)) //Set timeZone to +3 { Serial.println("Timezone is set to: " + String(NTP.getTimeZone())); } else { Serial.println("Error setting timezone."); } } if (syncEventTriggered) { processSyncEvent (ntpEvent); syncEventTriggered = false; } }
Основной файл выглядит весьма компактно:
#include <NtpClientLib.h> #include <ESP8266WiFi.h> void setup () { Serial.begin (9600); Serial.println (); setupNTP(); setupWiFi(); } void loop () { static int i = 0; static int last = 0; loopNTP(); if ((millis () - last) > 5100) { //Serial.println(millis() - last); last = millis (); Serial.print (i); Serial.print (" "); Serial.print (NTP.getTimeDateString ()); Serial.print (" "); Serial.print (NTP.isSummerTime () ? "Summer Time. " : "Winter Time. "); Serial.print ("WiFi is "); Serial.print (WiFi.isConnected () ? "connected" : "not connected"); Serial.print (". "); //-------------------- time_t moment = now(); char timeStr[10]; sprintf (timeStr, "%02d:%02d:%02d", hour (moment), minute (moment), second (moment)); Serial.println("Time: " + String(timeStr)); //-------------------- //Serial.print ("Uptime: "); //Serial.print (NTP.getUptimeString ()); Serial.print (" since "); //Serial.println (NTP.getTimeDateString (NTP.getFirstSync ()).c_str ()); i++; } //delay (0); }
Для примера я привел кусочек кода для конвертации результата функции now() в строковое значение времени.
Сохранение логов. Конфигурации. SPDIFF.
В случае с ESP можно скономить и на плате data logger.
Есть два способа сохранять данные на ESP8266. Первый — используя внутренний EEPROM размером всего 512 Bytes, но количество циклов записи 1 миллион раз (файловой системы нет). Это как использовать полноценный внешний I2C EEPROM. Для сохранения показаний счетчика в отсутствие Интернета — более чем достаточно. Самый дешевый вариант EEPROM за 0,5 $ с доставкой в Россию.
Второй — SPI Flash (от 64kBytes до 3Mbyte), который позволяет выполнять примерно 10000 (десять тысяч) циклов записи. Соответственно, эта память пригодна для хранения конфигурационных файлов, но писать логи сюда нужно крайне осторожно, чтобы не «убить» микроконтроллер. О сроках жизни можно посмотреть здесь.
Когда данные пишутся в «облако», при надежном Интернет соединении можно кэшировать данные в SPI Flash на время отсуствия соединения. Но если интернет долго не будет, можно «спалить» микроконтроллер.
При компиляции важно выбрать поддержку SPIFF:
Я вынес тестовый код работы с файловой системой в отдельный SPIFFS.ino.
#include <ESP8266WiFi.h> #include <FS.h> //http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html void setupFS() { //Initialize File System if(SPIFFS.begin()) { Serial.println("SPIFFS Initialize....ok"); } else { Serial.println("SPIFFS Initialization...failed"); } //Format File System /*if(SPIFFS.format()) { Serial.println("File System Formated"); } else { Serial.println("File System Formatting Error"); }*/ } void info() { FSInfo fs_info; SPIFFS.info(fs_info); Serial.println("---------------------------------------------"); Serial.println("totalBytes: " + String(fs_info.totalBytes)); Serial.println("usedBytes: " + String(fs_info.usedBytes)); Serial.println("blockSize: " + String(fs_info.blockSize)); Serial.println("pageSize: " + String(fs_info.pageSize)); Serial.println("maxOpenFiles: " + String(fs_info.maxOpenFiles)); Serial.println("maxPathLength: " + String(fs_info.maxPathLength)); Serial.println("---------------------------------------------"); } void writeFile(char* filename) { File file; if (SPIFFS.exists(filename)) { //w=Write Open file for writing file = SPIFFS.open(filename, "a"); Serial.println("[" + String(file.name()) + "] exists."); Serial.println("File size is " + String(file.size()) + "."); //f.seek(0, SeekEnd); //Serial.println("File position: " + String(file.position())); } else { //Create New File And Write Data to It //w=Write Open file for writing file = SPIFFS.open(filename, "w"); } if (!file) { Serial.println("File [" + String(file.name()) + "] open failed"); } else { //Write data to file Serial.println("Writing Data to file [" + String(file.name()) + "]."); file.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); file.close(); //Close file } } void readFile(char* filename) { int i; //Read File data File file = SPIFFS.open(filename, "r"); if (!file) { Serial.println("file open failed"); } else { Serial.println("Reading Data from File:"); //Data from file for(i=0;i<file.size();i++) //Read upto complete file size { Serial.print((char)file.read()); } file.close(); //Close file Serial.println(); Serial.println("File Closed."); } }
Открыть текстовый файл можно в разных режимах.
mode | Description |
---|---|
r | Open a text file for reading. (The file must exist.) |
w | opens or create a text file in writing mode. If the file already exists, its contents are destroyed. |
a | Open a text file in append mode for writing at the end of the file. Creates the file if it does not exist. |
r+ | Open a text file for both reading and writing. (The file must exist.) |
w+ | Opens a text file in both reading and writing mode. If the file already exists, its contents are destroyed. |
a+ | Open a text file in append mode for reading or updating at the end of the file. Creates the file if it does not exist. |
Основной код короткий.
char* filename = "/rs485.json"; void setup() { Serial.begin(9600); // put your setup code here, to run once: uint32_t realSize = ESP.getFlashChipRealSize(); uint32_t ideSize = ESP.getFlashChipSize(); FlashMode_t ideMode = ESP.getFlashChipMode(); Serial.printf("Flash real id: %08X\n", ESP.getFlashChipId()); Serial.printf("Flash real size: %u bytes\n\n", realSize); Serial.printf("Flash ide size: %u bytes\n", ideSize); Serial.printf("Flash ide speed: %u Hz\n", ESP.getFlashChipSpeed()); Serial.printf("Flash ide mode: %s\n", (ideMode == FM_QIO ? "QIO" : ideMode == FM_QOUT ? "QOUT" : ideMode == FM_DIO ? "DIO" : ideMode == FM_DOUT ? "DOUT" : "UNKNOWN")); if (ideSize != realSize) { Serial.println("Flash Chip configuration wrong!\n"); } else { Serial.println("Flash Chip configuration ok.\n"); } setupFS(); info(); writeFile(filename); readFile(filename); //delay(5000); } void loop() { // put your main code here, to run repeatedly: }
Полезные ссылки
Полезные примеры работы с SPIFFS & EEPROM:
- http://esp8266.github.io/Arduino/versions/2.0.0/doc/filesystem.html
- https://github.com/espressif/arduino-esp32/tree/master/libraries/SPIFFS
- https://techtutorialsx.com/2018/08/05/esp32-arduino-spiffs-writing-a-file/
- https://github.com/Marzogh/SPIMemory
- https://circuits4you.com/2018/01/31/example-of-esp8266-flash-file-system-spiffs/
- https://github.com/esp8266/Arduino/tree/master/libraries/EEPROM
- https://github.com/Chris—A/EEPROM
- https://circuits4you.com/2016/12/16/esp8266-internal-eeprom-arduino/
- https://www.arduino.cc/en/Tutorial/EEPROMGet
- http://tinkerman.cat/eeprom-rotation-for-esp8266-and-esp32/
- https://github.com/pellepl/spiffs/wiki/FAQ#how-long-will-my-spi-flash-live
- https://github.com/me-no-dev/arduino-esp32fs-plugin — плагин для загрузки файлов в SPIFFS