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.

Links

(*) Disclosure: Some of the links above are affiliate links, meaning, at no additional cost to you I will earn a (little) comission if you click through and make a purchase. I only recommend products I own myself and I'm convinced they are useful for other makers.

History

First upload: 2021-12-12 | Version: 2024-03-22