The Weatherstations Elsner P03/3-RS485 Basic, CET, and GPS
The German company Elsner offers several weather stations. You can read the data from these sensors with an Arduino and a RS485 adapter.
This page covers following modules:
- Elsner P03/3-RS485 Basic
- Elsner P03/3-RS485 CET
- Elsner P03/3-RS485 GPS
All three weather stations are using a RS485 interface. The sensors send data each second in plain ASCII. Each packet ends with an 0x03. The protocol is well described on the Elsner homepage. Obviously you can't use a Modbus RTU library for these weather stations, so you need to implement your own reader. At the end of the page you find a full working example to copy/paste into the Arduino IDE.
Some challenges of the Elsner P03/3-RS485 Weather Stations
The protocol start byte doesn't identify which kind of message you can expect. Some messages are 39 bytes long, the GPS version is 60 bytes long.
The time information is available in the CET and GPS Message,
Model | Length of payload | startbyte | time |
---|---|---|---|
Basic | 39 | W | no |
CET | 39 | W | yes |
GPS | 60 | G | yes |
The "startbyte" W is also used in the payload in the GPS. So be careful when you want to interpret a W as startbyte.
The endbyte is 0x03. You will need for testing a good serial program to send data in plain ASCII and the final marker as HEX. I'm using a old version of SSCOM3.2 where I can prepare some example messages to send them with a button press:
The Weatherstation - Going the OOP Way
Here I want to show a simple class for the Weatherstation from Elsner. I started with the "Serial Input Basics" Example 4 of the Arduino Forum and capsulated all functions into a weatherstation class. To be precise: three classes (one for each model).
The Class Constructor and Its Parameter
We don't want to hardcode the Serial interface. It's up to the user to use HW Serial, SoftSerial or AltSerial. So we need a way to hand over the Serial interface. Therefore the constructor will accept a parameter to a Streaming class.
If you want to use HW Serial3 on an Arduino Mega you can define a reference to Serial 3 and handover this Reference to the class:
HardwareSerial &mySerial = Serial3; WeatherstationBasic weatherstation(mySerial); // Weathersation Elsner P03/3-RS485 Basic
This will create one instance (one object) of a weatherstation of type WeatherstationBasic and as interface Serial3 will be used.
On an Arduino UNO you might want to use SoftSerial. You have do include the library, declare the used pins, create a Software Serial object and handover this interface to the weatherstation class:
#include <SoftwareSerial.h> constexpr byte rxPin = 2; // for SoftSerial constexpr byte txPin = 3; SoftwareSerial mySerial(rxPin, txPin); // RXpin, TXpin WeatherstationBasic weatherstation(mySerial); // Weathersation Elsner P03/3-RS485 Basic
Usually you would need to define a TX enable pin for RS485, but as there is no need to send data from the Arduino to the sensor there is no need to switch between TX/RX. The Arduino will only listen on the RS485 bus. You should connect the DE/RE pins of your RS485 module to LOW.
If you are using any other Serial interface than Serial - don't forget to call .begin()
myserial.begin(19200); // the Elsner sensor transmits at 19200 Baud
Class Inheritance
The protocol of these 3 sensors share some similarities. So I decided to implement classes for each sensor type but let the classes inherit from others.
The class WeatherstationBasic is for the Elsner P03/3-RS485 Basic and implements only the weather values:
float temperatureOut; // in °C de: AT Außentemperatur float sunSouth; // in kLux de: SoS Sonne Süd float sunWest; // in kLux de: SoW Sonne West float sunEast; // in kLux de: SoO Sonne Ost bool dusk; // de: Dämmerung float brightness; // de: Tageslicht [0..999] lux float wind; // windspeed in m/sec bool rain; // is raining de:Regen
The class WeatherstationCET inherits from the WeahterstationBasic and adds following date/time values:
uint8_t sec; // seconds after the minute 0..59 uint8_t min; // minutes after the hour 0..59 uint8_t hour; // hours since midnight 0..23 uint8_t mday; // day of the month 1..31 uint8_t mon; // month 1..12 ! unlike time.c January starts with 1 uint8_t year; // YY ! unlike time.c we have a two digit year with no epoch. 2021 --> 21 uint8_t wday; // time.c is days since sunday 0-6, sensor reports 1 Monday - 7 Sunday, (0 can be used as Sunday also) int8_t isdst; // Daylight Saving Time flag: 1 DST, 0 no DST, -1 information not available
The naming of the variables are somehow similar to the structure tm from the time.c library. Nevertheless please pay attention that the month will start with 1, the year is only two digits (like provided from the sensor) and the wday uses 0 and 7 for Sunday.
Finally the class WeatherstationGPS inherits from the WeatherstationCET. I.e. that WeatherstationGPS inherits all variables from WeatherstationCET and WeatherstationBasic. Additionally Weatherstation GPS provides following GPS data:
bool isValidGPS = false; float azimuth; float elevation; float lng; // longitude [-180 .. 180] - de: Geographische Länge float lat; // latitutde [-90 .. 90] - de: Geographische Breite
As the GPS module doesn't send a daylight saving time information, this variable is constant set to -1:
const int8_t isdst = -1; // DST information is not available on the GPS module
Public Members of Weatherstation Classes - The Interface to Your User Sketch
Public Methods (public member functions) represent the interface to the class.
.update() does the handling of the RS485 interface and will start parsing when new data was received. You must call .update() in loop over and over again
weatherstation.update(); // call this function in loop
Additionally you can define a callback function in your sketch. This callback
function will be called, when the Transmission of new Data was completed.
.setOnNewData(callbackOnNewData) will inform the class to call "callbackOnNewData()
whenever the data was updated completely. The usage of the callback is optional.
If you just want to access data after a period of time you can use any sort of
timer or "millis()-previousMillis" method also. If you want get notified when
new data is available you must define the function callbackOnNewData() in your usersketch.
weatherstation.setOnNewData(callbackOnNewData); // register a callback onNewData
The weather/time/GPS values from the sensor are also available public and can be used in the user sketch. If your sensor object is named weatherstation, you can print the outside temperature with
Serial.println(weatherstation.temperatureOut);
Hint: For usual one would write get functions to retrieve that data, but as this sketch is just a demo, I leave it open to the user to add these getters.
Class Internals - protected Members
Let's talk about further components of the classes:
.delBuffer() deletes the receiving / incoming buffer and it's contents. It's called by the constructor and can be called by other methods.
.parseLine() gets called when a full line was received. It can call several parsers (like for the CET or GPS). At the end it will call the callback function if you have defined one. This member function differs based on the implementation, therefore it is set virtual in the Basic class and "override" in CET and GPS class.
.parseBasic() is a helper function and parses the basic weather data of the sensor. It will be used in all three classes.
.parseDateTime() parses date and time data.
.parseGPS() parses GPS data.
And finally a remark to .update(): you will see one commented line
//if ((ndx!= 45 && rc == 'W') || rc == 'G') delBuffer();
I previously wanted to reset the buffer if we receive a start byte. As the startbyte 'W' is also a valid character in the payload in case of a GPS sensors, we would need to check also the position of the incoming character.
How to Organize Your Sketch
I propose to put the class definitions into a separate tab of the Arduino IDE called "elsner.h".
/* 3 classes for: Weatherstation Elsner P03/3-RS485 Basic Weatherstation Elsner P03/3-RS485 CET Weatherstation Elsner P03/3-RS485 GPS datasheets: https://www.elsner-elektronik.de/shop/de/fileuploader/download/download/?d=1&file=custom%2Fupload%2F30140_P033-RS485basic_Datenblatt_07Jun21_DBEEA6044.pdf https://www.elsner-elektronik.de/shop/de/fileuploader/download/download/?d=1&file=custom%2Fupload%2F30145_30151_P033-RS485-GPS_CET_Datenblatt_07Jun21_DBEEA6143.pdf length startbyte time Basic 39 W no CET 39 W yes GPS 60 G yes message example basic a123456789b123456789c123456789d123456789 WAAAAASSWWOODLLLWWWWR--------------CCCCE W+23.4123456J99912.3J999999999999998888x (remember: send 0x03 as end marker!) message example CET a123456789b123456789c123456789d123456789 WAAAAASSWWOODLLLWWWWRwDDMMYYHHMMSSdCCCCE W+23.4123456J99912.3J1080121083000N8888x (remember: send 0x03 as end marker!) message example GPS a123456789b123456789c123456789d123456789e123456789f123456789g WAAAAASSWWOODLLLWWWWRwDDMMYYHHMMSSgAAA.A+EE.EwLLL.Lsll.lCCCCE G+23.4123456J99912.3J10801210830001123.4+34.5W123.4N45.68888x (remember: send 0x03 as end marker!) open topic: - currently checksum is not checked documentation: - https://werner.rothschopf.net/microcontroller/202112_elsner_weatherstation_p03_basic_cet_GPS_en.htm by noiasca 2021-01-08 */ class WeatherstationBasic { protected: // receive data static const byte numChars = 64; // receiver buffer size char receivedChars[numChars]; // an array to store the received data byte ndx = 0; // length of received message boolean newData = false; // flag to indicate when new data is complete Stream &stream; // a reference to the serial interface void (*cbOnNewData)(); // gets called after we received a full message void delBuffer() { memcpy(receivedChars, "", numChars); ndx = 0; } void parseBasic() { // start marker W or G // +/- is set after parsing of the number temperatureOut = (receivedChars[2] - '0') * 10; temperatureOut += (receivedChars[3] - '0'); // . temperatureOut += (receivedChars[5] - '0') / 10.0; if (receivedChars[1] == '-') temperatureOut = temperatureOut * -1; sunSouth = (receivedChars[6] - '0') * 10; sunSouth += (receivedChars[7] - '0'); sunWest = (receivedChars[8] - '0') * 10; sunWest += (receivedChars[9] - '0'); sunEast = (receivedChars[10] - '0') * 10; sunEast += (receivedChars[11] - '0'); if (receivedChars[12] == 'J') dusk = true; else dusk = false; brightness = (receivedChars[13] - '0') * 100; brightness += (receivedChars[14] - '0') * 10; brightness += (receivedChars[15] - '0'); wind = (receivedChars[16] - '0') * 10; wind += (receivedChars[17] - '0'); // . wind += (receivedChars[19] - '0') / 10.0; if (receivedChars[20] == 'J') rain = true; else rain = false; } virtual void parseLine() { if (newData == true) { //Serial.print(F("This just in ... ")); Serial.println(receivedChars); // output all parseBasic(); if (cbOnNewData) cbOnNewData(); newData = false; } } public: // payload data basic float temperatureOut; // in °C de: AT Außentemperatur float sunSouth; // in kLux de:SoS Sonne Süd float sunWest; // in kLux de:Sonne West float sunEast; // in kLux de:Soo - Sonne Ost bool dusk; // de:Dämmerung float brightness; // de: Tageslicht [0..999] lux float wind; // windspeed in m/sec bool rain; // is raining de:Regen WeatherstationBasic (Stream &stream) : stream(stream) { delBuffer(); } void setOnNewData(void (*cbOnNewData)()) { (*this).cbOnNewData = cbOnNewData; } void update() // was recvWithEndMarker(); { char endMarker = 0x03; char rc; if (stream.available() > 0) { rc = stream.read(); if (rc != endMarker) { //if ((ndx!= 45 && rc == 'W') || rc == 'G') delBuffer(); // force begin of telegram but W is also in the payload... wtf receivedChars[ndx] = rc; ndx++; if (ndx >= numChars) { ndx = numChars - 1; } } else { receivedChars[ndx] = '\0'; // terminate the string ndx = 0; newData = true; parseLine(); } } } }; class WeatherstationCET : public WeatherstationBasic { protected: void parseDateTime() { wday = receivedChars[21] - '0'; mday = (receivedChars[22] - '0') * 10; mday += (receivedChars[23] - '0'); mon = (receivedChars[24] - '0') * 10; mon += (receivedChars[25] - '0'); year = (receivedChars[26] - '0') * 10; year += (receivedChars[27] - '0'); hour = (receivedChars[28] - '0') * 10; hour += (receivedChars[29] - '0'); min = (receivedChars[30] - '0') * 10; min += (receivedChars[31] - '0'); sec = (receivedChars[32] - '0') * 10; sec += (receivedChars[33] - '0'); } void parseLine() override { if (newData == true) { //Serial.print(F("This just in ... ")); Serial.println(receivedChars); // output all // MISSING: validate checksum parseBasic(); // parse basic data parseDateTime(); switch (receivedChars[34]) { case 'J' : isdst = 1; break; case 'N' : isdst = 0; break; default : isdst = -1; } if (cbOnNewData) cbOnNewData(); newData = false; } } public: // more payload data CET uint8_t sec; // seconds after the minute 0..59 uint8_t min; // minutes after the hour 0..59 uint8_t hour; // hours since midnight 0..23 uint8_t mday; // day of the month 1..31 uint8_t mon; // month 1..12 ! unlike time.c January starts with 1 uint8_t year; // YY ! unlike time.c we have a two digit year with no epoch 2021 --> 21 uint8_t wday; // time.c is days since sunday 0-6, sensor reports 1 Monday - 7 Sunday, (0 can be used as Sunday also) int8_t isdst; // Daylight Saving Time flag: 1 DST, 0 no DST, -1 information not available WeatherstationCET (Stream &stream) : WeatherstationBasic(stream) {} }; class WeatherstationGPS : public WeatherstationCET { protected: void parseGPS() { azimuth = (receivedChars[35] - '0') * 100; azimuth += (receivedChars[36] - '0') * 10; azimuth += (receivedChars[37] - '0'); // . azimuth += (receivedChars[39] - '0') / 10; elevation = (receivedChars[41] - '0') * 10; elevation += (receivedChars[42] - '0'); // . elevation += (receivedChars[44] - '0') / 10; if (receivedChars[40] == '-') elevation *= -1; lng = (receivedChars[46] - '0') * 100; lng += (receivedChars[47] - '0') * 10; lng += (receivedChars[48] - '0'); //. lng += (receivedChars[50] - '0') / 10; if (receivedChars[45] == 'W') lng *= -1; lat = (receivedChars[52] - '0') * 10; lat += (receivedChars[53] - '0'); //. lat += (receivedChars[55] - '0') / 10; if (receivedChars[51] == 'S') lat *= -1; } void parseLine() override { if (newData == true) { //Serial.print(F("This just in ... ")); Serial.println(receivedChars); // output all // MISSING: validate checksum parseBasic(); // parse basic data parseDateTime(); if (receivedChars[34] == '1') isValidGPS = true; else isValidGPS = false; parseGPS(); if (cbOnNewData) cbOnNewData(); newData = false; } } public: // more payload data GPS bool isValidGPS = false; // is GPS data valid float azimuth; // Azimuth of sun float elevation; // Elevation of sun float lng; // longitude [-180 .. 180] - de: Geographische Länge float lat; // latitutde [-90 .. 90] - de: Geographische Breite const int8_t isdst = -1; // DST information is not available on the GPS module WeatherstationGPS (Stream &stream) : WeatherstationCET(stream) {} };
in your maintab you write your implementation and include the tab elsner.h with a precompiler define:
/* Weatherstation Elsner P03/3-RS485 Basic, Elsner P03/3-RS485 CET, Elsner P03/3-RS485 GPS Based on a posting https://forum.arduino.cc/t/wetterstation-mit-rs485/943408/5 by noiasca 2021-01-08 */ #include "elsner.h" // classes for Elsner Weatherstations in a separate tab /* ******************************************************* Serial Interface * **************************************************** */ // if you don't have enough HW Serial (i.e. on an UNO) // you are forced to use SoftwareSerial or AltSoftSerial //#include//constexpr byte rxPin = 2; // for SoftSerial //constexpr byte txPin = 3; //SoftwareSerial mySerial(rxPin, txPin); // RXpin, TXpin // On a Mega you can simply use // a Reference to an existing HW Serial: // HardwareSerial &mySerial = Serial3; // If you don't need debugging output, you can even use // a reference to Serial but remember: the sensor needs 19200 baud! HardwareSerial &mySerial = Serial; //WeatherstationBasic weatherstation(mySerial); // Weathersation Elsner P03/3-RS485 Basic //WeatherstationCET weatherstation(mySerial); // Weathersation Elsner P03/3-RS485 CET WeatherstationGPS weatherstation(mySerial); // Weathersation Elsner P03/3-RS485 GPS // this function will be called each time we have received a new packet (=several lines) void callbackOnNewData() { printData(); } void printData() // quick'n'dirty print { Serial.print(F("temperatureOut ")); Serial.println(weatherstation.temperatureOut); Serial.print(F("wind ")); Serial.println(weatherstation.wind); Serial.print(F("sunSouth ")); Serial.println(weatherstation.sunSouth); Serial.print(F("sunEast ")); Serial.println(weatherstation.sunEast); Serial.print(F("sunWest ")); Serial.println(weatherstation.sunWest); Serial.print(F("brigthness ")); Serial.println(weatherstation.brightness); Serial.print(F("dusk ")); Serial.println(weatherstation.dusk); Serial.print(F("rain ")); Serial.println(weatherstation.rain); // for CET and GPS model Serial.print(F("date ")); Serial.print(weatherstation.year); Serial.print(":"); Serial.print(weatherstation.mon); Serial.print(":"); Serial.println(weatherstation.mday); Serial.print(F("time ")); Serial.print(weatherstation.hour); Serial.print(":"); Serial.print(weatherstation.min); Serial.print(":"); Serial.println(weatherstation.sec); // only for GPS model Serial.print(F("latitude ")); Serial.println(weatherstation.lat); Serial.print(F("longitute ")); Serial.println(weatherstation.lng); Serial.print(F("azimuth ")); Serial.println(weatherstation.azimuth); Serial.print(F("elevation ")); Serial.println(weatherstation.elevation); } // Arduino setup and loop void setup() { Serial.begin(19200); // local debug output //myserial.begin(19200); // the Elsner sensor run at 19200 Baud weatherstation.setOnNewData(callbackOnNewData); // register a callback onNewData } void loop() { weatherstation.update(); // call this function in loop }
How to Test Your Sketch
Open a new Arduino project and copy paste the code in the maintab and the classes in an additional tab elsner.h
When you do your testing, keep in mind to send the payload as ASCII but send a line end in hex with 0x03. The checksum is not implemented - so no checksum will be checked.
Disclaimer
This sketch has prototype status. Don't expect a perfect working example. The checksum is currently not validated and each checksum will be accepted.