diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/WS3/.DS_Store b/WS3/.DS_Store new file mode 100644 index 0000000..9173708 Binary files /dev/null and b/WS3/.DS_Store differ diff --git a/WS3/Serial-ESP/Serial-ESP.ino b/WS3/Serial-ESP/Serial-ESP.ino new file mode 100644 index 0000000..d1c6700 --- /dev/null +++ b/WS3/Serial-ESP/Serial-ESP.ino @@ -0,0 +1,823 @@ +// Weather Station 3 from AliExpress / TaoBao +// +// Highly inspired by https://github.com/kquinsland/ws3-to-esphome-bridge/ +// +// Maybe the code is not optimal but it works. +// +// Configuration of WS3 : +// +// JP1 toggle betwen ARS inch mode and professional metric mode +// short : inch +// open : metric +// +// JP2 baud rate +// short : 2400 +// open : 9600 +// +// Configuration used for this code : +// JP1 : open +// JP2 : closed +// +// Data is in the form : +// +// A4095B000C0000D0000E0000F0000G0000H0000I0000J0000K0000L0218M515N09654O.....*52 +// +#include +#include // Needed? +#include +#include +#include +//#include +// EspSoftwareSerial (or plain SoftwareSerial) +#include +// MQTT +#include +// WiFiManager +#include +#include // Version 5 ONLY (TODO upgrade this) + +#ifdef ESP32 + #iclude +#endif /* ESP32 */ + +// Domoticz MQTT configuration overidded by WiFiManager +const char *mqtt_server = "portbuild.home.oav.net"; +#define mqtt_port 1883 + +// Values +//char cfg_mqtt_server[40]; +//char cfg_mqtt_port[6]= "1883"; +char idx_windir[4]="48"; +char idx_temp[4] ="52"; +char idx_rain[4] ="51"; + + +volatile unsigned int loopcount=0; // number of loops (300 = 10 minutes, since every Serial loops is 2s) +volatile unsigned int WindGust=0; // wind gust data values to keep track of the strongest gust in the last 10 minutes + + +// Definitions +const char* Mqtt_clientid = "ESP-Weather-Station"; +const char* dom_in = "domoticz/in"; +#define MQTT_MAX_PACKET_SIZE 128 +char msgToPublish[MQTT_MAX_PACKET_SIZE + 1]; + +// Defines +#define DEBUG 1 + +#ifdef DEBUG + #define debug(x) Serial.print(x) + #define debugln(x) Serial.println(x) +#else /* DEBUG */ + #define debug(x) + #define debugln(x) +#endif /* DEBUG */ +// Baud used to read see JP2 +#define WS3_BAUD 2400 +// Toggle support for PM2.5 sensor +//#define SUPPORT_PM25_SENSOR + +// Define the length of data +#ifdef SUPPORT_PM25_SENSOR + // There is 88 bytes per packets + #define WS3_PKT_LEN 78 + // And the checksum is the last 2 bytes + #define WS3_CHK_LEN 2 + #define CHK_SUM_DELINEATOR 75 +#else /* SUPPORT_PM25_SENSOR */ + // There is 88 bytes per packets + #define WS3_PKT_LEN 78 + // And the checksum is the last 2 bytes + #define WS3_CHK_LEN 2 + #define CHK_SUM_DELINEATOR 75 +#endif /* SUPPRT_PM25_SENSOR */ + +// Seems the Metric format does not have a correct checksum +// In this case we should not test the checksum, just see +// if we have a correct dataline +#define DONT_CHKSUM 1 + +// Place holder for the packet received +String pkt_str = ""; + +// Flag for packet OK +volatile byte pkt_ok = false; + +// Flag for saving data +bool shouldSaveConfig = false; + +SoftwareSerial WS3(15,16); +WiFiClient espClient; +PubSubClient client(espClient); + +// After parsing the string of bytes, we'll have an easier to use struct +// TODO: this should be it's own file? +struct WS3Packet { + + // The 1st field is "A0000" - Wind direction AD value in real time (0-4096) + unsigned int wind_dir; + // The 2nd field is "B000" - Wind direct angle value (16 direction) + unsigned int wind_angle; // new + // The 3rd field is "C0000" - Real time wind speed frequency 1Hz + unsigned int wind_freq; // New + // The 4th field is "D0000" - Real time wind speed, unit: 0.1 m/s + unsigned int wind_speed; + // The 5th field is "E0000" - Avg wind speed in the previous minute, unit: 0.1m/s + unsigned int wind_speed_1m; + // The 6th field is "F0000" - the highest wind speed in the last 5 minutes, unit: 0.1m/s + unsigned int wind_speed_5m; + // The 7th field is "G0000" - Real time rain bucket (0-9999), loop-count + int rain_bucket_cnt; // New + // The 8th field is "H0000" - Number of rain bucket in the last minute, (0-9999) + int rt_rain_bucket; + // The 9th field is "I0000" - Rain fall in 1 minute, unit: 0.1mm + float rain_1m; + // The 10th field is "J0000" - the previous hour's rainfall ( 0.1 mm) + float rain_1h; + // The 11th field is "K0000" - rainfall during the first 24 hours ( 0.1 mm) + float rain_24h; + // The 12th field is "L0000" - temperature, unit: degree C (unit 0.1 Degree) + float temp_f; + // The 13th field is "M000" - humidity ( 00 % - 99 %), unit 0.1% + float humidity; + // The 14th field is "M10020" - air pressure ( 0.1 hpa ) + float air_pressure; +}; + +// Return the index according to Wind Angle +String str_windir(unsigned int WinVal){ + //debug("str_windir() : "); + //debugln(WinVal); + if(WinVal >= 360) return "N"; //N + if(WinVal >= 0 && WinVal < 22) return "N"; //N + if(WinVal >= 22 && WinVal < 45) return "NNE"; //NNE + if(WinVal >= 45 && WinVal < 67) return "NE"; //NE + if(WinVal >= 67 && WinVal < 90) return "ENE"; //ENE + if(WinVal >= 90 && WinVal < 112) return "E"; //E + if(WinVal >= 112 && WinVal < 135) return "ESE"; //ESE + if(WinVal >= 135 && WinVal < 157) return "SE"; //SE + if(WinVal >= 157 && WinVal < 180) return "S"; //S + if(WinVal >= 180 && WinVal < 202) return "S"; //S + if(WinVal >= 202 && WinVal < 225) return "SSW"; //SSW + if(WinVal >= 225 && WinVal < 247) return "SW"; //SW + if(WinVal >= 247 && WinVal < 270) return "WSW"; //WSW + if(WinVal >= 270 && WinVal < 292) return "W"; //W + if(WinVal >= 292 && WinVal < 315) return "WNW"; //WNW + if(WinVal >= 315 && WinVal < 337) return "NW"; //NW + if(WinVal >= 337 && WinVal < 359) return "NNW"; //NNW +} + +// Setup OTA stuff +void setup_ota() { + //Port defaults to 8266 + //ArduinoOTA.setPort(8266); + + // Hostname defaults to projet name + ArduinoOTA.setHostname(Mqtt_clientid); + + // No auth per default + // XXX: Change this + ArduinoOTA.setPassword((const char *)"123"); + + ArduinoOTA.onStart([]() { + Serial.println("Start"); + }); + + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + + 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"); + }); + + ArduinoOTA.begin(); +} + +// Setup the stuff. +void setup() { + //Serial.begin(460800); + Serial.begin(57600); + while (!Serial) ; // wait for Arduino Serial Monitor to open + Serial.println("\n\n"); + setupSpiffs(); + Serial.println("Weather Station 3 Adapter by Kiwi"); + Serial.println(ESP.getFullVersion()); + +// WiFi.hostname("ESP-Weather-Station"); +// WiFi.mode(WIFI_STA); +// WiFi.begin(ssid, password); + + // Use this loop instead to wait for an IPv6 routable address + + // addr->isLocal() (meaning "not routable on internet") is true with: + // - IPV4 DHCP autoconfigured address 169.254.x.x + // (false for any other including 192.168./16 and 10./24 since NAT may be in the equation) + // - IPV6 link-local addresses (fe80::/64) + +// for (bool configured = false; !configured;) { +// for (auto addr : addrList) +// if ((configured = !addr.isLocal() +// // && addr.isV6() // uncomment when IPv6 is mandatory +// // && addr.ifnumber() == STATION_IF +// )) { +// break; +// } +// Serial.print('.'); +// delay(500); +// } + WiFiManager wm; + + wm.setSaveConfigCallback(saveConfigCallback); + + // Setup custom parameters +// WiFiManagerParameter custom_mqtt_server("server", "Domoticz MQTT server hostname", cfg_mqtt_server, 40); +// WiFiManagerParameter custom_mqtt_port("port", "Domoticz MQTT port", cfg_mqtt_port, 6); + WiFiManagerParameter custom_idx_windir("idx1", "Domoticz Index for Wind Virtual Module", idx_windir, 4); + WiFiManagerParameter custom_idx_temp("idx2", "Domoticz Index for Temp/Hum/Baro Virtual Module", idx_temp, 4); + WiFiManagerParameter custom_idx_rain("idx3", "Domoticz Index for Rain Virtual Module", idx_rain, 4); + + // Add all parameters +// wm.addParameter(&custom_mqtt_server); +// wm.addParameter(&custom_mqtt_port); + wm.addParameter(&custom_idx_windir); + wm.addParameter(&custom_idx_temp); + wm.addParameter(&custom_idx_rain); + + // Automatic connect using saved stuff otherwise start as an AP to configure it +// if (!wm.autoConnect("ESP-Weather-Station","ESP")) { + if (!wm.autoConnect("ESP-Weather-Station")) { + Serial.println("Failer to connect and hit timeout"); + delay(3000); + // if we still have not connected restard and try again + ESP.restart(); + delay(5000); + } + + // Always start configportal for a little while (2 m) + wm.setConfigPortalTimeout(30); +// wm.startConfigPortal("ESP-Weather-Station","ESP"); + wm.startConfigPortal("ESP-Weather-Station"); + + // If we get here we are connected ! + Serial.println("Connected to WiFi !"); + + // Read the updated parameters +// strcpy(cfg_mqtt_server, custom_mqtt_server.getValue()); +// strcpy(cfg_mqtt_port, custom_mqtt_port.getValue()); + strcpy(idx_windir, custom_idx_windir.getValue()); + strcpy(idx_temp, custom_idx_temp.getValue()); + strcpy(idx_rain, custom_idx_rain.getValue()); + + // Save the custom parameters to FS + if (shouldSaveConfig) { + Serial.println("Saving config to FS..."); + DynamicJsonBuffer jsonBuffer; + + JsonObject& json = jsonBuffer.createObject(); +// json["mqtt_server"] = cfg_mqtt_server; +// json["mqtt_port"] = cfg_mqtt_port; + json["idx_windir"] = idx_windir; + json["idx_temp"] = idx_temp; + json["idx_rain"] = idx_rain; + + // json["ip"] = WiFi.localIP().toString(); + // json["gateway"] = WiFi.gatewayIP().toString(); + // json["subnet"] = WiFi.subnetMask().toString(); + + File configFile = SPIFFS.open("/config.json", "w"); + if (!configFile) { + Serial.println("failed to open config file for writing"); + } + + json.prettyPrintTo(Serial); + json.printTo(configFile); + configFile.close(); + //end save + shouldSaveConfig = false; + } + + // Launch OTA stuff + setup_ota(); + + //Serial.println(""); + //Serial.println(F("WiFi Connected !")); + #if LWIP_IPV6 + Serial.printf("IPV6 is enabled\n"); + #else + Serial.printf("IPV6 is not enabled\n"); + #endif + Serial.print("My IP address: "); + Serial.print(WiFi.localIP()); + Serial.print("/"); + Serial.print(WiFi.subnetMask()); + Serial.print(" GW:"); + Serial.println(WiFi.gatewayIP()); + + // Start the Software Serial for WS3 + WS3.begin(WS3_BAUD); + debugln("WS3 UART is ready..."); + + // Allocate memory for packet + pkt_str.reserve(WS3_PKT_LEN); + debugln(" -> Packet memory allocated!"); + + // Now connect to MQTT + client.setServer(mqtt_server, mqtt_port); + //client.setCallback(callback); + debugln("MQTT started"); + + // Initalize WinGust stuff + //loopcount = 0; + //WindGust = 0.0; +} + +#ifdef DONT_CHKSUM +// Validate packet using the checksum. +// Work only APRS data on this 51W3 board. +// Maybe the code on the board does not make the correct checksum ? +bool validate_packet(String pay, unsigned long chk) { + // Print the payload and the checksum we want + debugln("validate_packet:"); + debug(pay); + debug(" * "); + debugln(chk); + + // TEST DATA (actual packets) + // c000s000g000t075r000p019h43b09940*32 + // String pay = "c000s000g000t075r000p019h43b09940"; + // byte chk = 0x32; // this will be in HEX) + + // c000s000g000t075r000p019h42b09940*33 + // String pay = "c000s000g000t075r000p019h42b09940"; + // byte chk = 0x33; // this will be in HEX) + + // c000s000g000t075r000p019h42b09939*3D + // String pay = "c000s000g000t075r000p019h42b09939"; + // byte chk = 0x3D; // this will be in HEX) + + // SUPER grateful for the helpful https://toolslick.com/math/bitwise/xor-calculator to validate my + // code! + + // Current byte + byte i1=0; + + // the intermediate checksum + byte tmp = 0; + + // starting from the second character, we begin XORing + for (int x = 0; x < pay.length() ; x++) { + + i1=pay[x]; + + // Do the xOR + tmp = tmp^i1; + + } + + // do the check + if(tmp == chk){ + return true; + } else { + debugln("INVALID!"); + debug("calculated:"); + debugln(tmp); + return false; + } +} +#endif /* DONT_CHKSUM */ + + +// Parse the packet and fill the structure with data +void parse_packet(String payload, WS3Packet* p) { + + // E.G.: A4095 B000 C0000 D0000 E0000 F0000 G0000 H0000 I0000 J0000 K0000 L0237 M502 N09810 O..... + + // Parse in order, starting with A0000 (wind dir real time, 0-4096) + int wind_dir_idx = payload.indexOf('A'); + p->wind_dir = payload.substring(wind_dir_idx+1, wind_dir_idx+5).toInt(); + + // Then move on to B000 - wind direction angle (16 direction) + int wind_angle_idx = payload.indexOf('B'); + p->wind_angle = payload.substring(wind_angle_idx+1, wind_angle_idx+4).toInt(); + + // Then move on to C0000 - wind speed frequency (1 Hz) + int wind_freq_idx = payload.indexOf('C'); + p->wind_freq = payload.substring(wind_freq_idx+1, wind_freq_idx+5).toInt(); + + // Then move on to D0000 - wind speed real time (unit 0.1 m/s) + int wind_speed_idx = payload.indexOf('D'); + p->wind_speed = payload.substring(wind_speed_idx+1, wind_speed_idx+5).toInt(); + + // Then move on to E0000 - wind speed avg in the last minute (unit 0.1 m/s) + int wind_speed_1m_idx = payload.indexOf('D'); + p->wind_speed_1m = payload.substring(wind_speed_1m_idx+1, wind_speed_1m_idx+5).toInt(); + + // Then move on to F0000 - wind speed over the last 5 min + int wind_speed_5_idx = payload.indexOf('F'); + p->wind_speed_5m = payload.substring(wind_speed_5_idx+1, wind_speed_5_idx+5).toInt(); + + // Then move on to G0000 - Rain in Realtime (0-9999 counter) bucket + int rain_bucket_cnt_idx = payload.indexOf('G'); + p->rain_bucket_cnt = payload.substring(rain_bucket_cnt_idx+1, rain_bucket_cnt_idx+5).toInt(); + + // Then move on to H0000 - Rain bucket in the last 1 minute (0-9999 counter) + int rt_rain_bucket_idx = payload.indexOf('H'); + p->rt_rain_bucket = payload.substring(rt_rain_bucket_idx+1, rt_rain_bucket_idx+5).toInt(); + + // Then move on to I0000 - rain last minute (0.1mm) + int rain1m_idx = payload.indexOf('I'); + p->rain_1m = payload.substring(rain1m_idx+1, rain1m_idx+5).toInt() *.1; + + // Then move on to J0000 - rain last hour (0.1mm) + int rain1h_idx = payload.indexOf('J'); + p->rain_1h = payload.substring(rain1h_idx+1, rain1h_idx+5).toInt() *.1; + + // Then move on to K0000 - rain last 24h (0.1mm) + int rain24h_idx = payload.indexOf('K'); + p->rain_24h = payload.substring(rain24h_idx+1, rain24h_idx+5).toInt()*.1; + + // Then move on to L0200 - temp (0.1°C) + // XXX: Check with minus zero temperatures + int temp_idx = payload.indexOf('L'); + p->temp_f = payload.substring(temp_idx+1, temp_idx+5).toInt()*.1; + + // Then move on to M611 - Humidity + int humidity_idx = payload.indexOf('M'); +// p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt()*.1; + p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt(); + + // Then move on to N10020 - air pressure + int pressure_idx = payload.indexOf('N'); + p->air_pressure = payload.substring(pressure_idx+1, pressure_idx+6).toInt()*.1; + + // Handle Gust + debug(" B loopcount :"); + debugln(loopcount); + if(p->wind_speed > WindGust) { + WindGust = p->wind_speed; + debugln("Update WindGust"); + } + if(loopcount >= 300) { + loopcount = 0; + WindGust = p->wind_speed; + debugln("10 min expired -> reset counter"); + } + + + debug("WindGust :"); + debug(WindGust); + debug(" loopcount :"); + debugln(loopcount); + //loopcount++; + loopcount = loopcount + 1; + debug(" A loopcount :"); + debugln(loopcount); +} + +// Clear the packet before working on the next +void clear_pkt(WS3Packet* p) { + p->wind_dir = 0; + p->wind_angle = 0; + p->wind_freq = 0; + p->wind_speed = 0; + p->wind_speed_1m = 0; + p->wind_speed_5m = 0; + p->rain_bucket_cnt = 0; + p->rt_rain_bucket = 0; + p->rain_1m = 0; + p->rain_1h = 0; + p->rain_24h = 0; + p->temp_f = 0; + p->humidity = 0; + p->air_pressure = 0; +} + +#ifdef DEBUG +// Print the data +void print_weather(WS3Packet* p){ + Serial.print("Wind Direction (realtime): "); + Serial.println(p->wind_dir, DEC); + //Serial.println(" degrees"); + Serial.print("Wind direction angle : "); + Serial.print(p->wind_angle, DEC); + Serial.print(" degree "); + Serial.println(str_windir(p->wind_angle)); + Serial.print("Wind speed Frequency: "); + Serial.print(p->wind_freq, DEC); + Serial.println(" Hz"); + Serial.print("Wind speed: "); + Serial.print(p->wind_speed/10, DEC); + Serial.println(" m/s"); + Serial.print("Wind speed 1m: "); + Serial.print(p->wind_speed_1m/10, DEC); + Serial.println(" m/s"); + Serial.print("Wind speed 5m: "); + Serial.print(p->wind_speed_5m/10, DEC); + Serial.println(" m/s"); + Serial.print("temp_f: "); + Serial.print(p->temp_f, DEC); + Serial.println(" deg. C."); + + Serial.print("Rain buckets / buckets 1m: "); + Serial.print(p->rain_bucket_cnt, DEC); + Serial.print(" / "); + Serial.println(p->rt_rain_bucket, DEC); + + Serial.print("Rain 1m / 1H / 24H: "); + Serial.print(p->rain_1m, DEC); + Serial.print(" / "); + Serial.print(p->rain_1h, DEC); + Serial.print(" / "); + Serial.print(p->rain_24h, DEC); + Serial.println(" mm"); + + Serial.print("humidity: "); + Serial.print(p->humidity, DEC); + Serial.println(" %"); + + Serial.print("air_pressure: "); + Serial.print(p->air_pressure, DEC); + Serial.println(" hpa"); +} +#endif /* DEBUG */ + + +// Print the data over MQTT +void push_weather(WS3Packet* p) { + String MQPayload; + byte humidity_status = 0; // Domoticz humdity status + byte bar_forecast = 0; // Domoticz baro forcast + // Rain is mm x 100 -> Have to compute this. +// MQPayload = "{ \"idx\" : "+ String(domoticz_rain) +",\"nvalue\" : 0, \"svalue\" : \"" + String(p->rain_1m) + ";" + String(p->rain_1m) + "\"}"; + MQPayload = "{ \"idx\" : "+ String(idx_rain) +",\"nvalue\" : 0, \"svalue\" :\"" + String(p->rain_1h) + ";" + String(p->rain_1m) + "\"}"; + sendMQTTPayload(MQPayload); + // Temperature / Humidity / Baro + + // Humidity stuff + // O = Normal + // 1 = Confortable + // 2 = Dry (<35%) + // 3 = Wet (>70%) + if (p->humidity < 35) { + humidity_status = 2; // Dry + } else if (p->humidity >= 70) { + humidity_status = 3; // Wet + } else { + if ((p->humidity >=35) && (p->humidity <50)) { + humidity_status = 1; // Confortable + } else { + // >=45 -> 70% + humidity_status = 0; // Normal + } + } + // TODO : prediction + // 0 : No info + // 1 : Sunny + // 2 : Partly cloudy + // 3 : Cloudy + // 4 : Rain + // Maybe have to store values for last 3 hours? + // XXX: fix value with altitude + if (p->air_pressure < 1000) { + bar_forecast = 4; // Rain + } else if (p->air_pressure < 1020) { + bar_forecast = 3; // Cloudy + } else if (p->air_pressure < 1030) { + bar_forecast = 2; // Partly cloudy + } else { + bar_forecast = 1; // Sunny + } + // See : https://github.com/G6EJD/ESP32-Weather-Forecaster + //SLpressure_mB = (((p->air_pressure)/pow((1-((float)(ELEVATION))/44330), 5.255))/100.0); + //SLpressure_mB = p->air_pressure; +// MQPayload = "{ \"idx\" : "+ String(domoticz_temp) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->temp_f) + ";" + String(p->humidity) + ";0;" + String(SLpressure_mB) +";0\"}"; +// MQPayload = "{ \"idx\" : "+ String(idx_temp) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->temp_f) + ";" + String(p->humidity) + ";"+String(humidity_status)+ ";" + String(p->air_pressure) +";0\"}"; + MQPayload = "{ \"idx\" : "+ String(idx_temp) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->temp_f) + ";" + String(p->humidity) + ";"+String(humidity_status)+ ";" + String(p->air_pressure) +";"+String(bar_forecast)+"\"}"; + + sendMQTTPayload(MQPayload); + // Wind +// MQPayload = "{ \"idx\" : "+ String(domoticz_windir) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->wind_angle) + ";" + String(str_windir(p->wind_angle)) + ";" + String(p->wind_speed*10) + ";" + String(WindGust*10) +";" + String(p->temp_f) + ";"+String(p->temp_f)+"\"}"; + MQPayload = "{ \"idx\" : "+ String(idx_windir) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->wind_angle) + ";" + String(str_windir(p->wind_angle)) + ";" + String(p->wind_speed) + ";" + String(WindGust) +";" + String(p->temp_f) + ";"+String(p->temp_f)+"\"}"; + sendMQTTPayload(MQPayload); +} + + +// Processing the packet. +bool process_packet(String pkt, WS3Packet* p) { + debugln("[D] process_packet - ALive!"); + debugln(pkt); + + // Allocate bytes for the payload + String payload; + payload.reserve(WS3_PKT_LEN-WS3_CHK_LEN); + + #ifdef DONT_CHKSUM + // everything after the * is checksum (2 char long) + unsigned long chksum; + #endif /* DONT_CHKSUM */ + + // Check if the 75rd character is * + if (pkt.charAt(CHK_SUM_DELINEATOR) != '*') { + debugln("Packed invalid; no * character at position 75!"); + return false; + #ifdef DONT_CHKSUM + } else { + // The character indicating the checksum is coming is in the correct place. Yay. + // Now, we need to pull the two ascii characters that are transmitted to us + // and turn them into a single byte. E.G. Char 3, Char D should convert to 0x3D. + // + // We can do this with the strtoul() function; we indicate that we wante base 16 + + chksum = strtoul(pkt.substring(CHK_SUM_DELINEATOR+1, CHK_SUM_DELINEATOR+2).c_str(),NULL,16); + } + #endif /* DONT_CHKSUM */ + + // We have the checksum, Now we can bother to get the payload + payload = pkt.substring(0, CHK_SUM_DELINEATOR); + + // And try to validate... + #ifndef DONT_CHKSUM + if(!validate_packet(payload, chksum)){ + debugln("invalid packet! :("); + return false; + } else { + debugln("Valid packet!"); + } + #endif /* DONT_CHKSUM */ + parse_packet(payload, p); + return true; +} + +void loop() { + // MQTT + if (!client.connected()) { + reconnect(); + } + // While data comes in and we don't have a pending packet to process... + while (WS3.available() && pkt_ok !=true) { + + // Pull the bytes off the stream + char inChar = (char)WS3.read(); + + // And build up the packet + pkt_str += inChar; + + // Until we hit the end + if (inChar == '\n') { + pkt_ok = true; + } + } + + // Yay, we now have a packet! + // Now, we attempt to parse out the string into a packet that we can work with + if (pkt_ok) { + debugln("pkt_ok!"); + + // At this point, we have a string of characters that was probably a valid packet + // We set get some memory and attempt to parse the string into the struct + WS3Packet p = {}; + + // Validate the payload, then parse it. + if (process_packet(pkt_str, &p)) { + // print results if parse OK + #ifdef DEBUG + print_weather(&p); + #endif /* DEBUG */ + // Push to MQTT + push_weather(&p); + debugln("processed"); + } else { + debugln("unable to parse packet :("); + } + + + // clear so we can start again + pkt_str = ""; + pkt_ok = false; + clear_pkt(&p); + } + ArduinoOTA.handle(); +} + +// MQTT Stuff +void callback(char* topic, byte* payload, unsigned int length) { + debug("Message arrived ["); + Serial.print(topic); + debug("] "); + for (int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + debug(" "); +} +void reconnect() { + // Loop until we're reconnected + while (!client.connected()) { + debug("Attempting MQTT connection..."); + // Attempt to connect + if (client.connect(Mqtt_clientid)) { + debugln("connected"); + // ... and resubscribe + client.subscribe(dom_in); + } else { + debug("failed, rc="); + debug(client.state()); + debugln(" try again in 5 seconds"); + // Wait 5 seconds before retrying + for(int i = 0; i<5000; i++){ + delay(1); + } + } + } +} + +// Sends MQTT payload to the Mosquitto server running on a Raspberry Pi. +// Mosquitto server deliveres data to Domoticz server running on a same Raspberry Pi +void sendMQTTPayload(String msgpayload) +{ + // Convert payload to char array + msgpayload.toCharArray(msgToPublish, msgpayload.length()+1); + + //Publish payload to MQTT broker + if (client.publish(dom_in, msgToPublish)) + { + debug("Following data published to MQTT broker: "); + debug(dom_in); + debug(" "); + debugln(msgpayload); + } + else + { + debug("Publishing to MQTT broker failed... "); + debugln(client.state()); + } +} + +// WiFiManager & Autoconfig +//callback notifying us of the need to save config +void saveConfigCallback () { + Serial.println("Should save config"); + shouldSaveConfig = true; +} + +void setupSpiffs(){ + //clean FS, for testing + // SPIFFS.format(); + + //read configuration from FS json + debugln("mounting FS..."); + + if (SPIFFS.begin()) { + debugln("mounted file system"); + if (SPIFFS.exists("/config.json")) { + //file exists, reading and loading + debugln("reading config file"); + File configFile = SPIFFS.open("/config.json", "r"); + if (configFile) { + debugln("opened config file"); + size_t size = configFile.size(); + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + DynamicJsonBuffer jsonBuffer; + JsonObject& json = jsonBuffer.parseObject(buf.get()); + json.printTo(Serial); + if (json.success()) { + debugln("\nparsed json"); + + //strcpy(cfg_mqtt_server, json["mqtt_server"]); + //strcpy(cfg_mqtt_port, json["mqtt_port"]); + strcpy(idx_windir, json["idx_windir"]); + strcpy(idx_temp, json["idx_temp"]); + strcpy(idx_rain, json["idex_rain"]); + + // if(json["ip"]) { + // Serial.println("setting custom ip from config"); + // strcpy(static_ip, json["ip"]); + // strcpy(static_gw, json["gateway"]); + // strcpy(static_sn, json["subnet"]); + // Serial.println(static_ip); + // } else { + // Serial.println("no custom ip in config"); + // } + + } else { + Serial.println("failed to load json config"); + } + } + } + } else { + Serial.println("failed to mount FS"); + } + //end read +} diff --git a/WS3/Serial-ESP/Serial-ESP.ino_WORK b/WS3/Serial-ESP/Serial-ESP.ino_WORK new file mode 100644 index 0000000..d1a6a0f --- /dev/null +++ b/WS3/Serial-ESP/Serial-ESP.ino_WORK @@ -0,0 +1,588 @@ +// Weather Station 3 from AliExpress / TaoBao +// +// Highly inspired by https://github.com/kquinsland/ws3-to-esphome-bridge/ +// +// Maybe the code is not optimal but it works. +// +// Configuration of WS3 : +// +// JP1 toggle betwen ARS inch mode and professional metric mode +// short : inch +// open : metric +// +// JP2 baud rate +// short : 2400 +// open : 9600 +// +// Configuration used for this code : +// JP1 : open +// JP2 : closed +// +// Data is in the form : +// +// A4095B000C0000D0000E0000F0000G0000H0000I0000J0000K0000L0218M515N09654O.....*52 +// +// +// Serial connection used there (hardcoded using AltSoftSerial) : +// Only TX has to be connected, RX is not used by WS3. +// +// Board Receive +// ----- ------- +// Teensy 3.0 & 3.1 20 +// Teensy 2.0 10 +// Teensy++ 2.0 4 +// Arduino Uno 8 +// EtherTen 8 +// Arduino Leonardo 13 +// Arduino Mega 48 +// Wiring-S 6 +// Sanguino 14 +// +// This code was tested and developped on EtherTen board which +// is Arduino Uno compatible with some additions. +// +// Use following libraries: +// - AltSoftSerial +// - Adafruit_MQTT +// +#include +#include +// EspSoftwareSerial (or plain SoftwareSerial) +#include +// MQTT +#include + +// Domoticz MQTT configuration +const char* mqtt_server = "portbuild.home.oav.net"; +#define mqtt_port 1883 +// Domoticz Indexes (virtual devices has to be created) +#define domoticz_windir 48 // Wind Virtual device +#define domoticz_temp 52 // Temp / Him / Baro virtual device +#define domoticz_rain 51 // Rain Virtual device +#define ELEVATION 323 // Elevation from the place we are + // Longwy is at 323m + +// Definitions +const char* Mqtt_clientid = "ESP-Weather-Station"; +const char* dom_in = "domoticz/in"; +#define MQTT_MAX_PACKET_SIZE 128 +char msgToPublish[MQTT_MAX_PACKET_SIZE + 1]; + +// Wifi +const char* ssid = "Kiwi"; +const char* password = "ZeKiwi127"; + +// Defines +#define DEBUG 1 + +#ifdef DEBUG + #define debug(x) Serial.print(x) + #define debugln(x) Serial.println(x) +#else /* DEBUG */ + #define debug(x) + #define debugln(x) +#endif /* DEBUG */ +// Baud used to read see JP2 +#define WS3_BAUD 2400 +// Toggle support for PM2.5 sensor +//#define SUPPORT_PM25_SENSOR + +// Define the length of data +#ifdef SUPPORT_PM25_SENSOR + // There is 88 bytes per packets + #define WS3_PKT_LEN 78 + // And the checksum is the last 2 bytes + #define WS3_CHK_LEN 2 + #define CHK_SUM_DELINEATOR 75 +#else /* SUPPORT_PM25_SENSOR */ + // There is 88 bytes per packets + #define WS3_PKT_LEN 78 + // And the checksum is the last 2 bytes + #define WS3_CHK_LEN 2 + #define CHK_SUM_DELINEATOR 75 +#endif /* SUPPRT_PM25_SENSOR */ + +// Seems the Metric format does not have a correct checksum +// In this case we should not test the checksum, just see +// if we have a correct dataline +#define DONT_CHKSUM 1 + +// Place holder for the packet received +String pkt_str = ""; + +// Flag for packet OK +volatile byte pkt_ok = false; + +SoftwareSerial WS3(15,16); +WiFiClient espClient; +PubSubClient client(espClient); + +// After parsing the string of bytes, we'll have an easier to use struct +// TODO: this should be it's own file? +struct WS3Packet { + + // The 1st field is "A0000" - Wind direction AD value in real time (0-4096) + unsigned int wind_dir; + // The 2nd field is "B000" - Wind direct angle value (16 direction) + unsigned int wind_angle; // new + // The 3rd field is "C0000" - Real time wind speed frequency 1Hz + unsigned int wind_freq; // New + // The 4th field is "D0000" - Real time wind speed, unit: 0.1 m/s + float wind_speed; + // The 5th field is "E0000" - Avg wind speed in the previous minute, unit: 0.1m/s + float wind_speed_1m; + // The 6th field is "F0000" - the highest wind speed in the last 5 minutes, unit: 0.1m/s + float wind_speed_5m; + // The 7th field is "G0000" - Real time rain bucket (0-9999), loop-count + int rain_bucket_cnt; // New + // The 8th field is "H0000" - Number of rain bucket in the last minute, (0-9999) + int rt_rain_bucket; + // The 9th field is "I0000" - Rain fall in 1 minute, unit: 0.1mm + float rain_1m; + // The 10th field is "J0000" - the previous hour's rainfall ( 0.1 mm) + float rain_1h; + // The 11th field is "K0000" - rainfall during the first 24 hours ( 0.1 mm) + float rain_24h; + // The 12th field is "L0000" - temperature, unit: degree C (unit 0.1 Degree) + float temp_f; + // The 13th field is "M000" - humidity ( 00 % - 99 %), unit 0.1% + float humidity; + // The 14th field is "M10020" - air pressure ( 0.1 hpa ) + float air_pressure; +}; + +// Return the index according to Wind Angle +String str_windir(unsigned int WinVal){ + //debug("str_windir() : "); + //debugln(WinVal); + if(WinVal >= 360) return "N"; //N + if(WinVal >= 0 && WinVal < 22) return "N"; //N + if(WinVal >= 22 && WinVal < 45) return "NNE"; //NNE + if(WinVal >= 45 && WinVal < 67) return "NE"; //NE + if(WinVal >= 67 && WinVal < 90) return "ENE"; //ENE + if(WinVal >= 90 && WinVal < 112) return "E"; //E + if(WinVal >= 112 && WinVal < 135) return "ESE"; //ESE + if(WinVal >= 135 && WinVal < 157) return "SE"; //SE + if(WinVal >= 157 && WinVal < 180) return "S"; //S + if(WinVal >= 180 && WinVal < 202) return "S"; //S + if(WinVal >= 202 && WinVal < 225) return "SSW"; //SSW + if(WinVal >= 225 && WinVal < 247) return "SW"; //SW + if(WinVal >= 247 && WinVal < 270) return "WSW"; //WSW + if(WinVal >= 270 && WinVal < 292) return "W"; //W + if(WinVal >= 292 && WinVal < 315) return "WNW"; //WNW + if(WinVal >= 315 && WinVal < 337) return "NW"; //NW + if(WinVal >= 337 && WinVal < 359) return "NNW"; //NNW +} + +// Setup the stuff. +void setup() { + //Serial.begin(460800); + Serial.begin(57600); + while (!Serial) ; // wait for Arduino Serial Monitor to open + Serial.println(""); + Serial.println("Weather Station 3 Adapter by Kiwi"); + Serial.println(ESP.getFullVersion()); + + WiFi.hostname("ESP-Weather-Station"); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + // Use this loop instead to wait for an IPv6 routable address + + // addr->isLocal() (meaning "not routable on internet") is true with: + // - IPV4 DHCP autoconfigured address 169.254.x.x + // (false for any other including 192.168./16 and 10./24 since NAT may be in the equation) + // - IPV6 link-local addresses (fe80::/64) + + for (bool configured = false; !configured;) { + for (auto addr : addrList) + if ((configured = !addr.isLocal() + // && addr.isV6() // uncomment when IPv6 is mandatory + // && addr.ifnumber() == STATION_IF + )) { + break; + } + Serial.print('.'); + delay(500); + } + Serial.println(""); + Serial.println(F("WiFi Connected !")); + #if LWIP_IPV6 + Serial.printf("IPV6 is enabled\n"); + #else + Serial.printf("IPV6 is not enabled\n"); + #endif + Serial.print("My IP address: "); + Serial.println(WiFi.localIP()); + // TODO: Find the IPv6 given to the ESP ? + + + // Start the Software Serial for WS3 + WS3.begin(WS3_BAUD); + debugln("WS3 UART is ready..."); + + // Allocate memory for packet + pkt_str.reserve(WS3_PKT_LEN); + debugln(" -> Packet memory allocated!"); + + // Now connect to MQTT + client.setServer(mqtt_server, mqtt_port); + //client.setCallback(callback); + debugln("MQTT started"); +} + +#ifdef DONT_CHKSUM +// Validate packet using the checksum. +// Work only APRS data on this 51W3 board. +// Maybe the code on the board does not make the correct checksum ? +bool validate_packet(String pay, unsigned long chk) { + // Print the payload and the checksum we want + debugln("validate_packet:"); + debug(pay); + debug(" * "); + debugln(chk); + + // TEST DATA (actual packets) + // c000s000g000t075r000p019h43b09940*32 + // String pay = "c000s000g000t075r000p019h43b09940"; + // byte chk = 0x32; // this will be in HEX) + + // c000s000g000t075r000p019h42b09940*33 + // String pay = "c000s000g000t075r000p019h42b09940"; + // byte chk = 0x33; // this will be in HEX) + + // c000s000g000t075r000p019h42b09939*3D + // String pay = "c000s000g000t075r000p019h42b09939"; + // byte chk = 0x3D; // this will be in HEX) + + // SUPER grateful for the helpful https://toolslick.com/math/bitwise/xor-calculator to validate my + // code! + + // Current byte + byte i1=0; + + // the intermediate checksum + byte tmp = 0; + + // starting from the second character, we begin XORing + for (int x = 0; x < pay.length() ; x++) { + + i1=pay[x]; + + // Do the xOR + tmp = tmp^i1; + + } + + // do the check + if(tmp == chk){ + return true; + } else { + debugln("INVALID!"); + debug("calculated:"); + debugln(tmp); + return false; + } +} +#endif /* DONT_CHKSUM */ + + +// Parse the packet and fill the structure with data +void parse_packet(String payload, WS3Packet* p) { + + // E.G.: A4095 B000 C0000 D0000 E0000 F0000 G0000 H0000 I0000 J0000 K0000 L0237 M502 N09810 O..... + + // Parse in order, starting with A0000 (wind dir real time, 0-4096) + int wind_dir_idx = payload.indexOf('A'); + p->wind_dir = payload.substring(wind_dir_idx+1, wind_dir_idx+5).toInt(); + + // Then move on to B000 - wind direction angle (16 direction) + int wind_angle_idx = payload.indexOf('B'); + p->wind_angle = payload.substring(wind_angle_idx+1, wind_angle_idx+4).toInt(); + + // Then move on to C0000 - wind speed frequency (1 Hz) + int wind_freq_idx = payload.indexOf('C'); + p->wind_freq = payload.substring(wind_freq_idx+1, wind_freq_idx+5).toInt(); + + // Then move on to D0000 - wind speed real time (unit 0.1 m/s) + int wind_speed_idx = payload.indexOf('D'); + p->wind_speed = payload.substring(wind_speed_idx+1, wind_speed_idx+5).toInt() / 10; + + // Then move on to E0000 - wind speed avg in the last minute (unit 0.1 m/s) + int wind_speed_1m_idx = payload.indexOf('D'); + p->wind_speed_1m = payload.substring(wind_speed_1m_idx+1, wind_speed_1m_idx+5).toInt() / 10; + + // Then move on to F0000 - wind speed over the last 5 min + int wind_speed_5_idx = payload.indexOf('F'); + p->wind_speed_5m = payload.substring(wind_speed_5_idx+1, wind_speed_5_idx+5).toInt() / 10; + + // Then move on to G0000 - Rain in Realtime (0-9999 counter) bucket + int rain_bucket_cnt_idx = payload.indexOf('G'); + p->rain_bucket_cnt = payload.substring(rain_bucket_cnt_idx+1, rain_bucket_cnt_idx+5).toInt(); + + // Then move on to H0000 - Rain bucket in the last 1 minute (0-9999 counter) + int rt_rain_bucket_idx = payload.indexOf('H'); + p->rt_rain_bucket = payload.substring(rt_rain_bucket_idx+1, rt_rain_bucket_idx+5).toInt(); + + // Then move on to I0000 - rain last minute (0.1mm) + int rain1m_idx = payload.indexOf('I'); + p->rain_1m = payload.substring(rain1m_idx+1, rain1m_idx+5).toInt() *.1; + + // Then move on to J0000 - rain last hour (0.1mm) + int rain1h_idx = payload.indexOf('J'); + p->rain_1h = payload.substring(rain1h_idx+1, rain1h_idx+5).toInt() *.1; + + // Then move on to K0000 - rain last 24h (0.1mm) + int rain24h_idx = payload.indexOf('K'); + p->rain_24h = payload.substring(rain24h_idx+1, rain24h_idx+5).toInt()*.1; + + // Then move on to L0200 - temp (0.1°C) + // XXX: Check with minus zero temperatures + int temp_idx = payload.indexOf('L'); + p->temp_f = payload.substring(temp_idx+1, temp_idx+5).toInt()*.1; + + // Then move on to M611 - Humidity + int humidity_idx = payload.indexOf('M'); +// p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt()*.1; + p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt(); + + // Then move on to N10020 - air pressure + int pressure_idx = payload.indexOf('N'); + p->air_pressure = payload.substring(pressure_idx+1, pressure_idx+6).toInt()*.1; + +} + +// Clear the packet before working on the next +void clear_pkt(WS3Packet* p) { + p->wind_dir = 0; + p->wind_angle = 0; + p->wind_freq = 0; + p->wind_speed = 0; + p->wind_speed_1m = 0; + p->wind_speed_5m = 0; + p->rain_bucket_cnt = 0; + p->rt_rain_bucket = 0; + p->rain_1m = 0; + p->rain_1h = 0; + p->rain_24h = 0; + p->temp_f = 0; + p->humidity = 0; + p->air_pressure = 0; +} + +#ifdef DEBUG +// Print the data +void print_weather(WS3Packet* p){ + Serial.print("Wind Direction (realtime): "); + Serial.println(p->wind_dir, DEC); + //Serial.println(" degrees"); + Serial.print("Wind direction angle : "); + Serial.print(p->wind_angle, DEC); + Serial.print(" degree "); + Serial.println(str_windir(p->wind_angle)); + Serial.print("Wind speed Frequency: "); + Serial.print(p->wind_freq, DEC); + Serial.println(" Hz"); + Serial.print("Wind speed: "); + Serial.print(p->wind_speed, DEC); + Serial.println(" m/s"); + Serial.print("Wind speed 1m: "); + Serial.print(p->wind_speed_1m, DEC); + Serial.println(" m/s"); + Serial.print("Wind speed 5m: "); + Serial.print(p->wind_speed_5m, DEC); + Serial.println(" m/s"); + Serial.print("temp_f: "); + Serial.print(p->temp_f, DEC); + Serial.println(" deg. C."); + + Serial.print("Rain buckets / buckets 1m: "); + Serial.print(p->rain_bucket_cnt, DEC); + Serial.print(" / "); + Serial.println(p->rt_rain_bucket, DEC); + + Serial.print("Rain 1m / 1H / 24H: "); + Serial.print(p->rain_1m, DEC); + Serial.print(" / "); + Serial.print(p->rain_1h, DEC); + Serial.print(" / "); + Serial.print(p->rain_24h, DEC); + Serial.println(" mm"); + + Serial.print("humidity: "); + Serial.print(p->humidity, DEC); + Serial.println(" %"); + + Serial.print("air_pressure: "); + Serial.print(p->air_pressure, DEC); + Serial.println(" hpa"); +} +#endif /* DEBUG */ + +// Print the data over MQTT +void push_weather(WS3Packet* p) { + String MQPayload; + float SLpressure_mB; + float WindGust = 0.0; // Gust is actually set to 0.0 + // Have to find a nice way to compute this. + // Rain + MQPayload = "{ \"idx\" : "+ String(domoticz_rain) +",\"nvalue\" : 0, \"svalue\" : \"" + String(p->rain_1m) + ";" + String(p->rain_1m) + "\"}"; + sendMQTTPayload(MQPayload); + // Temperature / Humidity / Baro + //SLpressure_mB = (((p->air_pressure)/pow((1-((float)(ELEVATION))/44330), 5.255))/100.0); + SLpressure_mB = p->air_pressure; + MQPayload = "{ \"idx\" : "+ String(domoticz_temp) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->temp_f) + ";" + String(p->humidity) + ";0;" + String(SLpressure_mB) +";0\"}"; + sendMQTTPayload(MQPayload); + // Wind + MQPayload = "{ \"idx\" : "+ String(domoticz_windir) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->wind_angle) + ";" + String(str_windir(p->wind_angle)) + ";" + String(p->wind_speed*10) + ";" + String(WindGust*10) +";" + String(p->temp_f) + ";"+String(p->temp_f)+"\"}"; + sendMQTTPayload(MQPayload); +} + + +// Processing the packet. +bool process_packet(String pkt, WS3Packet* p) { + debugln("[D] process_packet - ALive!"); + debugln(pkt); + + // Allocate bytes for the payload + String payload; + payload.reserve(WS3_PKT_LEN-WS3_CHK_LEN); + + #ifdef DONT_CHKSUM + // everything after the * is checksum (2 char long) + unsigned long chksum; + #endif /* DONT_CHKSUM */ + + // Check if the 75rd character is * + if (pkt.charAt(CHK_SUM_DELINEATOR) != '*') { + debugln("Packed invalid; no * character at position 75!"); + return false; + #ifdef DONT_CHKSUM + } else { + // The character indicating the checksum is coming is in the correct place. Yay. + // Now, we need to pull the two ascii characters that are transmitted to us + // and turn them into a single byte. E.G. Char 3, Char D should convert to 0x3D. + // + // We can do this with the strtoul() function; we indicate that we wante base 16 + + chksum = strtoul(pkt.substring(CHK_SUM_DELINEATOR+1, CHK_SUM_DELINEATOR+2).c_str(),NULL,16); + } + #endif /* DONT_CHKSUM */ + + // We have the checksum, Now we can bother to get the payload + payload = pkt.substring(0, CHK_SUM_DELINEATOR); + + // And try to validate... + #ifndef DONT_CHKSUM + if(!validate_packet(payload, chksum)){ + debugln("invalid packet! :("); + return false; + } else { + debugln("Valid packet!"); + } + #endif /* DONT_CHKSUM */ + parse_packet(payload, p); + return true; +} + +void loop() { + // MQTT + if (!client.connected()) { + reconnect(); + } + // While data comes in and we don't have a pending packet to process... + while (WS3.available() && pkt_ok !=true) { + + // Pull the bytes off the stream + char inChar = (char)WS3.read(); + + // And build up the packet + pkt_str += inChar; + + // Until we hit the end + if (inChar == '\n') { + pkt_ok = true; + } + } + + // Yay, we now have a packet! + // Now, we attempt to parse out the string into a packet that we can work with + if (pkt_ok) { + debugln("pkt_ok!"); + + // At this point, we have a string of characters that was probably a valid packet + // We set get some memory and attempt to parse the string into the struct + WS3Packet p = {}; + + // Validate the payload, then parse it. + if (process_packet(pkt_str, &p)) { + // print results if parse OK + #ifdef DEBUG + print_weather(&p); + #endif /* DEBUG */ + push_weather(&p); + debugln("processed"); + } else { + debugln("unable to parse packet :("); + } + + + // clear so we can start again + pkt_str = ""; + pkt_ok = false; + clear_pkt(&p); + } +} + +// MQTT Stuff +void callback(char* topic, byte* payload, unsigned int length) { + debug("Message arrived ["); + Serial.print(topic); + debug("] "); + for (int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + debug(" "); +} +void reconnect() { + // Loop until we're reconnected + while (!client.connected()) { + debug("Attempting MQTT connection..."); + // Attempt to connect + if (client.connect(Mqtt_clientid)) { + debugln("connected"); + // ... and resubscribe + client.subscribe(dom_in); + } else { + debug("failed, rc="); + debug(client.state()); + debugln(" try again in 5 seconds"); + // Wait 5 seconds before retrying + for(int i = 0; i<5000; i++){ + delay(1); + } + } + } +} + +// Sends MQTT payload to the Mosquitto server running on a Raspberry Pi. +// Mosquitto server deliveres data to Domoticz server running on a same Raspberry Pi +void sendMQTTPayload(String msgpayload) +{ + // Convert payload to char array + msgpayload.toCharArray(msgToPublish, msgpayload.length()+1); + + //Publish payload to MQTT broker + if (client.publish(dom_in, msgToPublish)) + { + debug("Following data published to MQTT broker: "); + debug(dom_in); + debug(" "); + debugln(msgpayload); + } + else + { + debug("Publishing to MQTT broker failed... "); + debugln(client.state()); + } +} diff --git a/WS3/Serial/Serial.ino b/WS3/Serial/Serial.ino new file mode 100644 index 0000000..b9b909a --- /dev/null +++ b/WS3/Serial/Serial.ino @@ -0,0 +1,514 @@ +// Weather Station 3 from AliExpress / TaoBao +// +// Highly inspired by https://github.com/kquinsland/ws3-to-esphome-bridge/ +// +// Maybe the code is not optimal but it works. +// +// Configuration of WS3 : +// +// JP1 toggle betwen ARS inch mode and professional metric mode +// short : inch +// open : metric +// +// JP2 baud rate +// short : 2400 +// open : 9600 +// +// Configuration used for this code : +// JP1 : open +// JP2 : closed +// +// Data is in the form : +// +// A4095B000C0000D0000E0000F0000G0000H0000I0000J0000K0000L0218M515N09654O.....*52 +// +// +// Serial connection used there (hardcoded using AltSoftSerial) : +// Only TX has to be connected, RX is not used by WS3. +// +// Board Receive +// ----- ------- +// Teensy 3.0 & 3.1 20 +// Teensy 2.0 10 +// Teensy++ 2.0 4 +// Arduino Uno 8 +// EtherTen 8 +// Arduino Leonardo 13 +// Arduino Mega 48 +// Wiring-S 6 +// Sanguino 14 +// +// This code was tested and developped on EtherTen board which +// is Arduino Uno compatible with some additions. +// +// Use following libraries: +// - AltSoftSerial +// - Adafruit_MQTT +// + +// soft or alt +// AltSerial takes more global variables. +#define ALTSERIAL +#ifdef ALTSERIAL +#include +#else +#include +#endif +// Ethernet +#include +#include +// MQTT +#include + +// Define the mac address. +const byte mac[] PROGMEM = { + 0xDE, 0xAA, 0xBE, 0xEC, 0xAF, 0x02 +}; + +// Domoticz MQTT configuration +#define mqtt_server "192.168.20.253" +#define mqtt_port 1883 +// Domoticz Indexes (virtual devices has to be created) +#define domoticz_windir 160 +#define domoticz_temp 161 +#define domoticz_hum 162 +#define domoticz_rain 163 +#define domoticz_pres 164 +// Definitions +const char* Mqtt_clientid = "51WS3Wether"; +const char* dom_in = "domoticz/in"; + + +// Defines +#define DEBUG + +#ifdef DEBUG + #define debug(x) Serial.print(x) + #define debugln(x) Serial.println(x) +#else /* DEBUG */ + #define debug(x) + #define debugln(x) +#endif /* DEBUG */ +// Baud used to read see JP2 +#define WS3_BAUD 2400 +// Toggle support for PM2.5 sensor +//#define SUPPORT_PM25_SENSOR + +// Define the length of data +#ifdef SUPPORT_PM25_SENSOR + // There is 88 bytes per packets + #define WS3_PKT_LEN 78 + // And the checksum is the last 2 bytes + #define WS3_CHK_LEN 2 + #define CHK_SUM_DELINEATOR 75 +#else /* SUPPORT_PM25_SENSOR */ + // There is 88 bytes per packets + #define WS3_PKT_LEN 78 + // And the checksum is the last 2 bytes + #define WS3_CHK_LEN 2 + #define CHK_SUM_DELINEATOR 75 +#endif /* SUPPRT_PM25_SENSOR */ + +// Seems the Metric format does not have a correct checksum +// In this case we should not test the checksum, just see +// if we have a correct dataline +#define DONT_CHKSUM + +// Place holder for the packet received +String pkt_str = ""; + +// Flag for packet OK +bool pkt_ok = false; + +#ifdef ALTSERIAL +AltSoftSerial WS3; +#else +SoftwareSerial WS3(8,9); +#endif +EthernetClient ethClient; +//PubSubClient client(ethClient); + +// After parsing the string of bytes, we'll have an easier to use struct +// TODO: this should be it's own file? +struct WS3Packet { + + // The 1st field is "A0000" - Wind direction AD value in real time (0-4096) + int wind_dir; + // The 2nd field is "B000" - Wind direct angle value (16 direction) + int wind_angle; // new + // The 3rd field is "C0000" - Real time wind speed frequency 1Hz + int wind_freq; // New + // The 4th field is "D0000" - Real time wind speed, unit: 0.1 m/s + float wind_speed; + // The 5th field is "E0000" - Avg wind speed in the previous minute, unit: 0.1m/s + float wind_speed_1m; + // The 6th field is "F0000" - the highest wind speed in the last 5 minutes, unit: 0.1m/s + float wind_speed_5m; + // The 7th field is "G0000" - Real time rain bucket (0-9999), loop-count + int rain_bucket_cnt; // New + // The 8th field is "H0000" - Number of rain bucket in the last minute, (0-9999) + int rt_rain_bucket; + // The 9th field is "I0000" - Rain fall in 1 minute, unit: 0.1mm + float rain_1m; + // The 10th field is "J0000" - the previous hour's rainfall ( 0.1 mm) + float rain_1h; + // The 11th field is "K0000" - rainfall during the first 24 hours ( 0.1 mm) + float rain_24h; + // The 12th field is "L0000" - temperature, unit: degree C (unit 0.1 Degree) + float temp_f; + // The 13th field is "M000" - humidity ( 00 % - 99 %), unit 0.1% + float humidity; + // The 14th field is "M10020" - air pressure ( 0.1 hpa ) + float air_pressure; +}; + +// Setup the stuff. +void setup() { + // You can use Ethernet.init(pin) to configure the CS pin + //Ethernet.init(10); // Most Arduino shields + //Ethernet.init(5); // MKR ETH shield + //Ethernet.init(0); // Teensy 2.0 + //Ethernet.init(20); // Teensy++ 2.0 + //Ethernet.init(15); // ESP8266 with Adafruit Featherwing Ethernet + //Ethernet.init(33); // ESP32 with Adafruit Featherwing Ethernet + + Serial.begin(57600); + while (!Serial) ; // wait for Arduino Serial Monitor to open + Serial.println("Weather Station 3 Adapter by Kiwi"); + + // Wait for eternet to initialize + // start the Ethernet connection: + Serial.println("Initialize Ethernet with DHCP:"); + if (Ethernet.begin(mac) == 0) { + Serial.println("Failed to configure Ethernet using DHCP"); + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :("); + } else if (Ethernet.linkStatus() == LinkOFF) { + Serial.println("Ethernet cable is not connected."); + } + // no point in carrying on, so do nothing forevermore: + while (true) { + delay(1); + } + } + // print your local IP address: + Serial.print("My IP address: "); + Serial.println(Ethernet.localIP()); + + // Start the Software Serial for WS3 + WS3.begin(WS3_BAUD); + debugln("WS3 UART is ready..."); + + // Allocate memory for packet + pkt_str.reserve(WS3_PKT_LEN); + //payload.reserve(WS3_PKT_LEN-WS3_CHK_LEN); + debugln(" -> Packet memory allocated!"); +} + +#ifdef DONT_CHKSUM +// Validate packet using the checksum. +// Work only APRS data on this 51W3 board. +// Maybe the code on the board does not make the correct checksum ? +bool validate_packet(String pay, unsigned long chk) { + // Print the payload and the checksum we want + debugln("validate_packet:"); + debug(pay); + debug(" * "); + debugln(chk); + + // TEST DATA (actual packets) + // c000s000g000t075r000p019h43b09940*32 + // String pay = "c000s000g000t075r000p019h43b09940"; + // byte chk = 0x32; // this will be in HEX) + + // c000s000g000t075r000p019h42b09940*33 + // String pay = "c000s000g000t075r000p019h42b09940"; + // byte chk = 0x33; // this will be in HEX) + + // c000s000g000t075r000p019h42b09939*3D + // String pay = "c000s000g000t075r000p019h42b09939"; + // byte chk = 0x3D; // this will be in HEX) + + // SUPER grateful for the helpful https://toolslick.com/math/bitwise/xor-calculator to validate my + // code! + + // Current byte + byte i1=0; + + // the intermediate checksum + byte tmp = 0; + + // starting from the second character, we begin XORing + for (int x = 0; x < pay.length() ; x++) { + + i1=pay[x]; + + // Do the xOR + tmp = tmp^i1; + + } + + // do the check + if(tmp == chk){ + return true; + } else { + debugln("INVALID!"); + debug("calculated:"); + debugln(tmp); + return false; + } +} +#endif /* DONT_CHKSUM */ + + +// Parse the packet and fill the structure with data +void parse_packet(String payload, WS3Packet* p) { + + // E.G.: A4095 B000 C0000 D0000 E0000 F0000 G0000 H0000 I0000 J0000 K0000 L0237 M502 N09810 O..... + + // Parse in order, starting with A0000 (wind dir real time, 0-4096) + int wind_dir_idx = payload.indexOf('A'); + p->wind_dir = payload.substring(wind_dir_idx+1, wind_dir_idx+5).toInt(); + + // Then move on to B000 - wind direction angle (16 direction) + int wind_angle_idx = payload.indexOf('B'); + p->wind_angle = payload.substring(wind_angle_idx+1, wind_angle_idx+4).toInt(); + + // Then move on to C0000 - wind speed frequency (1 Hz) + int wind_freq_idx = payload.indexOf('C'); + p->wind_freq = payload.substring(wind_freq_idx+1, wind_freq_idx+5).toInt(); + + // Then move on to D0000 - wind speed real time (unit 0.1 m/s) + int wind_speed_idx = payload.indexOf('D'); + p->wind_speed = payload.substring(wind_speed_idx+1, wind_speed_idx+5).toInt() / 10; + + // Then move on to E0000 - wind speed avg in the last minute (unit 0.1 m/s) + int wind_speed_1m_idx = payload.indexOf('D'); + p->wind_speed_1m = payload.substring(wind_speed_1m_idx+1, wind_speed_1m_idx+5).toInt() / 10; + + // Then move on to F0000 - wind speed over the last 5 min + int wind_speed_5_idx = payload.indexOf('F'); + p->wind_speed_5m = payload.substring(wind_speed_5_idx+1, wind_speed_5_idx+5).toInt() / 10; + + // Then move on to G0000 - Rain in Realtime (0-9999 counter) bucket + int rain_bucket_cnt_idx = payload.indexOf('G'); + p->rain_bucket_cnt = payload.substring(rain_bucket_cnt_idx+1, rain_bucket_cnt_idx+5).toInt(); + + // Then move on to H0000 - Rain bucket in the last 1 minute (0-9999 counter) + int rt_rain_bucket_idx = payload.indexOf('H'); + p->rt_rain_bucket = payload.substring(rt_rain_bucket_idx+1, rt_rain_bucket_idx+5).toInt(); + + // Then move on to I0000 - rain last minute (0.1mm) + int rain1m_idx = payload.indexOf('I'); + p->rain_1m = payload.substring(rain1m_idx+1, rain1m_idx+5).toInt() *.1; + + // Then move on to J0000 - rain last hour (0.1mm) + int rain1h_idx = payload.indexOf('J'); + p->rain_1h = payload.substring(rain1h_idx+1, rain1h_idx+5).toInt() *.1; + + // Then move on to K0000 - rain last 24h (0.1mm) + int rain24h_idx = payload.indexOf('K'); + p->rain_24h = payload.substring(rain24h_idx+1, rain24h_idx+5).toInt()*.1; + + // Then move on to L0200 - temp (0.1°C) + // XXX: Check with minus zero temperatures + int temp_idx = payload.indexOf('L'); + p->temp_f = payload.substring(temp_idx+1, temp_idx+5).toInt()*.1; + + // Then move on to M611 - Humidity + int humidity_idx = payload.indexOf('M'); +// p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt()*.1; + p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt(); + + // Then move on to N10020 - air pressure + int pressure_idx = payload.indexOf('N'); + p->air_pressure = payload.substring(pressure_idx+1, pressure_idx+6).toInt()*.1; + +} + +// Clear the packet before working on the next +void clear_pkt(WS3Packet* p) { + p->wind_dir = 0; + p->wind_angle = 0; + p->wind_freq = 0; + p->wind_speed = 0; + p->wind_speed_1m = 0; + p->wind_speed_5m = 0; + p->rain_bucket_cnt = 0; + p->rt_rain_bucket = 0; + p->rain_1m = 0; + p->rain_1h = 0; + p->rain_24h = 0; + p->temp_f = 0; + p->humidity = 0; + p->air_pressure = 0; +} + +// Print the data +void print_weather(WS3Packet* p){ + Serial.print("Wind Direction (realtime): "); + Serial.println(p->wind_dir, DEC); + //Serial.println(" degrees"); + Serial.print("Wind direction angle : "); + Serial.print(p->wind_angle, DEC); + Serial.println(" degree"); + Serial.print("Wind speed Frequency: "); + Serial.print(p->wind_freq, DEC); + Serial.println(" Hz"); + Serial.print("Wind speed: "); + Serial.print(p->wind_speed, DEC); + Serial.println(" m/s"); + Serial.print("Wind speed 1m: "); + Serial.print(p->wind_speed_1m, DEC); + Serial.println(" m/s"); + Serial.print("Wind speed 5m: "); + Serial.print(p->wind_speed_5m, DEC); + Serial.println(" m/s"); + Serial.print("temp_f: "); + Serial.print(p->temp_f, DEC); + Serial.println(" deg. C."); + + Serial.print("Rain buckets / buckets 1m: "); + Serial.print(p->rain_bucket_cnt, DEC); + Serial.print(" / "); + Serial.println(p->rt_rain_bucket, DEC); + + Serial.print("Rain 1m / 1H / 24H: "); + Serial.print(p->rain_1m, DEC); + Serial.print(" / "); + Serial.print(p->rain_1h, DEC); + Serial.print(" / "); + Serial.print(p->rain_24h, DEC); + Serial.println(" mm"); + + Serial.print("humidity: "); + Serial.print(p->humidity, DEC); + Serial.println(" %"); + + Serial.print("air_pressure: "); + Serial.print(p->air_pressure, DEC); + Serial.println(" hpa"); + } + +// Processing the packet. +bool process_packet(String pkt, WS3Packet* p) { + debugln("[D] process_packet - ALive!"); + debugln(pkt); + + // Allocate bytes for the payload + String payload; + payload.reserve(WS3_PKT_LEN-WS3_CHK_LEN); + + #ifdef DONT_CHKSUM + // everything after the * is checksum (2 char long) + unsigned long chksum; + #endif /* DONT_CHKSUM */ + + // Check if the 75rd character is * + if (pkt.charAt(CHK_SUM_DELINEATOR) != '*') { + debugln("Packed invalid; no * character at position 75!"); + return false; + #ifdef DONT_CHKSUM + } else { + // The character indicating the checksum is coming is in the correct place. Yay. + // Now, we need to pull the two ascii characters that are transmitted to us + // and turn them into a single byte. E.G. Char 3, Char D should convert to 0x3D. + // + // We can do this with the strtoul() function; we indicate that we wante base 16 + + chksum = strtoul(pkt.substring(CHK_SUM_DELINEATOR+1, CHK_SUM_DELINEATOR+2).c_str(),NULL,16); + //Serial.print("Checksum :"); + //Serial.println(pkt.substring(CHK_SUM_DELINEATOR+1, CHK_SUM_DELINEATOR+2).c_str()); + } + #endif /* DONT_CHKSUM */ + + // We have the checksum, Now we can bother to get the payload + payload = pkt.substring(0, CHK_SUM_DELINEATOR); + + // And try to validate... + #ifndef DONT_CHKSUM + if(!validate_packet(payload, chksum)){ + debugln("invalid packet! :("); + return false; + } else { + debugln("Valid packet!"); + } + #endif /* DONT_CHKSUM */ + parse_packet(payload, p); + return true; +} + +void loop() { + // While data comes in and we don't have a pending packet to process... + while (WS3.available() && pkt_ok !=true) { + + // Pull the bytes off the stream + char inChar = (char)WS3.read(); + + // And build up the packet + pkt_str += inChar; + + // Until we hit the end + if (inChar == '\n') { + pkt_ok = true; + } + } + + // Yay, we now have a packet! + // Now, we attempt to parse out the string into a packet that we can work with + if (pkt_ok) { + debugln("pkt_ok!"); + + // At this point, we have a string of characters that was probably a valid packet + // We set get some memory and attempt to parse the string into the struct + WS3Packet p = {}; + + // Validate the payload, then parse it. + if (process_packet(pkt_str, &p)) { + // print results if parse OK + print_weather(&p); + debugln("processed"); + } else { + debugln("unable to parse packet :("); + } + + + // clear so we can start again + pkt_str = ""; + pkt_ok = false; + clear_pkt(&p); + } +} + +// MQTT Stuff +#ifdef MQTT +void callback(char* topic, byte* payload, unsigned int length) { + debug("Message arrived ["); + Serial.print(topic); + debug("] "); + for (int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + debug(" "); +} +void reconnect() { + // Loop until we're reconnected + while (!client.connected()) { + debug("Attempting MQTT connection..."); + // Attempt to connect + if (client.connect(Mqtt_clientid)) { + debugln("connected"); + // ... and resubscribe + client.subscribe(dom_in); + } else { + debug("failed, rc="); + debug(client.state()); + debugln(" try again in 5 seconds"); + // Wait 5 seconds before retrying + for(int i = 0; i<5000; i++){ + delay(1); + } + } + } +} +#endif