esp8266 base station/sensor for smartweather api
M firmware/base_station/platformio.ini +4 -3
@@ 6,12 6,13 @@ 
 ;   Advanced options: extra scripting
 ;
 ; Please visit documentation for the other options and examples
-; http://docs.platformio.org/page/projectconf.html
+; https://docs.platformio.org/page/projectconf.html
 
 [env:diecimilaatmega328]
 platform = atmelavr
 board = diecimilaatmega328
 framework = arduino
+build_unflags = -Wall
 lib_deps = 
-https://github.com/jcw/ethercard.git
-;https://github.com/jcw/jeelib.git
+	bblanchon/ArduinoJson@^6.18.3
+	https://github.com/jeelabs/jeelib.git

          
M firmware/base_station/src/base_station.cpp +148 -57
@@ 1,28 1,27 @@ 
 #include <Arduino.h>
+#include <ArduinoJson.h>
 
 
-#include <enc28j60.h>
-#include <EtherCard.h>
+#include "../lib/EtherCard/EtherCard.h"
 #include <net.h>
 
 #include <JeeLib.h>
 #include <RF12.h>
 
-#if 0
-#include <Wire.h>
-
-#endif
-
 #include <avr/wdt.h>
 
 void postData (uint8_t *dip, struct payload * pl);
-static void addPayload (struct payload * data);
+
+#define MAX_JSON_LEN 150
 
 #define WX_DATA_PORT 7656
 
-#define gPB ether.buffer
-
-static byte* bufPtr;
+char json[MAX_JSON_LEN];
+uint8_t bc_dest[4];
+int gotTime = 0;
+unsigned long epoch;
+unsigned long epoch_came;
+int i = 0;
 /*
  struct {
    uint8_t station_id;

          
@@ 62,8 61,13 @@ struct payload {
 typedef struct payload wx_data;
 
 // typedef struct { int16_t temp; int32_t pres; } Payload;
-int i = 0;
-static byte mymac[] = { 0x74, 0x69, 0x69, 0x2d, 0x30, 0x31 };
+
+const byte mymac[] = { 0x74, 0x69, 0x69, 0x2d, 0x30, 0x31 };
+
+const char NTP_REMOTEHOST[] PROGMEM = "time.apple.com";  // NTP server name
+const unsigned int NTP_REMOTEPORT = 123;             // NTP requests are to port 123
+const unsigned int NTP_LOCALPORT = 8888;             // Local UDP port to use
+const unsigned int NTP_PACKET_SIZE = 48;             // NTP time stamp is in the first 48 bytes of the message
 
 byte Ethernet::buffer[700];
 

          
@@ 92,21 96,96 @@ void wdt_init(void)
     return;
 }
 */
+	
+	
+void udpReceiveNtpPacket(uint16_t dest_port, uint8_t src_ip[IP_LEN], uint16_t src_port, const char *packetBuffer, uint16_t len) {
+//StaticJsonDocument<100> doc;
+	epoch_came = millis();
+  Serial.println(F("NTP response received."));
+  
+  // the timestamp starts at byte 40 of the received packet and is four bytes,
+  // or two words, long. First, extract the two words:
+  unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
+  unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
+  // combine the four bytes (two words) into a long integer
+  // this is NTP time (seconds since Jan 1 1900):
+  unsigned long secsSince1900 = highWord << 16 | lowWord;
+  // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
+  const unsigned long seventyYears = 2208988800UL;
+  // subtract seventy years:
+  epoch = secsSince1900 - seventyYears;
+
+  // print Unix time:
+  Serial.print(F("Unix time = "));
+  Serial.println(epoch);
+  gotTime = 1;
+//  size_t docsize;
+//  ++dnstid_l; // increment for next request, finally wrap
+ 
+//  DynamicJsonDocument  doc(200);
+
+//  doc["sensor"] = "gps";
+//  doc["time"] = 1351824120;
+
+  // JsonArray data = doc.createNestedArray("data");
+//   data.add(48.75608);
+//   data.add(2.302038);
+
+
+//  doc["ts"] = epoch + ((millis() - epoch_came)/1000);
+ 	//    docsize = measureJson(doc);
+	//   Serial.println(F("doc size"));
+	//     Serial.println(docsize);
+	// serializeJson(doc, json, MAX_JSON_LEN);
+	// Serial.println(F("json comin at ya"));
+	// Serial.println(json);
+// ether.sendUdp(json, docsize, WX_DATA_PORT, bc_dest, WX_DATA_PORT);
+	
+}
+
+// send an NTP request to the time server at the given address
+void sendNTPpacket(const uint8_t* remoteAddress) {
+  // buffer to hold outgoing packet
+  byte packetBuffer[ NTP_PACKET_SIZE];
+  // set all bytes in the buffer to 0
+  memset(packetBuffer, 0, NTP_PACKET_SIZE);
+  // Initialize values needed to form NTP request
+  // (see URL above for details on the packets)
+  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
+  packetBuffer[1] = 0;            // Stratum, or type of clock
+  packetBuffer[2] = 6;            // Polling Interval
+  packetBuffer[3] = 0xEC;         // Peer Clock Precision
+  // 8 bytes of zero for Root Delay & Root Dispersion
+  packetBuffer[12]  = 49;
+  packetBuffer[13]  = 0x4E;
+  packetBuffer[14]  = 49;
+  packetBuffer[15]  = 52;
+
+  // all NTP fields have been given values, now
+  // you can send a packet requesting a timestamp:
+  ether.sendUdp(packetBuffer, NTP_PACKET_SIZE, NTP_LOCALPORT, remoteAddress, NTP_REMOTEPORT );
+  Serial.println(F("NTP request sent."));
+}
+
+	
 void setup() {
  Serial.begin(57600);
- Serial.println("\n[bmp085recv]\n");
+ Serial.println(F("\n[bmp085recv]\n"));
  
-// rf12_initialize(30, RF12_915MHZ, 5);
+  rf12_initialize(30, RF12_915MHZ, 5);
+
+ Serial.println(F("\nRF12 initialized\n"));
  
 // return;
   
-  if(ether.begin(sizeof Ethernet::buffer, mymac) == 0)
-  {
-    Serial.println("failed to access Ethernet controller");
-  }
+ if (ether.begin(sizeof Ethernet::buffer, mymac, 8) == 0)
+     Serial.println(F("Failed to access Ethernet controller"));
+ 
+  Serial.println(F("\nEtherCard initialized\n"));
+  
   if(!ether.dhcpSetup())
   {
-    Serial.println("DHCP failed");    
+    Serial.println(F("DHCP failed"));    
     delay(5000);
     void (*softReset) (void) = 0; //declare reset function @ address 0
     softReset();

          
@@ 114,27 193,44 @@ void setup() {
   ether.printIp("IP: ", ether.myip);
   ether.printIp("GW: ", ether.gwip);
   
-  if(!ether.dnsLookup(PSTR("192.168.1.10")))
-  {
-    Serial.println("DNS failed");
-    delay(5000);
-    void (*softReset) (void) = 0; //declare reset function @ address 0
-    softReset();
-  }    
-  ether.printIp("SRV: ", ether.hisip);
-    
-//  ether.registerPingCallback(gotPinged);
+   bc_dest[0] = ether.myip[0];
+    bc_dest[1] = ether.myip[1];
+   bc_dest[2] = ether.myip[2];
+    bc_dest[3] = 255;
+  
+  if (!ether.dnsLookup(NTP_REMOTEHOST)) {
+     Serial.println(F("DNS failed"));
+     delay(5000);
+     void (*softReset) (void) = 0; //declare reset function @ address 0
+     softReset();
+  }
+ 
+   uint8_t ntpIp[IP_LEN];
+   ether.copyIp(ntpIp, ether.hisip);
+   ether.printIp("NTP: ", ntpIp);
+
+   ether.udpServerListenOnPort(&udpReceiveNtpPacket, NTP_LOCALPORT);
+   Serial.println(F("Started listening for response."));
+
+   sendNTPpacket(ntpIp);
+
 }
 
 void loop() {
+	payload d;
 //return; 
-	 if(rf12_recvDone() && rf12_crc == 0)
-// if(rf12_len != sizeof(wx_data)) 
+//	 if(rf12_recvDone() && rf12_crc == 0)
+	 if(rf12_len == sizeof(wx_data)) 
 // else
- {
+	 if(rf12_recvDone()) {
+			 Serial.println("receive done");
+			 Serial.println(rf12_len);
+		 }
+if(0) {
+	 Serial.println(F("Received data"));
      wx_data * data = (wx_data *) rf12_data;
      Serial.print((int)data->station_id);
-     Serial.print(" BMP / RHumidity / Rain / Wind / Lux ");
+     Serial.print(F(" BMP / RHumidity / Rain / Wind / Lux "));
      Serial.print(data->cookiea);
      Serial.print(data->cookieb);
      Serial.print(data->cookiec);

          
@@ 167,31 263,26 @@ void loop() {
     postData(ether.hisip, data);
  //    i++;
  } 
+ ether.packetLoop(ether.packetReceive());
+ if(gotTime && i < 2) {
+	 delay(1200);
+ 	    postData(ether.hisip, &d);
+		i++;
+ }
+ //Serial.println("f");
 }
 
  void postData (uint8_t *dip, struct payload * pl) {
+     size_t docsize;
+     StaticJsonDocument<100> doc;
   ++dnstid_l; // increment for next request, finally wrap
-
-  ether.udpPrepare((DNSCLIENT_SRC_PORT_H << 8) | dnstid_l,
-                                                dip, WX_DATA_PORT);
-  memset(gPB + UDP_DATA_P, 1, sizeof(struct payload));
-
-  bufPtr = gPB + UDP_DATA_P;
-  addPayload(pl);
-
-  ether.udpTransmit((bufPtr - gPB) - UDP_DATA_P);
+  
+  doc["station_id"] = (1000 + pl->station_id);
+  doc["uptime"] = pl->uptime;
+  doc["ts"] = (epoch + ((millis() - epoch_came)/1000));
+  doc["temp"] = pl->temp;
+  doc["humidity"] = pl->relhx;
+  docsize = measureJson(doc);
+  serializeJson(doc, json, MAX_JSON_LEN);
+  ether.sendUdp(json, docsize, WX_DATA_PORT, bc_dest, WX_DATA_PORT);
 }
-
-static void addToBuf (byte b) {
-    *bufPtr++ = b;
-}
-
-static void addPayload (struct payload * data) {
-  int len = sizeof(struct payload);
-  byte * d;
-  
-  d = (byte *) data;
-    while (len-- > 0)
-        addToBuf(*d++);
-}
-

          
A => firmware/remote_sensors_esp32/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/remote_sensors_esp32/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/remote_sensors_esp32/platformio.ini +27 -0
@@ 0,0 1,27 @@ 
+; 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:nodemcuv2]
+upload_protocol = espota
+upload_port = 192.168.5.73
+upload_flags = --auth=0681
+platform = espressif8266
+platform_packages = 
+	platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git
+board = nodemcuv2
+framework = arduino
+lib_deps = 
+	beegee-tokyo/SHT1x-ESP@^1.0.0
+	tzapu/WiFiManager
+	https://github.com/esp8266/ESPWebServer.git
+	bblanchon/ArduinoJson@^6.18.4
+	arduino-libraries/NTPClient@^3.1.0
+	mathertel/OneButton@^2.0.2
+	jwrw/ESP_EEPROM@^2.1.1

          
A => firmware/remote_sensors_esp32/src/main.cpp +312 -0
@@ 0,0 1,312 @@ 
+#include <FS.h>                   //this needs to be first, or it all crashes and burns...
+
+#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino
+#include <ESP8266mDNS.h>
+#include <WiFiUdp.h>
+#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager
+#include <NTPClient.h>
+#include <ArduinoOTA.h>
+#include <Arduino.h>
+#include <ArduinoJson.h>
+#include <SHT1x.h>
+#include <ESP_EEPROM.h>
+#include <OneButton.h>
+
+#define DEBUG(X...) do{if(Serial)Serial.println(X);}while(0)
+#define DEBUGCHAR(X...) do{if(Serial)Serial.print(X);}while(0)
+
+#define FIRMWARE_REVISION 1024
+
+#define SLOW 250
+#define MEDIUM 100
+#define FAST 50
+
+#define sleep(X) delay(X)
+#define ON 1
+#define OFF 0
+
+WiFiClient wifiClient;
+
+#define dataPin D6
+#define clockPin D5
+
+#define SMARTWEATHER_PORT 50222
+// if 3.3v board is used
+SHT1x sht1x(dataPin, clockPin, SHT1x::Voltage::DC_3_3v);
+IPAddress br_adr;
+WiFiUDP udp;
+WiFiUDP ntpUdp;
+
+String hubsn;
+String airsn;
+int reportfreq = 60;
+
+NTPClient timeClient(ntpUdp);
+
+
+
+#define BUTTON_PIN 0
+
+/**
+ * Initialize a new OneButton instance for a button
+ * connected to digital pin 4 and GND, which is active low
+ * and uses the internal pull-up resistor.
+ */
+
+OneButton btn = OneButton(
+        BUTTON_PIN,  // Input pin for the button
+        true,        // Button is active LOW
+        true         // Enable internal pull-up resistor
+);
+
+
+long ts;
+int reportcnt = 0;
+int lastobservation = 0;
+int lastreport = 0;
+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 resetEverything() {
+    WiFi.enableSTA(true);
+    WiFi.persistent(true);
+    WiFi.disconnect(true);
+    WiFi.persistent(false);
+
+    delay(250);
+    ESP.reset();
+    delay(5000);
+}
+
+bool shouldSaveConfig = false;
+bool enteredConfigMode = false;
+void configModeCallback (WiFiManager *myWiFiManager) {
+    Serial.println("config mode");
+    enteredConfigMode = true;
+}
+void saveConfigCallback () {
+    Serial.println("Should save config now");
+    if(enteredConfigMode)
+        shouldSaveConfig = true;
+}
+
+void writeStringToEEPROM(int addrOffset, const String &strToWrite)
+{
+    uint8_t len = strToWrite.length();
+    EEPROM.write(addrOffset, len);
+    for (int i = 0; i < len; i++)
+    {
+        EEPROM.write(addrOffset + 1 + i, strToWrite[i]);
+    }
+}
+
+String readStringFromEEPROM(int addrOffset)
+{
+    uint8_t newStrLen;
+    Serial.println("reading a string from eeprom");
+    EEPROM.get(addrOffset, newStrLen);
+
+    char data[newStrLen + 1];
+    for (int i = 0; i < newStrLen; i++)
+    {
+        data[i] = EEPROM.read(addrOffset + 1 + i);
+    }
+    data[newStrLen] = '\0'; // !!! NOTE !!! Remove the space between the slash "/" and "0" (I've added a space because otherwise there is a display bug)
+    return String(data);
+}
+//***********************************************
+
+void setup() {
+    blink(MEDIUM, 10);
+    // put your setup code here, to run once:
+    if(Serial)
+        Serial.begin(115200);
+
+    DEBUG("Welcome\n");
+
+    delay(100);
+   // resetEverything();
+    //WiFiManager
+    //Local intialization. Once its business is done, there is no need to keep it around
+    WiFiManager wifiManager;
+
+    WiFiManagerParameter hub_serial_number("hubsn", "HubSerialNumber", "10010", 7);
+    wifiManager.addParameter(&hub_serial_number);
+
+    WiFiManagerParameter air_serial_number("airsn", "AirSerialNumber", "10012", 7);
+    wifiManager.addParameter(&air_serial_number);
+
+    WiFiManagerParameter report_frequency("reportfreq", "ReportFreq (seconds)", "60", 4);
+    wifiManager.addParameter(&report_frequency);
+
+    //exit after config instead of connecting
+   wifiManager.setBreakAfterConfig(true);
+
+    wifiManager.setAPCallback(configModeCallback);
+    wifiManager.setSaveConfigCallback(saveConfigCallback);
+    //reset settings - for testing - seems broken, see resetEverything()
+    //wifiManager.resetSettings();
+
+    //tries to connect to last known settings
+    //if it does not connect it starts an access point with the specified name
+    //here  "AutoConnectAP" with password "password"
+    //and goes into a blocking loop awaiting configuration
+
+ if (!wifiManager.autoConnect()) {
+        DEBUG("WifiManager: failed to connect, we should reset as see if it connects");
+        delay(3000);
+        ESP.reset();
+        delay(5000);
+    }
+
+    blink(MEDIUM, 2);
+
+  ArduinoOTA.onStart([]() {
+        DEBUG("OTA Start");
+    });
+    ArduinoOTA.onEnd([]() {
+        DEBUG("\nOTA End");
+    });
+    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
+        if(Serial) Serial.printf("OTA: Progress: %u%%\r", (progress / (total / 100)));
+    });
+    ArduinoOTA.onError([](ota_error_t error) {
+        if(Serial) Serial.printf("OTA Error[%u]: ", error);
+        if (error == OTA_AUTH_ERROR) DEBUG("OTA: Auth Failed");
+        else if (error == OTA_BEGIN_ERROR) DEBUG("OTA: Begin Failed");
+        else if (error == OTA_CONNECT_ERROR) DEBUG("OTA: Connect Failed");
+        else if (error == OTA_RECEIVE_ERROR) DEBUG("OTA: Receive Failed");
+        else if (error == OTA_END_ERROR) DEBUG("OTA: End Failed");
+    });
+
+    ArduinoOTA.setPassword((const char *)"0681");
+
+    //if you get here you have connected to the WiFi
+    DEBUG("connected...yay :)");
+
+    ArduinoOTA.begin();
+
+    DEBUG("local ip: ");
+    DEBUG(WiFi.localIP());
+    br_adr = WiFi.localIP();
+    br_adr[3] = 255;
+
+    EEPROM.begin(50);
+
+    if (shouldSaveConfig) {
+        String hs ="HB-";
+        hs = hs + (hub_serial_number.getValue());
+        hubsn = hs;
+        String as = "AR-";
+        as = as + (air_serial_number.getValue());
+        airsn = as;
+        String frq = String(report_frequency.getValue());
+        reportfreq = (frq).toInt();
+        writeStringToEEPROM(0, hubsn); // 7 bytes max
+        writeStringToEEPROM(15, airsn); // 7 bytes max
+        writeStringToEEPROM(30, frq); // 4 bytes max
+        if(!EEPROM.commit()) resetEverything();
+        delay(100);
+
+    } else if(1){
+       hubsn = readStringFromEEPROM(0);
+       airsn = readStringFromEEPROM(15);
+
+        reportfreq = 0; // (readStringFromEEPROM(30)).toInt();
+    }
+    if(reportfreq < 30) reportfreq = 30;
+
+    udp.begin(SMARTWEATHER_PORT);
+
+    timeClient.begin();
+
+    delay(1000);
+    Serial.println("ready to read");
+
+    btn.attachLongPressStart(resetEverything);
+}
+
+void loop() {
+    timeClient.update();
+    ArduinoOTA.handle();
+    long tsm = millis();
+    btn.tick();
+
+    if((tsm/1000) > (lastreport + 30)) {
+        lastreport = millis() / 1000;
+        ts = timeClient.getEpochTime();
+
+        blink(SLOW, 3);
+
+        if (1) {
+            StaticJsonDocument<200> doc;
+
+            doc["serial_number"] = hubsn;
+            doc["type"] = "hub_status";
+            doc["firmware_revision"] = FIRMWARE_REVISION;
+            doc["uptime"] = millis() / 1000;
+            doc["rssi"] = WiFi.RSSI();
+            doc["timestamp"] = ts;
+
+            udp.beginPacket(br_adr, SMARTWEATHER_PORT);
+            serializeJson(doc, udp);
+            udp.println();
+            udp.endPacket();
+            reportcnt++;
+        }
+        if (1) {
+            StaticJsonDocument<200> doc;
+
+            doc["serial_number"] = airsn;
+            doc["hub_sn"] = hubsn;
+            doc["type"] = "device_status";
+            doc["firmware_revision"] = FIRMWARE_REVISION;
+            doc["uptime"] = millis() / 1000;
+            doc["rssi"] = WiFi.RSSI();
+            doc["hub_rssi"] = WiFi.RSSI();
+            doc["voltage"] = 0.0;
+            doc["timestamp"] = ts;
+
+            udp.beginPacket(br_adr, SMARTWEATHER_PORT);
+            serializeJson(doc, udp);
+            udp.println();
+            udp.endPacket();
+        }
+    }
+    if((tsm/1000) > (lastobservation + reportfreq)) {
+        StaticJsonDocument<250> doc;
+        blink(FAST, 2);
+
+        ts = timeClient.getEpochTime();
+
+        lastobservation = (tsm/1000);
+        doc["serial_number"] = airsn;
+        doc["hub_sn"] = hubsn;
+        doc["type"] = "obs_air";
+        doc["firmware_revision"] = FIRMWARE_REVISION;
+        JsonArray obs = doc.createNestedArray("obs");
+        JsonArray obsValues = obs.createNestedArray();
+        obsValues.add(ts);
+        obsValues.add(0.0);
+        obsValues.add(sht1x.readTemperatureC());
+        obsValues.add(sht1x.readHumidity());
+        obsValues.add(0);
+        obsValues.add(0);
+        obsValues.add(0.0);
+        obsValues.add((reportfreq/60));
+
+        udp.beginPacket(br_adr, SMARTWEATHER_PORT);
+        serializeJson(doc, udp);
+        udp.println();
+        udp.endPacket();
+    }
+    delay(100);
+}
  No newline at end of file

          
A => firmware/remote_sensors_esp32/test/README +11 -0
@@ 0,0 1,11 @@ 
+
+This directory is intended for PlatformIO Unit Testing 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/page/plus/unit-testing.html