# HG changeset patch # User H William Welliver # Date 1713215016 14400 # Mon Apr 15 17:03:36 2024 -0400 # Node ID cdd9960441e2e7ee98bd7f5b9fd678f4ff079140 # Parent a8a3aebfe421983677be4f2c60c29586879a3d80 initial commit of ethernet/poe wx station diff --git a/firmware/esp32-ethernet/.hgignore b/firmware/esp32-ethernet/.hgignore new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/.hgignore @@ -0,0 +1,1 @@ +.pio diff --git a/firmware/esp32-ethernet/README.md b/firmware/esp32-ethernet/README.md new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/README.md @@ -0,0 +1,2 @@ +This firmware is configured for use with the Olimex ESP32-POE-ISO + diff --git a/firmware/esp32-ethernet/include/README b/firmware/esp32-ethernet/include/README new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/firmware/esp32-ethernet/include/config.h b/firmware/esp32-ethernet/include/config.h new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/include/config.h @@ -0,0 +1,20 @@ +#define WANT_NTP 1 +#define WANT_OTA 1 +#define WANT_MDNS 1 + +#define FIRMWARE_REVISION 30337 + +/* by default, the value of PROJECTNAME will be used as the prefix of the devices's + * hostname and the configuration access point ssid. + */ +#define PROJECTNAME "WXSENSOR" + +#define OTA_PORT 2040 +#define OTA_PASSWORD "2468" + +#define FAILED_SEND_LIMIT 3 + +#define SENDCONTENT_P_BUFFER_SZ 8096 + +/* once weekly */ +#define RESET_INTERVAL (1000 * 3600 * 24 * 7) \ No newline at end of file diff --git a/firmware/esp32-ethernet/lib/README b/firmware/esp32-ethernet/lib/README new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/firmware/esp32-ethernet/platformio.ini b/firmware/esp32-ethernet/platformio.ini new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/platformio.ini @@ -0,0 +1,43 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32-poe-iso] +platform = espressif32 +board = esp32-poe-iso +framework = arduino +upload_protocol = espota +upload_port = wxsensor-84d33f.local +upload_flags = --auth=2468 +lib_ignore = DueFlashStorage + WiFiEspAT + WiFiNINA + WiFiNINA_Generic + WiFi101_Generic + ESP8266_AT_WebServer + khoih-prog/ESP_AT_Lib + ESP_AT_Lib + WiFi101 + ArduinoSTL + "Adafruit GFX Library" + "Adafruit SH110X" +lib_deps = + SPI + bblanchon/ArduinoJson@^6.18.4 + arduino-libraries/NTPClient@^3.2.1 + bblanchon/StreamUtils@^1.6.3 + ESPWebServer + https://github.com/enjoyneering/HTU2xD_SHT2x_Si70xx + https://github.com/finitespace/BME280 + khoih-prog/TimerInterrupt_Generic@^1.11.0 + mathertel/OneButton@^2.0.2 + Wire + arduino-libraries/Arduino_DebugUtils + adafruit/Adafruit PM25 AQI Sensor@^1.1.0 + adafruit/Adafruit BusIO@^1.15.0 diff --git a/firmware/esp32-ethernet/src/main.cpp b/firmware/esp32-ethernet/src/main.cpp new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/src/main.cpp @@ -0,0 +1,1085 @@ +#include "config.h" +#include +#include +#include + +#include + +#ifdef WANT_OTA +#include +#endif /* WANT_OTA */ + +#ifdef WANT_NTP +#include +#endif /* WANT_NTP */ + +#ifdef WANT_MDNS +#include +#endif /* WANT_MDNS */ + +#include +#include + +#include +#include +#include + +#include +//5 seconds WDT +#define WDT_TIMEOUT 5 +long watchdog_last_patted = 0; + +bool got_address = false; + +void watchdog_update() { + if (millis() - watchdog_last_patted >= 1000) { + // Serial.println("Resetting WDT..."); + // esp_task_wdt_reset(); + watchdog_last_patted = millis(); + } +} + +#define LOGERROR(X...) Serial.printf(X); + +String ssid = PROJECTNAME "-"; + +String airsn = "ST-51516"; +String hubsn = "HB-51516"; +String aqsn = "AQ-61521"; + +int report_frequency = 60; +String hostname; + +int failedSends = 0; + +bool statusNotDisplayed = true; + +HTU2xD_SHT2x_SI70xx ht2x(HTU2xD_SENSOR, HUMD_08BIT_TEMP_12BIT); + +BME280I2C::Settings settings( + BME280::OSR_X1, + BME280::OSR_X1, + BME280::OSR_X1, + BME280::Mode_Forced, + BME280::StandbyTime_1000ms, + BME280::Filter_Off, + BME280::SpiEnable_False, + BME280I2C::I2CAddr_0x76 // I2C address. I2C specific. +); + +BME280I2C bme280(settings); +Adafruit_PM25AQI aqi = Adafruit_PM25AQI(); + +ESP32Timer ITimer0(1); +bool wantRapidWind = false; + +#define DEBUG(X...) do{if(Serial)Serial.println(X);}while(0) +#define DEBUGCHAR(X...) do{if(Serial)Serial.print(X);}while(0) + +double windValues[60]; +int currentWindValue = 0; + +/* when storing data in the pseudo-eeprom of the ESP, you must specify how much space + * will be needed for the data stored. changing this value will likely cause existing + * data to be lost, so best over-estimate before going live. + */ +#define EEPROM_BYTES_NEEDED 50 + +boolean sensorPresent; +boolean bmeSensorPresent; + +#define SLOW 250 +#define MEDIUM 100 +#define FAST 50 + +#define sleep(X) delay(X) +#define ON 1 +#define OFF 0 +// +//WiFiClient wifiClient; +void weather_report(); + + + +/* + * GPIO4 -> GP2: PULSE (WH) + * GPIO15 -> GP3: RAIN (BK) + * SDA -> GP6: SDA (BL) + * SCK -> GP7: SCL (GY) + * 3V3 -> 3v3 (RD) + * GND -> GND (GR) + * GPIO14 -> GP26 WIND (OR) + */ + +#define SDA_PIN SDA +#define SCL_PIN SCL + +#define RAIN_PIN 15 +//#define SPEED_PIN 2 +#define SPEED_PIN 4 +//#define DIR_PIN 26 +//#define EXCITER_PIN 4 +#define DIR_PIN 14 +#define EXCITER_PIN 14 + +#define RAIN_DEBOUNCE_MS 350 +#define WIND_DEBOUNCE_MS 15 + +#define SMARTWEATHER_PORT 50222 + +volatile unsigned long w_last_interrupt_time = 0; + +volatile unsigned long wind_time = 0; +volatile unsigned long wind_interval = 0; +volatile unsigned long max_wind_interval = 0; +volatile int last_awoke = 0; + +volatile int did_wdt_check = 0; + +double calcMaxWindspeed(); +double calcWindSpeed(); + +uint16_t calcWindDir(); + +#define TIMER0_INTERVAL_MS 1000000 + +bool updateWindValues(void * y); + +IPAddress br_adr; +String macAddress; + +WiFiUDP Udp; + +#ifdef WANT_NTP +WiFiUDP ntpUdp; + +// for some reason NTPClient gets into a DNS query loop on update(), +// causing the call to hang for 5-20 seconds. +IPAddress ntpserver(192, 168, 1, 2); +NTPClient timeClient(ntpUdp, ntpserver); +#endif /* WANT_NTP */ + +void rainInterrupt(); +void windInterrupt(); +void reportRain(int increments); +void rapidWind(); + +void hubStatus(long tsm); +void deviceStatus(long tsm); +void weatherObservation(long tsm); + +long ts; +int reportcnt = 0; +int lastobservation = 0; +int lastreport = 0; + +#define LED_ON HIGH +#define LED_OFF LOW + +#define MIN_AP_PASSWORD_SIZE 8 + +#define SSID_MAX_LEN 32 +//From v1.0.10, WPA2 passwords can be up to 63 characters long. +#define PASS_MAX_LEN 64 + +typedef struct +{ + char wifi_ssid[SSID_MAX_LEN]; + char wifi_pw [PASS_MAX_LEN]; +} WiFi_Credentials; + +typedef struct +{ + char hub_sn[10]; + char air_sn[10]; + int report_frequency; + char hostname[32]; +} App_Parameters; + +#define NUM_WIFI_CREDENTIALS 1 + +typedef struct +{ + uint8_t cookie[2]; + App_Parameters Params; +} WM_Config; + +WM_Config WM_config; + +////// + +#include + +static bool eth_connected = false; +void WiFiEvent(WiFiEvent_t event) +{ + switch (event) { + case ARDUINO_EVENT_ETH_START: + Serial.println("ETH Started"); + //set eth hostname here + ETH.setHostname(hostname.c_str()); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println("ETH Connected"); + ETH.config(IPAddress(0,0,0,0), + IPAddress(0,0,0,0), + IPAddress(0,0,0,0), + IPAddress(0,0,0,0)); + if(got_address) eth_connected = true; + break; + case ARDUINO_EVENT_ETH_GOT_IP: + Serial.print("ETH MAC: "); + Serial.print(ETH.macAddress()); + Serial.print(", IPv4: "); + Serial.print(ETH.localIP()); + if (ETH.fullDuplex()) { + Serial.print(", FULL_DUPLEX"); + } + Serial.print(", "); + Serial.print(ETH.linkSpeed()); + Serial.println("Mbps"); + macAddress = ETH.macAddress(); + + Serial.print("MAC ADDRESS "); + Serial.println(macAddress); + ssid += macAddress.substring(9, 11); + ssid += macAddress.substring(12, 14); + ssid += macAddress.substring(15, 17); + Serial.println(ssid); + + if(hostname == NULL) { + hostname = ssid; + } + + got_address = true; + eth_connected = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println("ETH Disconnected"); + eth_connected = false; + break; + case ARDUINO_EVENT_ETH_STOP: + Serial.println("ETH Stopped"); + eth_connected = false; + break; + default: + Serial.println("ETH Unknown event"); + break; + } +} + +void blink(int speed, int count) { + pinMode(16, OUTPUT); + for(; count > 0; count--) { + digitalWrite(16, ON); + sleep(speed); + digitalWrite(16, OFF); + sleep(speed); + } + digitalWrite(16, ON); +} + +void resetFunc() { + ESP.restart(); +} + +//bool loadConfigData() +//{ +// memset((void*) &WM_config, 0, sizeof(WM_config)); +// +// // New in v1.4.0 +// //memset((void*) &WM_STA_IPconfig, 0, sizeof(WM_STA_IPconfig)); +// ////// +// +// { +// EEPROM.get(0, WM_config); +// Serial.println(F("OK")); +// +// if(WM_config.cookie[0] != 'W' || WM_config.cookie[1] != 'x') { +// Serial.println(F("Invalid config in EEPROM")); +// return false; +// } +// +// +// airsn = String(WM_config.Params.air_sn); +// hubsn = String(WM_config.Params.hub_sn); +// report_frequency = (WM_config.Params.report_frequency); +// hostname = String(WM_config.Params.hostname);; +// +// Serial.println(String(F("Air Serial = ")) + airsn); +// Serial.println(String(F("Hub Serial = ")) + hubsn); +// Serial.println(String(F("Report Frequency = ")) + String(report_frequency)); +// Serial.println(String(F("Hostname = ")) + hostname); +// +// return true; +// } +//} + +void setup() +{ + Serial.begin(115200); + while (!Serial && millis() < 5000); + + while (!Serial && millis() < 5000); + + delay(200); + +// Wire1.setSDA(6); +// Wire1.setSCL(7); + Wire.begin(); + // RP2040 does not have an internal reference, A_REF should be wired + // to the approproate refrence, on pico boards, this is set to the 3.3v regulated + // line, which is not super accurate. + // analogReference(AR_EXTERNAL); + + pinMode(RAIN_PIN, INPUT_PULLUP); + pinMode(SPEED_PIN, INPUT_PULLUP); + pinMode(DIR_PIN, INPUT); + + Serial.print("Firmware revision "); + Serial.println(FIRMWARE_REVISION); + + //Preferences.begin(PROJECT_NAME); + + WiFi.onEvent(WiFiEvent); + ETH.begin(); + +#if WANT_OTA + +ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "sketch"; + } else { // U_FS + type = "filesystem"; + } + + // NOTE: if updating FS this would be the place to unmount FS using FS.end() + Serial.println("Start updating " + type); + }); + + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + watchdog_update(); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + Serial.println("Auth Failed"); + } else if (error == OTA_BEGIN_ERROR) { + Serial.println("Begin Failed"); + } else if (error == OTA_CONNECT_ERROR) { + Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + Serial.println("End Failed"); + } + }); + +#endif /* WANT_OTA */ + + + int attempts = 0; + + while ((sensorPresent = ht2x.begin()) != true && attempts < 10) //reset sensor, set heater off, set resolution, check power (sensor doesn't operate correctly if VDD < +2.25v) + { + Serial.println(F("HTU2xD/SHT2x not connected, fail or VDD < +2.25v")); //(F()) save string to flash & keeps dynamic memory free + delay(10); + attempts++; + } + + if(sensorPresent) + Serial.println(F("HTU2xD/SHT2x OK")); + + attempts = 0; + while ((bmeSensorPresent = bme280.begin()) != true && attempts < 10) //reset sensor, set heater off, set resolution, check power (sensor doesn't operate correctly if VDD < +2.25v) + { + Serial.println(F("BME280 not connected or failed")); //(F()) save string to flash & keeps dynamic memory free + delay(10); + attempts++; + } + if(bmeSensorPresent) { + Serial.println(F("BME280 OK")); + } + + while ((sensorPresent = aqi.begin_I2C()) != true && attempts < 10) + { + Serial.println(F("PM25AQI not connected, fail or VDD < +2.25v")); //(F()) save string to flash & keeps dynamic memory free + delay(10); + attempts++; + } + + if(sensorPresent) + Serial.println(F("PM25AQI Sensor OK")); + + + // clear the wind values array. + for(int i = 0; i < 60; i++) + windValues[i] = 0.0; + currentWindValue = 0; + + ITimer0.attachInterruptInterval(TIMER0_INTERVAL_MS, updateWindValues); + attachInterrupt(digitalPinToInterrupt(RAIN_PIN), rainInterrupt, FALLING); + attachInterrupt(digitalPinToInterrupt(SPEED_PIN), windInterrupt, FALLING); + +// esp_task_wdt_init(WDT_TIMEOUT, true); //enable panic so ESP32 restarts +// esp_task_wdt_add(NULL); //add current thread to WDT watch + +} + +boolean haveStarted = false; + +bool updateWindValues(void * y) { + if(currentWindValue==60) currentWindValue = 0; +// Serial.print("updateWindValues() "); +// Serial.println(millis()); + if(currentWindValue % 3 == 0) { + // Serial.println("wantRapidWind"); + wantRapidWind = true; + } + //Serial.println(calcWindSpeed()); + windValues[currentWindValue++] = calcWindSpeed(); + return true; +} + +double calcMaxWindspeed() +{ + double q = windValues[0]; + + for(int i = 0; i < 60; i++) + if(windValues[i] > q) + q = windValues[i]; + + return q; +} + +double calcMinWindspeed() +{ + double q = windValues[0]; + + for(int i = 0; i < 60; i++) + if(windValues[i] < q) + q = windValues[i]; + + return q; +} + +double calcAvgWindspeed() +{ + double q = 0.0; + + for(int i = 0; i < 60; i++) + q += windValues[i]; + + return q/60; +} + + +// return m/s +double calcWindSpeed() +{ + double q; +//return wind_interval; + if(wind_interval < 5) return 0.0; + q = 1000.0/wind_interval; + + return q; +} + +uint16_t getWindDirReading() +{ + delay(5); + + // RP2040 does not have an internal reference, A_REF should be wired + // to the approproate refrence, on pico boards, this is set to the 3.3v regulated + // line, which is not super accurate. + //analogReference(AR_EXTERNAL); +// digitalWrite(EXCITER_PIN, HIGH); +// delay(5); + uint16_t windDir = analogRead(DIR_PIN); +// digitalWrite(EXCITER_PIN, LOW); + + return windDir; +} + +uint16_t calcWindDirDegrees() +{ + uint16_t windDirX; + + windDirX = getWindDirReading(); + + windDirX = (uint16_t)(windDirX/11.37); + + return windDirX; + +} +uint16_t calcWindDir() +{ + uint16_t windDirX; + + windDirX = getWindDirReading(); + if(windDirX >= 959) + windDirX = 0; + else + windDirX = (windDirX+(64))/(128); + + return windDirX; +} + +int rainIncrements = 0; + +static unsigned long last_rain_interrupt_time = 0; + +void windInterrupt() +{ + last_awoke = 0; + + unsigned long w_interrupt_time = millis(); + int x; + + // If interrupts come faster than 10ms, assume it's a bounce and ignore + if ((w_interrupt_time - w_last_interrupt_time) > WIND_DEBOUNCE_MS) + { + if(wind_time) + wind_interval = (w_interrupt_time - wind_time); + if(wind_interval && (!max_wind_interval || (max_wind_interval > wind_interval))) + max_wind_interval = wind_interval; + wind_time = w_interrupt_time; + w_last_interrupt_time = w_interrupt_time; + } +} + +void rainInterrupt() { + unsigned long interrupt_time = millis(); + + if ((interrupt_time - last_rain_interrupt_time) > RAIN_DEBOUNCE_MS) + { + rainIncrements++; + last_rain_interrupt_time = interrupt_time; + } +} + +void loop() { + watchdog_update(); + + if (haveStarted && eth_connected) { +#ifdef WANT_OTA + ArduinoOTA.handle(); +#else +#ifdef WANT_MDNS + // ArduinoOTA.handle() will call mdns.update(), so only need to do this if + // we aren't doing OTA. + // + // This actually runs the mDNS module. YOU HAVE TO CALL THIS PERIODICALLY, + // OR NOTHING WILL WORK! Preferably, call it once per loop(). + MDNS.update(); +#endif +#endif /* WANT_OTA */ + +// MDNS.update(); +#ifdef WANT_NTP + timeClient.update(); + ts = timeClient.getEpochTime(); +#else + ts = millis(); +#endif /* WANT_NTP */ + if ((millis() - w_last_interrupt_time) > 2000 && wind_interval != 0) wind_interval = 0; + +// Serial.println(calcWindDirDegrees()); +/* + if(((millis() / 1000) % 3) == 0) { + Serial.println("Wind Speed"); + Serial.println(calcWindSpeed()); + } +*/ + if (wantRapidWind) { + wantRapidWind = false; + rapidWind(); + } + weather_report(); + } + + if (millis() > 5000 && !haveStarted) { + if (eth_connected) { + haveStarted = true; + statusNotDisplayed = false; + + hostname.toLowerCase(); + hostname.replace(" ", "-"); + hostname.replace("_", "-"); + + br_adr = ETH.localIP(); + br_adr[3] = 255; + + Serial.println(F("Local Address")); + Serial.println(ETH.localIP()); + Serial.println(F("Smartweather destination")); + Serial.println(br_adr); + Serial.println(SMARTWEATHER_PORT); + + Serial.println(F("Setting up UDP communications")); + Serial.println(MEMP_NUM_NETCONN); + Serial.println(MEMP_NUM_UDP_PCB); + Serial.println(MEMP_NUM_PBUF); + Serial.println(MEMP_NUM_NETBUF); +#ifdef WANT_MDNS + MDNS.begin(hostname.c_str()); +#endif /* WANT_MDNS */ +#ifdef WANT_NTP + Serial.println(F("Registering NTP client")); + timeClient.begin(); +#endif /* WANT_NTP */ + + Udp.begin(SMARTWEATHER_PORT); + +#ifdef WANT_OTA + ArduinoOTA.setPassword(OTA_PASSWORD); + ArduinoOTA.setHostname(hostname.c_str()); + ArduinoOTA.begin(); +#else +#ifdef WANT_MDNS + // ArduinoOTA.handle() will call mdns.run(), so only need to do this if + // we aren't doing OTA. + // + // This actually runs the mDNS module. YOU HAVE TO CALL THIS PERIODICALLY, + // OR NOTHING WILL WORK! Preferably, call it once per loop(). + MDNS.begin(hostname.c_str(), WiFi.localIP()); +#endif /* WANT_MDNS */ +#endif /* WANT_OTA */ + +#ifdef WANT_MDNS + // Initialize the mDNS library. You can now reach or ping this + // Arduino via the host name "hostname.local", provided that your operating + // system is mDNS/Bonjour-enabled (such as MacOS X). + // Always call this before any other method! + + Serial.print(F("Registering mDNS hostname: ")); Serial.println(hostname); + Serial.print(F("To access, using ")); + Serial.print(hostname); Serial.println(F(".local")); + Serial.print(F("Local address: ")); + Serial.println(ETH.localIP()); + // mdns.begin(WiFi.localIP(), hostname.c_str()); + + Serial.println(F("Registering MDNS")); + // String svc = hostname + "._http"; + MDNS.addService("http", "tcp", 80); + +#ifdef WANT_OTA + // MDNS.enableArduino(OTA_PORT, true); +#endif /* WANT_OTA */ +#endif /* WANT_MDNS */ + + } + + } +} +void sendFailed() +{ + failedSends++; + if(failedSends > FAILED_SEND_LIMIT) + resetFunc(); +} + +void rapidWind() { + StaticJsonDocument<386> doc; + blink(FAST, 2); + + if (haveStarted) { +#ifdef WANT_NTP + timeClient.update(); + ts = timeClient.getEpochTime(); +#endif /* WANT_NTP */ + Serial.println(F("Sending observation")); + + doc["serial_number"] = airsn; + doc["hub_sn"] = hubsn; + doc["type"] = "rapid_wind"; + JsonArray ob = doc.createNestedArray("ob"); + ob.add(ts); + + // speed + // direction + double_t windSpeed = calcWindSpeed(); + if (windSpeed < 0.01) ob.add(0.0); + else ob.add(windSpeed); + ob.add(calcWindDirDegrees()); + + int written; + + if (Udp.beginPacket(br_adr, SMARTWEATHER_PORT)) + Serial.println("Packet begun 1."); + written = serializeJson(doc, Udp); + Serial.println(written); + if (Udp.endPacket()) + Serial.println("Sent."); + else + { + Serial.println("Failed to send packet."); + sendFailed(); + } + + } else { + Serial.println(F("LAN status indicates not connected, not sending rapid wind report.")); + } +} + +void hubStatus(long tsm) +{ + int written = 0; + + Serial.println(F("Last Wind Interrupt")); + Serial.println(w_last_interrupt_time); + Serial.println(F("Sending hub status")); + +#ifdef WANT_NTP + timeClient.update(); + ts = timeClient.getEpochTime(); +#else + ts = millis(); +#endif /* WANT_NTP */ + Serial.println(ts); + + StaticJsonDocument<386> doc; + + doc[F("serial_number")] = hubsn; + doc[F("type")] = "hub_status"; + doc[F("firmware_revision")] = FIRMWARE_REVISION; + doc[F("uptime")] = tsm/1000; // millis() / 1000; + doc[F("rssi")] = 0; + doc[F("timestamp")] = ts; + + + if (Udp.beginPacket(br_adr, SMARTWEATHER_PORT)) { + Serial.println("Packet begun. 2"); + written = serializeJson(doc, Udp); + Serial.println(written); + if (Udp.endPacket()) + Serial.println("Sent."); + reportcnt++; + } else { + Serial.println("Failed to send packet."); + sendFailed(); + } +} + +void deviceStatus(long tsm) { + int written = 0; + + Serial.println("Sending device status"); + + StaticJsonDocument<436> doc; + + doc["serial_number"] = airsn; + doc["hub_sn"] = hubsn; + doc["type"] = "device_status"; + doc["firmware_revision"] = FIRMWARE_REVISION; + doc["uptime"] = tsm / 1000; + doc["rssi"] = 0; + doc["hub_rssi"] = 0; + doc["voltage"] = 0.0; + doc["timestamp"] = ts; + +// if(!did_wdt_check) { +//// did_wdt_check = 1; +// if(rtc_get_reset_reason()) { +// doc["watchdog_reset"] = 1; +// } +// } + + if (Udp.beginPacket(br_adr, SMARTWEATHER_PORT)) + Serial.println("Packet begun. 3"); + written = serializeJson(doc, Udp); + Serial.println(written); + if (Udp.endPacket()) + Serial.println("Sent."); + else { + Serial.println("Failed to send packet."); + sendFailed(); + } +} + +void aqDeviceStatus(long tsm) { + int written = 0; + + Serial.println("Sending AQ device status"); + + StaticJsonDocument<436> doc; + + doc["serial_number"] = aqsn; + doc["hub_sn"] = hubsn; + doc["type"] = "device_status"; + doc["firmware_revision"] = FIRMWARE_REVISION; + doc["uptime"] = tsm / 1000; + doc["rssi"] = 0; + doc["hub_rssi"] = 0; + doc["voltage"] = 0.0; + doc["timestamp"] = ts; + +// if(!did_wdt_check) { +//// did_wdt_check = 1; +// if(rtc_get_reset_reason()) { +// doc["watchdog_reset"] = 1; +// } +// } + + if (Udp.beginPacket(br_adr, SMARTWEATHER_PORT)) + Serial.println("Packet begun. 3"); + written = serializeJson(doc, Udp); + Serial.println(written); + if (Udp.endPacket()) + Serial.println("Sent."); + else { + Serial.println("Failed to send packet."); + sendFailed(); + } +} + +void airQualityObservation(long tsm) { + StaticJsonDocument<768> doc; + int written = 0; + + PM25_AQI_Data data; + int attempts = 0; + while (attempts < 10 && !aqi.read(&data)) { + Serial.println("Could not read from AQI"); + delay(500); // try again in a bit! + attempts++; + } + Serial.println("AQI reading success"); + + Serial.println(); + Serial.println(F("---------------------------------------")); + Serial.println(F("Concentration Units (standard)")); + Serial.println(F("---------------------------------------")); + Serial.print(F("PM 1.0: ")); Serial.print(data.pm10_standard); + Serial.print(F("\t\tPM 2.5: ")); Serial.print(data.pm25_standard); + Serial.print(F("\t\tPM 10: ")); Serial.println(data.pm100_standard); + Serial.println(F("Concentration Units (environmental)")); + Serial.println(F("---------------------------------------")); + Serial.print(F("PM 1.0: ")); Serial.print(data.pm10_env); + Serial.print(F("\t\tPM 2.5: ")); Serial.print(data.pm25_env); + Serial.print(F("\t\tPM 10: ")); Serial.println(data.pm100_env); + Serial.println(F("---------------------------------------")); + Serial.print(F("Particles > 0.3um / 0.1L air:")); Serial.println(data.particles_03um); + Serial.print(F("Particles > 0.5um / 0.1L air:")); Serial.println(data.particles_05um); + Serial.print(F("Particles > 1.0um / 0.1L air:")); Serial.println(data.particles_10um); + Serial.print(F("Particles > 2.5um / 0.1L air:")); Serial.println(data.particles_25um); + Serial.print(F("Particles > 5.0um / 0.1L air:")); Serial.println(data.particles_50um); + Serial.print(F("Particles > 10 um / 0.1L air:")); Serial.println(data.particles_100um); + Serial.println(F("---------------------------------------")); + +#ifdef WANT_NTP + ts = timeClient.getEpochTime(); +#endif /* WANT_NTP */ + Serial.println("Sending observation"); + + lastobservation = (tsm/1000); + doc["serial_number"] = aqsn; + doc["hub_sn"] = hubsn; + doc["type"] = "obs_pm"; + doc["firmware_revision"] = FIRMWARE_REVISION; + JsonArray obs = doc.createNestedArray("obs"); + JsonArray obsValues = obs.createNestedArray(); + obsValues.add(ts); + obsValues.add(data.pm10_standard); + obsValues.add(data.pm25_standard); + obsValues.add(data.pm100_standard); + obsValues.add(data.pm10_env); + obsValues.add(data.pm25_env); + obsValues.add(data.pm100_env); + obsValues.add(data.particles_03um); + obsValues.add(data.particles_05um); + obsValues.add(data.particles_10um); + obsValues.add(data.particles_25um); + obsValues.add(data.particles_50um); + obsValues.add(data.particles_100um); + // sample interval + obsValues.add(60); + + // battery voltage + obsValues.add(0); + + obsValues.add((report_frequency/60)); + + if(Udp.beginPacket(br_adr, SMARTWEATHER_PORT)) + Serial.println("Packet begun. 4"); + written = serializeJson(doc, Udp); + Serial.println(written); + + if(Udp.endPacket()) + Serial.println("Sent."); + else + { + Serial.println("Failed to send packet."); + sendFailed(); + } +} + +void weatherObservation(long tsm) { + StaticJsonDocument<512> doc; + int written = 0; + +#ifdef WANT_NTP + ts = timeClient.getEpochTime(); +#endif /* WANT_NTP */ + Serial.println("Sending observation"); + + lastobservation = (tsm/1000); + doc["serial_number"] = airsn; + doc["hub_sn"] = hubsn; + doc["type"] = "obs_st"; + doc["firmware_revision"] = FIRMWARE_REVISION; + JsonArray obs = doc.createNestedArray("obs"); + JsonArray obsValues = obs.createNestedArray(); + obsValues.add(ts); + // lull + // avg + // gust + // direction + // sample interval + obsValues.add(calcMinWindspeed()); + obsValues.add(calcAvgWindspeed()); + obsValues.add(calcMaxWindspeed()); + obsValues.add(calcWindDirDegrees()); + obsValues.add(60); + + if(bmeSensorPresent) { + float t(NAN),h(NAN),p(NAN); + + BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); + BME280::PresUnit presUnit(BME280::PresUnit_Pa); + + bme280.read(p, t, h, tempUnit, presUnit); + + p/=100; + + if(sensorPresent) { + float t, h; + t = ht2x.readTemperature(); + h = ht2x.readHumidity(); + } + + Serial.print("T "); + Serial.println(t); + + Serial.print("H "); + Serial.println(h); + + Serial.print("P "); + Serial.println(p); + + obsValues.add(p); + obsValues.add(t); + obsValues.add(h); + + } else { + obsValues.add(0); + if(sensorPresent) { + float t,h; + t = ht2x.readTemperature(); + h = ht2x.readHumidity(); + + Serial.print("T "); + Serial.println(t); + + Serial.print("H "); + Serial.println(h); + + obsValues.add(t); + obsValues.add(h); + } else { + obsValues.add(0); + obsValues.add(0); + } + } + + // ill + obsValues.add(0); + // uv + obsValues.add(0); + // rad + obsValues.add(0); + //rain prev minute + // employing a 0.01 inch increment gauge + int rain = rainIncrements; + rainIncrements = 0; + + Serial.println("rain increments"); + Serial.println(rain); + obsValues.add(rain * (0.01 * 25.4)); + + // precip type + obsValues.add(0); + // lightning distance + obsValues.add(0); + // lightning count + obsValues.add(0); + // battery voltage + obsValues.add(0); + + obsValues.add((report_frequency/60)); + if(bmeSensorPresent && sensorPresent) { + float t, h; + t = ht2x.readTemperature(); + h = ht2x.readHumidity(); + + Serial.print("T "); + Serial.println(t); + + Serial.print("H "); + Serial.println(h); + + obsValues.add(t); + obsValues.add(h); + } + + if(Udp.beginPacket(br_adr, SMARTWEATHER_PORT)) + Serial.println("Packet begun. 4"); + written = serializeJson(doc, Udp); + Serial.println(written); + + if(Udp.endPacket()) + Serial.println("Sent."); + else + { + Serial.println("Failed to send packet."); + sendFailed(); + } +} + + +void weather_report() { + long tsm = millis(); + + if(tsm > RESET_INTERVAL) { + Serial.println("Uptime limit reached (RESET_INTERVAL). Resetting."); + delay(1000); + resetFunc(); + } + + if((tsm/1000) > (lastreport + 30)) { +// if(WiFi.status() != WL_CONNECTED) { +// Serial.println("Lost connectivity with WLAN. Resetting."); +// delay(1000); +// resetFunc(); +// } + lastreport = tsm / 1000; +#ifdef WANT_NTP + +#endif /* WANT_NTP */ + // blink(SLOW, 3); + hubStatus(tsm); + deviceStatus(tsm); + aqDeviceStatus(tsm); + } + + if((tsm/1000) > (lastobservation + report_frequency)) { + watchdog_update(); + weatherObservation(tsm); + airQualityObservation(tsm); + } +} \ No newline at end of file diff --git a/firmware/esp32-ethernet/test/README b/firmware/esp32-ethernet/test/README new file mode 100644 --- /dev/null +++ b/firmware/esp32-ethernet/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html