initial commit of ethernet/poe wx station
A => firmware/esp32-ethernet/.hgignore +1 -0
@@ 0,0 1,1 @@ 
+.pio

          
A => firmware/esp32-ethernet/README.md +2 -0
@@ 0,0 1,2 @@ 
+This firmware is configured for use with the Olimex ESP32-POE-ISO
+

          
A => firmware/esp32-ethernet/include/README +39 -0
@@ 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

          
A => firmware/esp32-ethernet/include/config.h +20 -0
@@ 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

          
A => firmware/esp32-ethernet/lib/README +46 -0
@@ 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 <Foo.h>
+#include <Bar.h>
+
+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

          
A => firmware/esp32-ethernet/platformio.ini +43 -0
@@ 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

          
A => firmware/esp32-ethernet/src/main.cpp +1085 -0
@@ 0,0 1,1085 @@ 
+#include "config.h"
+#include <Arduino.h>
+#include <Preferences.h>
+#include <WiFi.h>
+
+#include <TimerInterrupt_Generic.h>
+
+#ifdef WANT_OTA
+#include <ArduinoOTA.h>
+#endif /* WANT_OTA */
+
+#ifdef WANT_NTP
+#include <NTPClient.h>
+#endif /* WANT_NTP */
+
+#ifdef WANT_MDNS
+#include <ESPmDNS.h>
+#endif /* WANT_MDNS */
+
+#include <StreamUtils.h>
+#include <ArduinoJson.h>
+
+#include <HTU2xD_SHT2x_Si70xx.h>
+#include <BME280I2C.h>
+#include <Adafruit_PM25AQI.h>
+
+#include <esp_task_wdt.h>
+//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 <ETH.h>
+
+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

          
A => firmware/esp32-ethernet/test/README +11 -0
@@ 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