diff --git a/code-snippets/client/main_client_station/.gitignore b/code-snippets/client/main_client_station/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..89cc49cbd652508924b868ea609fa8f6b758ec56
--- /dev/null
+++ b/code-snippets/client/main_client_station/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/code-snippets/client/main_client_station/.vscode/extensions.json b/code-snippets/client/main_client_station/.vscode/extensions.json
new file mode 100644
index 0000000000000000000000000000000000000000..080e70d08b9811fa743afe5094658dba0ed6b7c2
--- /dev/null
+++ b/code-snippets/client/main_client_station/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/code-snippets/client/main_client_station/.vscode/settings.json b/code-snippets/client/main_client_station/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..691a8f68f405ccdc2ba08296bd21c804bbec2ff3
--- /dev/null
+++ b/code-snippets/client/main_client_station/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+    "C_Cpp.errorSquiggles": "Disabled"
+}
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/README.md b/code-snippets/client/main_client_station/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..56969c608fe5ba87344dc2e9902dcdea7bb96215
--- /dev/null
+++ b/code-snippets/client/main_client_station/README.md
@@ -0,0 +1,6 @@
+# Main client station
+
+This is the main client station which  has a solar and 
+-humidity sensor
+-rain precipitation
+-solarRadiation
diff --git a/code-snippets/client/main_client_station/include/README b/code-snippets/client/main_client_station/include/README
new file mode 100644
index 0000000000000000000000000000000000000000..194dcd43252dcbeb2044ee38510415041a0e7b47
--- /dev/null
+++ b/code-snippets/client/main_client_station/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/code-snippets/client/main_client_station/include/forte_sensor.hpp b/code-snippets/client/main_client_station/include/forte_sensor.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4ca8e39602ae03062a79a2acb8c25e634521f8b8
--- /dev/null
+++ b/code-snippets/client/main_client_station/include/forte_sensor.hpp
@@ -0,0 +1,15 @@
+#ifndef _FORTE_SENSOR
+#define _FORTE_SENSOR
+
+// #include "Message.hpp"
+template <class T>
+class Forte_Sensor {
+  public:
+	virtual T read_data() = 0;
+	virtual void setup() = 0;
+	//virtual Message build_message() = 0;
+
+  private:
+};
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/Message/ClientDataPackage.hpp b/code-snippets/client/main_client_station/lib/Message/ClientDataPackage.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..52f2be6032a190e6346bef71457cb78c983b0038
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/Message/ClientDataPackage.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#define NUM_SENSORS 10
+// packing the struct without padding, makes reading it on the fipy easier
+#pragma pack(1)
+
+// having the data be a struct of basic types makes sending easier,
+// otherwise we would have to serialize the data before sending
+struct ClientDataPackage {
+	int identifiers[NUM_SENSORS];
+	float values[NUM_SENSORS];
+	int amountData;
+	long timestamp; // maybe make this array
+};
diff --git a/code-snippets/client/main_client_station/lib/Message/Message.cpp b/code-snippets/client/main_client_station/lib/Message/Message.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6975ab6cd228ab218df9759abddc498a082c288c
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/Message/Message.cpp
@@ -0,0 +1,29 @@
+#include "Message.hpp"
+
+void Message::add_data(float value, int identifier)
+{
+	if (data.amountData < NUM_SENSORS) {
+		data.values[data.amountData] = value;
+		data.identifiers[data.amountData] = identifier;
+		data.amountData++;
+	}
+}
+
+ClientDataPackage Message ::get_client_data_package() const
+{
+	return data;
+}
+
+Message ::Message()
+{
+	// check for existing host mac address, use broadcast otherwise
+
+	data.amountData = 0;
+	data.timestamp = Time::getInstance().getMillis(); // I am assuming we are not sending data from Unix Epoch
+}
+
+Message ::Message(ClientDataPackage old_data)
+{
+	data = old_data;
+	// memcpy(&data, &old_data, sizeof(data));
+}
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/Message/Message.hpp b/code-snippets/client/main_client_station/lib/Message/Message.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..397ca6f707105be90fb76add8200b8edef93c8ed
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/Message/Message.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "ClientDataPackage.hpp"
+#include "Time.hpp"
+#include <Arduino.h>
+#include <ESP32Time.h>
+#include <esp_now.h>
+
+// Format of the message sent from host to client
+// if more things are sent from the host the name might not be accurate anymore
+class Message {
+  public:
+	Message();
+	Message(ClientDataPackage old_data);
+	void add_data(float value, int identifier);
+	ClientDataPackage get_client_data_package() const;
+
+  private:
+	ClientDataPackage data;
+};
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/SentecSensors/SentecSensors.cpp b/code-snippets/client/main_client_station/lib/SentecSensors/SentecSensors.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..76ca9f90ad538bf75288798863868642fa59cd50
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/SentecSensors/SentecSensors.cpp
@@ -0,0 +1,416 @@
+#include <Arduino.h>
+#include <SoftwareSerial.h>
+
+#include <SentecSensors.h>
+/***************************************
+ *          RS485 SENSOR READOUT
+ ****************************************/
+
+SentecSensorRS485::SentecSensorRS485(SoftwareSerial *ser, byte add)
+{
+    address = add;
+    RS485 = ser;
+}
+
+void SentecSensorRS485::write(byte queryFrame[], int length)
+{
+    // sends a message (bytes) to the sensor
+
+    // Initialize the transmitter
+    digitalWrite(serialCommunicationControlPin, HIGH);
+    // Send message: request a reading from the sensor
+    RS485->write(queryFrame, length);
+    RS485->flush();
+    // Initialize the receiver
+    digitalWrite(serialCommunicationControlPin, LOW);
+}
+
+String SentecSensorRS485::getValueStr(float value)
+{
+    if (valid)
+    {
+        return String(value, 1);
+    }
+    else
+    {
+        return String("null");
+    }
+}
+
+String SentecSensorRS485::getValueStr(int value)
+{
+    if (valid)
+    {
+        return String(value);
+    }
+    else
+    {
+        return String("null");
+    }
+}
+
+void SentecSensorRS485::queryAddress()
+{
+    // request the address of the sensor with ONLY ONE SENSOR ON THE BUS
+
+    byte tmp_addr = address; // store the address in a temporary byte
+    address = 0xFF;          // change the address to FF (0) for address check
+    readRegister(word(0x07, 0xD0), 2);
+    address = tmp_addr; // set the original address back
+}
+
+void SentecSensorRS485::readRegister(int registerStartAddress)
+{
+    readRegister(registerStartAddress, 1);
+}
+
+void SentecSensorRS485::readRegister(int registerStartAddress, int registerLength)
+{
+    // function code 0x03: get data measured by the sensor
+    byte query[8];
+    query[0] = address;
+    // function code
+    query[1] = 0x03;
+    // register start address
+    query[2] = registerStartAddress >> 8;
+    query[3] = registerStartAddress & 0xFF;
+    // register length
+    query[4] = registerLength >> 8;
+    query[5] = registerLength & 0xFF;
+    // calculate last two bytes (CRC check)
+    calculateCRC(query, sizeof(query) - 2);
+ //#   Serial.print("Query (get data): 0x");                    #Print bytes
+ //#   printBytes(query, 8);
+    // write the data request to the modbus line
+    write(query, sizeof(query));
+    // get response from sensor
+    getResponse();
+}
+
+void SentecSensorRS485::writeRegister(int registerAddress, int value)
+{
+    // function code 0x06: change sensor settings
+    // e.g. a new address, reset rainfal data...
+
+    byte query[8];
+    query[0] = address;
+    // function code
+    query[1] = 0x06;
+    // register start address
+    query[2] = registerAddress >> 8;
+    query[3] = registerAddress & 0xFF;
+    // register length
+    query[4] = value >> 8;
+    query[5] = value & 0xFF;
+    calculateCRC(query, sizeof(query) - 2);
+    Serial.print("Query (settings):    ");
+    printBytes(query, 8);
+    write(query, sizeof(query));
+    getResponse();
+}
+
+void SentecSensorRS485::setAddress(byte add)
+{
+    // change the address of a sensor
+    writeRegister(word(0x07, 0xD0), add);
+    // TODO check response: matches the sent message exactly
+    address = add;
+}
+
+void SentecSensorRS485::resetAnswerFrame()
+{
+    for (int i = 0; i < 10; i++)
+    {
+        answerFrame[i] = 0;
+    }
+}
+
+bool SentecSensorRS485::getResponse()
+{
+    // reads the response of a sensor
+    valid = true;
+    int idx = 0;
+    int byteReceived;
+    // usual response length: changed in the while loop to match the response,
+    //   changed only when reading data (then it's 7 or 9  bytes, sensor dpendent)
+    int responseLength = 8;
+    // reading an answer takes up to 39 milliseconds for 2 byte readRegister
+    const int timeout = 200;
+    const int retries = 3; //#editet to q
+    size_t tries = 1;
+
+    for (tries; tries <= retries; tries++)
+    {
+        // if we lose connection with the sensor, we get an array of zeros back
+        resetAnswerFrame();
+
+        unsigned long time = millis();
+        while (idx < responseLength && (millis() - time) < timeout)
+        {
+            if (RS485->available())
+            {
+                byteReceived = RS485->read();
+                // Serial.println(byteReceived, HEX);
+                // check for first byte. It has to be the device address unless for broadcasts with address = 0xFF
+                if (idx == 0 && address != 0xFF && byteReceived != address)
+                {
+                    Serial.print("Invalid byte. First byte needs to be address 0x");
+                    Serial.print(address, HEX);
+                    Serial.print(" but got 0x");
+                    Serial.print(byteReceived, HEX);
+                    Serial.println("instead");
+                }
+                else
+                {
+                    answerFrame[idx] = byteReceived;
+                    // for reading register: third received byte is data length, read number of bytes accordingly
+                    if (idx == 2 && answerFrame[1] == 0x03)
+                    {
+                        // 5 bytes for address, function, data length, CRC_H, CRC_L
+                        responseLength = 5 + byteReceived;
+                    }
+                    idx++;
+                }
+            }
+        }
+
+        delay(10);
+        Serial.print("Response: 0x");
+        printBytes(answerFrame, responseLength);
+        Serial.print("Tries: ");
+        Serial.println(tries);
+        Serial.print("Bytes received: ");
+        Serial.println(idx);
+        word crc_received = word(answerFrame[responseLength - 2], answerFrame[responseLength - 1]);
+        word crc = calculateCRC(answerFrame, responseLength - 2);
+        if (crc_received != word(crc))
+        {
+            Serial.print("CRC wrong: Expected ");
+            printBytes(crc);
+            Serial.print(" got ");
+            printBytes(crc_received);
+            Serial.println();
+            valid = false;
+            resetAnswerFrame();
+        }
+
+        if (answerFrame[0] == 0)
+        {
+            valid = false;
+        }
+        if(valid)
+        {
+            break;
+        }
+    }
+    return valid;
+}
+
+unsigned int SentecSensorRS485::calculateCRC(byte query[], int length)
+{
+    // Change the last two bytes of the queryFrame to conform to a CRC check
+    // Yes, this is necessary. No, I don't know exactly what it does.
+    unsigned int tmp1, tmp2, flag;
+    tmp1 = 0xFFFF;
+    for (unsigned char i = 0; i < length; i++)
+    {
+        tmp1 = tmp1 ^ query[i];
+        for (unsigned char j = 1; j <= 8; j++)
+        {
+            flag = tmp1 & 0x0001;
+            tmp1 >>= 1;
+            if (flag)
+                tmp1 ^= 0xA001;
+        }
+    }
+    // Reverse byte order.
+    tmp2 = tmp1 >> 8;
+    tmp1 = (tmp1 << 8) | tmp2;
+    tmp1 &= 0xFFFF;
+    // change last two query bytes
+    query[length + 1] = tmp1;
+    query[length] = tmp1 >> 8;
+
+    return tmp1; // the returned value is already swapped - CRC_L byte is first & CRC_H byte is last
+}
+
+void SentecSensorRS485::printBytes(byte *data, int length)
+{
+    // prints 8-bit data in hex with leading zeroes
+    char tmp[16];
+    for (int i = 0; i < length; i++)
+    {
+        sprintf(tmp, "%.2X", data[i]);
+        // sprintf(tmp, "0x%.2X",data[i]);
+        Serial.print(tmp);
+        Serial.print(" ");
+    }
+    Serial.println();
+}
+
+void SentecSensorRS485::printBytes(word data)
+{
+    char tmp[10];
+    sprintf(tmp, "0x%.2X", data >> 8);
+    Serial.print(tmp);
+    sprintf(tmp, "%.2X", data & 0xFF);
+    // sprintf(tmp, "0x%.2X",data[i]);
+    Serial.print(tmp);
+}
+
+word SolarRadiationSensor::getSolarRadiation()
+{
+    readRegister(0, 1);
+    glob = word(answerFrame[3], answerFrame[4]);
+    Serial.print("Global solar radiation: ");
+    Serial.print(glob, DEC);
+    Serial.println(" W/m^2");
+    return glob;
+}
+
+String SolarRadiationSensor::getSolarRadiationStr()
+{
+    return getValueStr((int)glob);
+}
+
+void RainGaugeSensor::queryTime()
+{
+    // get time setting of the rain gauge
+    readRegister(0x34, 0x03);
+}
+
+void RainGaugeSensor::setTime()
+{
+    // set time of the rain gauge
+    byte query[15];
+    query[0] = address;
+    query[1] = 0x10; // function code
+    query[2] = 0x00; // register start address high
+    query[3] = 0x34; // register start address low
+    query[4] = 0x00; // register length high
+    query[5] = 0x03; // register length low
+    query[6] = 0x06; // data length
+    // TODO parse timestamp... hard-code a random date for now.
+    query[7] = 0x20;  // year
+    query[8] = 0x04;  // month
+    query[9] = 0x03;  // day
+    query[10] = 0x17; // hours
+    query[11] = 0x06; // minutes
+    query[12] = 0x28; // seconds
+    query[13] = 0x00; // CRC low
+    query[14] = 0x00; // CRC high
+
+    calculateCRC(query, sizeof(query) - 2);
+    Serial.print("Query (set time):    ");
+    printBytes(query, 15);
+    write(query, sizeof(query));
+    getResponse();
+}
+
+void RainGaugeSensor::resetPrecipitation()
+{
+    // clears rainfall rata from the rain gauge
+    // delay resetting after the last register reading
+    delay(100);
+    Serial.println("Resetting precipitation sum");
+    writeRegister(0x00, 0x5A);
+    // TODO check response: matches the sent message exactly
+}
+
+void RainGaugeSensor::resetSensor()
+{
+    // no clue what the difference between this one and the previous one is...
+    //   and the manual is not helpful, of course
+    delay(100);
+    Serial.println("Resetting precipitation sum");
+    writeRegister(0x37, 0x03);
+    // TODO check response: matches the sent message exactly
+}
+
+void RainGaugeSensor::getPrecipitation()
+{
+    // get all precipitation statistics
+    readRegister(0, 0x0A);
+    word prcpToday = word(answerFrame[3], answerFrame[4]);
+    word prcpInstantaneous = word(answerFrame[5], answerFrame[6]); // TODO I'm pretty sure this value is bullshit (0.1 mm resolution)
+    word prcpYesterday = word(answerFrame[7], answerFrame[8]);
+    word prcpTotal = word(answerFrame[9], answerFrame[10]);
+    word prcpHourly = word(answerFrame[11], answerFrame[12]);
+    word prcpLastHourly = word(answerFrame[13], answerFrame[14]);
+    word prcp24max = word(answerFrame[15], answerFrame[16]);
+    word prcp24maxPeriod = word(answerFrame[17], answerFrame[18]);
+    word prcp24min = word(answerFrame[19], answerFrame[20]);
+    word prcp24minPeriod = word(answerFrame[21], answerFrame[22]);
+
+    Serial.print("Today: ");
+    Serial.print(prcpToday / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("Instantaneous: ");
+    Serial.print(prcpInstantaneous / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("Total: ");
+    Serial.print(prcpTotal / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("Hourly: ");
+    Serial.print(prcpHourly / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("24 max: ");
+    Serial.print(prcp24max / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("24 min: ");
+    Serial.print(prcp24min / 10.0, 1);
+    Serial.println(" mm");
+}
+
+word RainGaugeSensor::getInstantaneousPrecipitation()
+{
+    // gets tips of scale since the last reset, i.e. total precipitation (I THINK)
+    //   manual says this is current precipitation - is it?
+    readRegister(0, 0x01);
+    precipitation = word(answerFrame[3], answerFrame[4]);
+    Serial.print("Precipitation: ");
+    Serial.print(precipitation / 10.0, 1);
+    Serial.println(" mm");
+    // resetPrecipitation();
+    return precipitation;
+}
+
+String RainGaugeSensor::getPrecipitationStr()
+{
+    return getValueStr((float)(precipitation / 10.0));
+}
+
+word SoilMoistureSensor::getMoistureTemp()
+{
+    readRegister(0, 2); // start register at 0, read 2 variables (vwc, soil temp)
+    moistureRaw = word(answerFrame[3], answerFrame[4]);
+    if (answerFrame[5] < 0x80)
+    {
+        temperatureRaw = word(answerFrame[5], answerFrame[6]);
+    }
+    else
+    {
+        temperatureRaw = word(answerFrame[5], answerFrame[6]) - 65536;
+    }
+    Serial.print("Soil moisture: ");
+    Serial.print((moistureRaw - moistureOffset) / 10.0, 1);
+    Serial.println(" %");
+    Serial.print("Temperature: ");
+    Serial.print((temperatureRaw - temperatureOffset) / 10.0 , 1);
+    Serial.println(" °C");
+    return temperatureRaw;
+}
+
+float SoilMoistureSensor::getMoisture(){
+   return (float)((moistureRaw - moistureOffset) / 10.0);
+}
+
+String SoilMoistureSensor::getMoistureStr()
+{
+    return getValueStr((float)((moistureRaw - moistureOffset) / 10.0));
+}
+
+String SoilMoistureSensor::getTemperatureStr()
+{
+    return getValueStr((float)((temperatureRaw - temperatureOffset) / 10.0));
+}
diff --git a/code-snippets/client/main_client_station/lib/SentecSensors/SentecSensors.h b/code-snippets/client/main_client_station/lib/SentecSensors/SentecSensors.h
new file mode 100644
index 0000000000000000000000000000000000000000..b21f8c56c2af6353d8a06aeb41c2a1ded08449ac
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/SentecSensors/SentecSensors.h
@@ -0,0 +1,88 @@
+#ifndef SENTECSENSORS_H
+#define SENTECSENSORS_H
+
+#include <Arduino.h>
+#include <SoftwareSerial.h>
+
+class SentecSensorRS485
+{
+public:
+    byte address;
+    uint8_t serialCommunicationControlPin = 6;
+    SoftwareSerial *RS485;
+    byte answerFrame[10];
+    // TODO use valid flag to log None
+    bool valid = false;
+    SentecSensorRS485(SoftwareSerial *ser, byte add);
+
+    void write(byte queryFrame[], int length);
+    String getValueStr(float value);
+    String getValueStr(int value);
+    void queryAddress();
+    void readRegister(int registerStartAddress);
+    void readRegister(int registerStartAddress, int registerLength);
+    void writeRegister(int registerAddress, int value);
+    void setAddress(byte add);
+    void resetAnswerFrame();
+    bool getResponse();
+    unsigned int calculateCRC(byte query[], int length);
+    void printBytes(byte *data, int length);
+    void printBytes(word data);
+};
+
+class SolarRadiationSensor : public SentecSensorRS485
+{
+public:
+    using SentecSensorRS485::SentecSensorRS485;
+    // global radiation [W/m^2]
+    word glob = 0;
+
+    word getSolarRadiation();
+    String getSolarRadiationStr();
+};
+
+class RainGaugeSensor : public SentecSensorRS485
+{
+public:
+    using SentecSensorRS485::SentecSensorRS485;
+    // precipitation values [mm]
+    word precipitation = 0;     // prcp since reset? (I THINK!!!)
+    word prcpToday = 0;         // prcp since 00:00
+    word prcpInstantaneous = 0; // rainfall since last query - THIS IS OFF
+    word prcpYesterday = 0;     // prcp sum yesterday 00:00-24:00
+    word prcpTotal = 0;         // prcp since sensor power-up
+    word prcpHourly = 0;        // prcp this hour
+    word prcpLastHourly = 0;    // prcp last hour
+    word prcp24max = 0;         // max mm in the last 24h
+    word prcp24maxPeriod = 0;   // time when max prcp was measured
+    word prcp24min = 0;         // min mm in the last 24h
+    word prcp24minPeriod = 0;   // time when min prcp was measured
+
+    void queryTime();
+    void setTime();
+    void resetPrecipitation();
+    void resetSensor();
+    void getPrecipitation();
+    word getInstantaneousPrecipitation();
+    String getPrecipitationStr();
+};
+
+class SoilMoistureSensor : public SentecSensorRS485
+{
+public:
+    using SentecSensorRS485::SentecSensorRS485;
+    // vwc: volumetric water content [%]
+    word moistureRaw = 0;
+    int moistureOffset = 0;
+    // soil temperature [deg C]
+    int temperatureRaw = 0;
+    int temperatureOffset = 0;
+
+    word getMoistureTemp();
+    float getMoisture();
+    String getMoistureStr();
+    String getTemperatureStr();
+};
+
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/caching/src/ram_caching.cpp b/code-snippets/client/main_client_station/lib/caching/src/ram_caching.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9325e7c7909473c998d2a113779d94480fa3589c
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/caching/src/ram_caching.cpp
@@ -0,0 +1,24 @@
+#include "ram_caching.hpp"
+
+RTC_DATA_ATTR int cachedAmount = -1;
+RTC_DATA_ATTR ClientDataPackage backup[NUM_SENSORS];
+
+ClientDataPackage ram_cache_pop()
+{
+	return backup[cachedAmount--];
+}
+
+void ram_cache_push(ClientDataPackage data)
+{
+	backup[++cachedAmount] = data;
+}
+
+bool ram_cache_is_empty()
+{
+	return cachedAmount == -1;
+}
+
+bool ram_cache_is_full()
+{
+	return cachedAmount == 9;
+}
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/caching/src/ram_caching.hpp b/code-snippets/client/main_client_station/lib/caching/src/ram_caching.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..466668ba65578e0a9aa3f0af9bf20174889b0eb2
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/caching/src/ram_caching.hpp
@@ -0,0 +1,11 @@
+#ifndef _RAM_CACHE
+#define _RAM_CACHE
+#include "ClientDataPackage.hpp"
+#include <ESP32Time.h>
+
+bool ram_cache_is_empty();
+bool ram_cache_is_full();
+void ram_cache_push(ClientDataPackage data);
+ClientDataPackage ram_cache_pop();
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/espnow/README b/code-snippets/client/main_client_station/lib/espnow/README
new file mode 100644
index 0000000000000000000000000000000000000000..55f89c0b2141283b3dd2d94c882cdaccbe2064fe
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/espnow/README
@@ -0,0 +1,11 @@
+# basic usage
+
+To send data using espnow, create a new Message object,
+then use the add_data(value, identifier) method for every value
+to fill the message.
+when every value is added, use the send() method to send the data
+to the host (fipy). If the esp client has never recieved a config
+message from the host, it will instead broadcast the message.
+
+---
+right now, it is not possible to add more than 10 values.
diff --git a/code-snippets/client/main_client_station/lib/espnow/src/ClientDataPackage.hpp b/code-snippets/client/main_client_station/lib/espnow/src/ClientDataPackage.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..52f2be6032a190e6346bef71457cb78c983b0038
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/espnow/src/ClientDataPackage.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#define NUM_SENSORS 10
+// packing the struct without padding, makes reading it on the fipy easier
+#pragma pack(1)
+
+// having the data be a struct of basic types makes sending easier,
+// otherwise we would have to serialize the data before sending
+struct ClientDataPackage {
+	int identifiers[NUM_SENSORS];
+	float values[NUM_SENSORS];
+	int amountData;
+	long timestamp; // maybe make this array
+};
diff --git a/code-snippets/client/main_client_station/lib/espnow/src/ESPNow.cpp b/code-snippets/client/main_client_station/lib/espnow/src/ESPNow.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..359b43c50b855b7f90d955995befe915a3846e3d
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/espnow/src/ESPNow.cpp
@@ -0,0 +1,91 @@
+#include "ESPNow.hpp"
+
+uint8_t BROADCAST_MAC[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+esp_now_peer_info_t hostInfo;
+Preferences preferences;
+
+void get_host_mac(uint8_t *destination)
+{
+	preferences.begin("config", true);
+	if (!preferences.isKey("host")) {
+		preferences.getBytes("host", destination, sizeof(uint8_t) * 6);
+	} else {
+		memcpy(destination, BROADCAST_MAC, sizeof(BROADCAST_MAC));
+		Serial.println("backup mac used");
+	}
+	preferences.end();
+}
+void on_data_sent(const uint8_t *mac_addr, esp_now_send_status_t status)
+{
+	// go to sleep
+}
+
+void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len)
+{
+	Serial.println("message recieved");
+	config new_config;
+	memcpy(&new_config, incomingData, sizeof(new_config)); // TODO: check for valid mac
+
+	// put the host address in flash mem
+	preferences.begin("config", false);
+	if (!preferences.isKey("host")) {
+		preferences.putBytes("host", new_config.host, sizeof(new_config.host));
+		Serial.println("host mac saved to flash");
+	} else{
+		Serial.println("host mac already exists");
+	}// host change shouldn't be an issue
+	preferences.end();
+	// sync time
+	Time::getInstance().setTime(
+	    new_config.time_millis); // see https://www.esp32.com/viewtopic.php?t=9965, maybe this needs an offset
+	Serial.println("Saved Time: " + (String) new_config.time_millis);
+	Serial.flush();
+}
+
+esp_err_t espnow_setup()
+{
+	esp_err_t result;
+	WiFi.mode(WIFI_STA);
+	result = esp_now_init();
+	if (result != ESP_OK) {
+		// initialization failed
+		return result; // not sure about this
+	}
+
+	get_host_mac(hostInfo.peer_addr); // check if there is a host saved in flash mem, broadcast otherwise
+
+	hostInfo.channel = 0;
+	hostInfo.encrypt = 0;
+	esp_now_add_peer(&hostInfo);
+
+	esp_now_register_recv_cb(on_data_recv);
+	esp_now_register_send_cb(on_data_sent);
+
+	return ESP_OK;
+}
+
+esp_err_t espnow_send_message(const Message& message){
+	Serial.println("sending Message");
+	esp_err_t success;
+	ClientDataPackage dataP = message.get_client_data_package();
+	uint8_t recipient;
+	get_host_mac(&recipient);
+
+	success = esp_now_send(&recipient, (uint8_t *) &dataP, sizeof(ClientDataPackage));
+	// if(success != ESP_OK){
+	//     if(!ram_cache_is_full()){
+	//         ram_cache_push(*data);
+	//     }
+	// }
+
+	for (int i = 0; i < dataP.amountData; i++) {
+		Serial.println(dataP.values[i]);
+	}
+	
+	Serial.println((String) "time sent: " + dataP.timestamp);
+	Serial.println((String) "Send status: " + success);
+	Serial.println();
+	Serial.println("done");
+	Serial.flush();
+	return success;
+}
diff --git a/code-snippets/client/main_client_station/lib/espnow/src/ESPNow.hpp b/code-snippets/client/main_client_station/lib/espnow/src/ESPNow.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e4ac43b351c4431e8a1bd1b15740ae5fe085e2fe
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/espnow/src/ESPNow.hpp
@@ -0,0 +1,25 @@
+#ifndef _ESPNOW
+#define _ESPNOW
+
+#include "Message.hpp"
+#include "Time.hpp"
+#include "ram_caching.hpp"
+#include <ClientDataPackage.hpp>
+#include <ESP32Time.h>
+#include <Preferences.h>
+#include <WiFi.h>
+#include <esp_now.h>
+
+typedef struct config {
+	uint8_t host[6];
+	long time_millis;
+} config;
+
+esp_err_t espnow_setup();
+esp_err_t espnow_send_message(const Message& message);
+bool is_host_defined();
+void get_host_mac(uint8_t *destination);
+void on_data_sent(const uint8_t *mac_addr, esp_now_send_status_t status);
+void on_data_recv(const uint8_t *mac, const uint8_t *incomingData, int len);
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/espnow/src/Message.cpp b/code-snippets/client/main_client_station/lib/espnow/src/Message.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6975ab6cd228ab218df9759abddc498a082c288c
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/espnow/src/Message.cpp
@@ -0,0 +1,29 @@
+#include "Message.hpp"
+
+void Message::add_data(float value, int identifier)
+{
+	if (data.amountData < NUM_SENSORS) {
+		data.values[data.amountData] = value;
+		data.identifiers[data.amountData] = identifier;
+		data.amountData++;
+	}
+}
+
+ClientDataPackage Message ::get_client_data_package() const
+{
+	return data;
+}
+
+Message ::Message()
+{
+	// check for existing host mac address, use broadcast otherwise
+
+	data.amountData = 0;
+	data.timestamp = Time::getInstance().getMillis(); // I am assuming we are not sending data from Unix Epoch
+}
+
+Message ::Message(ClientDataPackage old_data)
+{
+	data = old_data;
+	// memcpy(&data, &old_data, sizeof(data));
+}
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/espnow/src/Message.hpp b/code-snippets/client/main_client_station/lib/espnow/src/Message.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..397ca6f707105be90fb76add8200b8edef93c8ed
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/espnow/src/Message.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "ClientDataPackage.hpp"
+#include "Time.hpp"
+#include <Arduino.h>
+#include <ESP32Time.h>
+#include <esp_now.h>
+
+// Format of the message sent from host to client
+// if more things are sent from the host the name might not be accurate anymore
+class Message {
+  public:
+	Message();
+	Message(ClientDataPackage old_data);
+	void add_data(float value, int identifier);
+	ClientDataPackage get_client_data_package() const;
+
+  private:
+	ClientDataPackage data;
+};
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/rs485/SentecSensors.cpp b/code-snippets/client/main_client_station/lib/rs485/SentecSensors.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..76ca9f90ad538bf75288798863868642fa59cd50
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/rs485/SentecSensors.cpp
@@ -0,0 +1,416 @@
+#include <Arduino.h>
+#include <SoftwareSerial.h>
+
+#include <SentecSensors.h>
+/***************************************
+ *          RS485 SENSOR READOUT
+ ****************************************/
+
+SentecSensorRS485::SentecSensorRS485(SoftwareSerial *ser, byte add)
+{
+    address = add;
+    RS485 = ser;
+}
+
+void SentecSensorRS485::write(byte queryFrame[], int length)
+{
+    // sends a message (bytes) to the sensor
+
+    // Initialize the transmitter
+    digitalWrite(serialCommunicationControlPin, HIGH);
+    // Send message: request a reading from the sensor
+    RS485->write(queryFrame, length);
+    RS485->flush();
+    // Initialize the receiver
+    digitalWrite(serialCommunicationControlPin, LOW);
+}
+
+String SentecSensorRS485::getValueStr(float value)
+{
+    if (valid)
+    {
+        return String(value, 1);
+    }
+    else
+    {
+        return String("null");
+    }
+}
+
+String SentecSensorRS485::getValueStr(int value)
+{
+    if (valid)
+    {
+        return String(value);
+    }
+    else
+    {
+        return String("null");
+    }
+}
+
+void SentecSensorRS485::queryAddress()
+{
+    // request the address of the sensor with ONLY ONE SENSOR ON THE BUS
+
+    byte tmp_addr = address; // store the address in a temporary byte
+    address = 0xFF;          // change the address to FF (0) for address check
+    readRegister(word(0x07, 0xD0), 2);
+    address = tmp_addr; // set the original address back
+}
+
+void SentecSensorRS485::readRegister(int registerStartAddress)
+{
+    readRegister(registerStartAddress, 1);
+}
+
+void SentecSensorRS485::readRegister(int registerStartAddress, int registerLength)
+{
+    // function code 0x03: get data measured by the sensor
+    byte query[8];
+    query[0] = address;
+    // function code
+    query[1] = 0x03;
+    // register start address
+    query[2] = registerStartAddress >> 8;
+    query[3] = registerStartAddress & 0xFF;
+    // register length
+    query[4] = registerLength >> 8;
+    query[5] = registerLength & 0xFF;
+    // calculate last two bytes (CRC check)
+    calculateCRC(query, sizeof(query) - 2);
+ //#   Serial.print("Query (get data): 0x");                    #Print bytes
+ //#   printBytes(query, 8);
+    // write the data request to the modbus line
+    write(query, sizeof(query));
+    // get response from sensor
+    getResponse();
+}
+
+void SentecSensorRS485::writeRegister(int registerAddress, int value)
+{
+    // function code 0x06: change sensor settings
+    // e.g. a new address, reset rainfal data...
+
+    byte query[8];
+    query[0] = address;
+    // function code
+    query[1] = 0x06;
+    // register start address
+    query[2] = registerAddress >> 8;
+    query[3] = registerAddress & 0xFF;
+    // register length
+    query[4] = value >> 8;
+    query[5] = value & 0xFF;
+    calculateCRC(query, sizeof(query) - 2);
+    Serial.print("Query (settings):    ");
+    printBytes(query, 8);
+    write(query, sizeof(query));
+    getResponse();
+}
+
+void SentecSensorRS485::setAddress(byte add)
+{
+    // change the address of a sensor
+    writeRegister(word(0x07, 0xD0), add);
+    // TODO check response: matches the sent message exactly
+    address = add;
+}
+
+void SentecSensorRS485::resetAnswerFrame()
+{
+    for (int i = 0; i < 10; i++)
+    {
+        answerFrame[i] = 0;
+    }
+}
+
+bool SentecSensorRS485::getResponse()
+{
+    // reads the response of a sensor
+    valid = true;
+    int idx = 0;
+    int byteReceived;
+    // usual response length: changed in the while loop to match the response,
+    //   changed only when reading data (then it's 7 or 9  bytes, sensor dpendent)
+    int responseLength = 8;
+    // reading an answer takes up to 39 milliseconds for 2 byte readRegister
+    const int timeout = 200;
+    const int retries = 3; //#editet to q
+    size_t tries = 1;
+
+    for (tries; tries <= retries; tries++)
+    {
+        // if we lose connection with the sensor, we get an array of zeros back
+        resetAnswerFrame();
+
+        unsigned long time = millis();
+        while (idx < responseLength && (millis() - time) < timeout)
+        {
+            if (RS485->available())
+            {
+                byteReceived = RS485->read();
+                // Serial.println(byteReceived, HEX);
+                // check for first byte. It has to be the device address unless for broadcasts with address = 0xFF
+                if (idx == 0 && address != 0xFF && byteReceived != address)
+                {
+                    Serial.print("Invalid byte. First byte needs to be address 0x");
+                    Serial.print(address, HEX);
+                    Serial.print(" but got 0x");
+                    Serial.print(byteReceived, HEX);
+                    Serial.println("instead");
+                }
+                else
+                {
+                    answerFrame[idx] = byteReceived;
+                    // for reading register: third received byte is data length, read number of bytes accordingly
+                    if (idx == 2 && answerFrame[1] == 0x03)
+                    {
+                        // 5 bytes for address, function, data length, CRC_H, CRC_L
+                        responseLength = 5 + byteReceived;
+                    }
+                    idx++;
+                }
+            }
+        }
+
+        delay(10);
+        Serial.print("Response: 0x");
+        printBytes(answerFrame, responseLength);
+        Serial.print("Tries: ");
+        Serial.println(tries);
+        Serial.print("Bytes received: ");
+        Serial.println(idx);
+        word crc_received = word(answerFrame[responseLength - 2], answerFrame[responseLength - 1]);
+        word crc = calculateCRC(answerFrame, responseLength - 2);
+        if (crc_received != word(crc))
+        {
+            Serial.print("CRC wrong: Expected ");
+            printBytes(crc);
+            Serial.print(" got ");
+            printBytes(crc_received);
+            Serial.println();
+            valid = false;
+            resetAnswerFrame();
+        }
+
+        if (answerFrame[0] == 0)
+        {
+            valid = false;
+        }
+        if(valid)
+        {
+            break;
+        }
+    }
+    return valid;
+}
+
+unsigned int SentecSensorRS485::calculateCRC(byte query[], int length)
+{
+    // Change the last two bytes of the queryFrame to conform to a CRC check
+    // Yes, this is necessary. No, I don't know exactly what it does.
+    unsigned int tmp1, tmp2, flag;
+    tmp1 = 0xFFFF;
+    for (unsigned char i = 0; i < length; i++)
+    {
+        tmp1 = tmp1 ^ query[i];
+        for (unsigned char j = 1; j <= 8; j++)
+        {
+            flag = tmp1 & 0x0001;
+            tmp1 >>= 1;
+            if (flag)
+                tmp1 ^= 0xA001;
+        }
+    }
+    // Reverse byte order.
+    tmp2 = tmp1 >> 8;
+    tmp1 = (tmp1 << 8) | tmp2;
+    tmp1 &= 0xFFFF;
+    // change last two query bytes
+    query[length + 1] = tmp1;
+    query[length] = tmp1 >> 8;
+
+    return tmp1; // the returned value is already swapped - CRC_L byte is first & CRC_H byte is last
+}
+
+void SentecSensorRS485::printBytes(byte *data, int length)
+{
+    // prints 8-bit data in hex with leading zeroes
+    char tmp[16];
+    for (int i = 0; i < length; i++)
+    {
+        sprintf(tmp, "%.2X", data[i]);
+        // sprintf(tmp, "0x%.2X",data[i]);
+        Serial.print(tmp);
+        Serial.print(" ");
+    }
+    Serial.println();
+}
+
+void SentecSensorRS485::printBytes(word data)
+{
+    char tmp[10];
+    sprintf(tmp, "0x%.2X", data >> 8);
+    Serial.print(tmp);
+    sprintf(tmp, "%.2X", data & 0xFF);
+    // sprintf(tmp, "0x%.2X",data[i]);
+    Serial.print(tmp);
+}
+
+word SolarRadiationSensor::getSolarRadiation()
+{
+    readRegister(0, 1);
+    glob = word(answerFrame[3], answerFrame[4]);
+    Serial.print("Global solar radiation: ");
+    Serial.print(glob, DEC);
+    Serial.println(" W/m^2");
+    return glob;
+}
+
+String SolarRadiationSensor::getSolarRadiationStr()
+{
+    return getValueStr((int)glob);
+}
+
+void RainGaugeSensor::queryTime()
+{
+    // get time setting of the rain gauge
+    readRegister(0x34, 0x03);
+}
+
+void RainGaugeSensor::setTime()
+{
+    // set time of the rain gauge
+    byte query[15];
+    query[0] = address;
+    query[1] = 0x10; // function code
+    query[2] = 0x00; // register start address high
+    query[3] = 0x34; // register start address low
+    query[4] = 0x00; // register length high
+    query[5] = 0x03; // register length low
+    query[6] = 0x06; // data length
+    // TODO parse timestamp... hard-code a random date for now.
+    query[7] = 0x20;  // year
+    query[8] = 0x04;  // month
+    query[9] = 0x03;  // day
+    query[10] = 0x17; // hours
+    query[11] = 0x06; // minutes
+    query[12] = 0x28; // seconds
+    query[13] = 0x00; // CRC low
+    query[14] = 0x00; // CRC high
+
+    calculateCRC(query, sizeof(query) - 2);
+    Serial.print("Query (set time):    ");
+    printBytes(query, 15);
+    write(query, sizeof(query));
+    getResponse();
+}
+
+void RainGaugeSensor::resetPrecipitation()
+{
+    // clears rainfall rata from the rain gauge
+    // delay resetting after the last register reading
+    delay(100);
+    Serial.println("Resetting precipitation sum");
+    writeRegister(0x00, 0x5A);
+    // TODO check response: matches the sent message exactly
+}
+
+void RainGaugeSensor::resetSensor()
+{
+    // no clue what the difference between this one and the previous one is...
+    //   and the manual is not helpful, of course
+    delay(100);
+    Serial.println("Resetting precipitation sum");
+    writeRegister(0x37, 0x03);
+    // TODO check response: matches the sent message exactly
+}
+
+void RainGaugeSensor::getPrecipitation()
+{
+    // get all precipitation statistics
+    readRegister(0, 0x0A);
+    word prcpToday = word(answerFrame[3], answerFrame[4]);
+    word prcpInstantaneous = word(answerFrame[5], answerFrame[6]); // TODO I'm pretty sure this value is bullshit (0.1 mm resolution)
+    word prcpYesterday = word(answerFrame[7], answerFrame[8]);
+    word prcpTotal = word(answerFrame[9], answerFrame[10]);
+    word prcpHourly = word(answerFrame[11], answerFrame[12]);
+    word prcpLastHourly = word(answerFrame[13], answerFrame[14]);
+    word prcp24max = word(answerFrame[15], answerFrame[16]);
+    word prcp24maxPeriod = word(answerFrame[17], answerFrame[18]);
+    word prcp24min = word(answerFrame[19], answerFrame[20]);
+    word prcp24minPeriod = word(answerFrame[21], answerFrame[22]);
+
+    Serial.print("Today: ");
+    Serial.print(prcpToday / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("Instantaneous: ");
+    Serial.print(prcpInstantaneous / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("Total: ");
+    Serial.print(prcpTotal / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("Hourly: ");
+    Serial.print(prcpHourly / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("24 max: ");
+    Serial.print(prcp24max / 10.0, 1);
+    Serial.println(" mm");
+    Serial.print("24 min: ");
+    Serial.print(prcp24min / 10.0, 1);
+    Serial.println(" mm");
+}
+
+word RainGaugeSensor::getInstantaneousPrecipitation()
+{
+    // gets tips of scale since the last reset, i.e. total precipitation (I THINK)
+    //   manual says this is current precipitation - is it?
+    readRegister(0, 0x01);
+    precipitation = word(answerFrame[3], answerFrame[4]);
+    Serial.print("Precipitation: ");
+    Serial.print(precipitation / 10.0, 1);
+    Serial.println(" mm");
+    // resetPrecipitation();
+    return precipitation;
+}
+
+String RainGaugeSensor::getPrecipitationStr()
+{
+    return getValueStr((float)(precipitation / 10.0));
+}
+
+word SoilMoistureSensor::getMoistureTemp()
+{
+    readRegister(0, 2); // start register at 0, read 2 variables (vwc, soil temp)
+    moistureRaw = word(answerFrame[3], answerFrame[4]);
+    if (answerFrame[5] < 0x80)
+    {
+        temperatureRaw = word(answerFrame[5], answerFrame[6]);
+    }
+    else
+    {
+        temperatureRaw = word(answerFrame[5], answerFrame[6]) - 65536;
+    }
+    Serial.print("Soil moisture: ");
+    Serial.print((moistureRaw - moistureOffset) / 10.0, 1);
+    Serial.println(" %");
+    Serial.print("Temperature: ");
+    Serial.print((temperatureRaw - temperatureOffset) / 10.0 , 1);
+    Serial.println(" °C");
+    return temperatureRaw;
+}
+
+float SoilMoistureSensor::getMoisture(){
+   return (float)((moistureRaw - moistureOffset) / 10.0);
+}
+
+String SoilMoistureSensor::getMoistureStr()
+{
+    return getValueStr((float)((moistureRaw - moistureOffset) / 10.0));
+}
+
+String SoilMoistureSensor::getTemperatureStr()
+{
+    return getValueStr((float)((temperatureRaw - temperatureOffset) / 10.0));
+}
diff --git a/code-snippets/client/main_client_station/lib/rs485/SentecSensors.h b/code-snippets/client/main_client_station/lib/rs485/SentecSensors.h
new file mode 100644
index 0000000000000000000000000000000000000000..b21f8c56c2af6353d8a06aeb41c2a1ded08449ac
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/rs485/SentecSensors.h
@@ -0,0 +1,88 @@
+#ifndef SENTECSENSORS_H
+#define SENTECSENSORS_H
+
+#include <Arduino.h>
+#include <SoftwareSerial.h>
+
+class SentecSensorRS485
+{
+public:
+    byte address;
+    uint8_t serialCommunicationControlPin = 6;
+    SoftwareSerial *RS485;
+    byte answerFrame[10];
+    // TODO use valid flag to log None
+    bool valid = false;
+    SentecSensorRS485(SoftwareSerial *ser, byte add);
+
+    void write(byte queryFrame[], int length);
+    String getValueStr(float value);
+    String getValueStr(int value);
+    void queryAddress();
+    void readRegister(int registerStartAddress);
+    void readRegister(int registerStartAddress, int registerLength);
+    void writeRegister(int registerAddress, int value);
+    void setAddress(byte add);
+    void resetAnswerFrame();
+    bool getResponse();
+    unsigned int calculateCRC(byte query[], int length);
+    void printBytes(byte *data, int length);
+    void printBytes(word data);
+};
+
+class SolarRadiationSensor : public SentecSensorRS485
+{
+public:
+    using SentecSensorRS485::SentecSensorRS485;
+    // global radiation [W/m^2]
+    word glob = 0;
+
+    word getSolarRadiation();
+    String getSolarRadiationStr();
+};
+
+class RainGaugeSensor : public SentecSensorRS485
+{
+public:
+    using SentecSensorRS485::SentecSensorRS485;
+    // precipitation values [mm]
+    word precipitation = 0;     // prcp since reset? (I THINK!!!)
+    word prcpToday = 0;         // prcp since 00:00
+    word prcpInstantaneous = 0; // rainfall since last query - THIS IS OFF
+    word prcpYesterday = 0;     // prcp sum yesterday 00:00-24:00
+    word prcpTotal = 0;         // prcp since sensor power-up
+    word prcpHourly = 0;        // prcp this hour
+    word prcpLastHourly = 0;    // prcp last hour
+    word prcp24max = 0;         // max mm in the last 24h
+    word prcp24maxPeriod = 0;   // time when max prcp was measured
+    word prcp24min = 0;         // min mm in the last 24h
+    word prcp24minPeriod = 0;   // time when min prcp was measured
+
+    void queryTime();
+    void setTime();
+    void resetPrecipitation();
+    void resetSensor();
+    void getPrecipitation();
+    word getInstantaneousPrecipitation();
+    String getPrecipitationStr();
+};
+
+class SoilMoistureSensor : public SentecSensorRS485
+{
+public:
+    using SentecSensorRS485::SentecSensorRS485;
+    // vwc: volumetric water content [%]
+    word moistureRaw = 0;
+    int moistureOffset = 0;
+    // soil temperature [deg C]
+    int temperatureRaw = 0;
+    int temperatureOffset = 0;
+
+    word getMoistureTemp();
+    float getMoisture();
+    String getMoistureStr();
+    String getTemperatureStr();
+};
+
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/rs485/forte_sensor.hpp b/code-snippets/client/main_client_station/lib/rs485/forte_sensor.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4ca8e39602ae03062a79a2acb8c25e634521f8b8
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/rs485/forte_sensor.hpp
@@ -0,0 +1,15 @@
+#ifndef _FORTE_SENSOR
+#define _FORTE_SENSOR
+
+// #include "Message.hpp"
+template <class T>
+class Forte_Sensor {
+  public:
+	virtual T read_data() = 0;
+	virtual void setup() = 0;
+	//virtual Message build_message() = 0;
+
+  private:
+};
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/rs485/rs485.cpp b/code-snippets/client/main_client_station/lib/rs485/rs485.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ca675cbb4e9e823c7aa7942599e2ad62582b1bda
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/rs485/rs485.cpp
@@ -0,0 +1,29 @@
+#include "rs485.hpp"
+// RS485 control
+#define RXPin 0 // Serial Receive pin
+#define TXPin 1 // Serial Transmit pin
+
+//Configer sensors
+SoftwareSerial RS485Serial1(0, 1);
+SolarRadiationSensor solarSensor1(&RS485Serial1, 1);
+RainGaugeSensor rainGauge1 = RainGaugeSensor(&RS485Serial1, 2);               //Give 2 Sensor Adress 2
+SoilMoistureSensor soilSensor31 = SoilMoistureSensor(&RS485Serial1, 3);       //.....
+SoilMoistureSensor soilSensor41 = SoilMoistureSensor(&RS485Serial1, 4);
+SoilMoistureSensor soilSensor51 = SoilMoistureSensor(&RS485Serial1, 5);
+
+void Forte_RS485::setup(){
+     // configure the pins to be output only
+    pinMode(SERIAL_COMMUNICATION_CONTROL_PIN, OUTPUT);
+    gpio_set_direction(GPIO_NUM_3, GPIO_MODE_OUTPUT);
+     // Set data rates: Serial baud rate has to be WAY HIGHER than RS485Serial!
+    Serial.begin(115200);
+    RS485Serial1.begin(4800);   
+}
+
+out_data_rs485 Forte_RS485::read_data(){
+    data.solarRadiation=solarSensor1.getSolarRadiation();
+    data.moisture=soilSensor41.getMoisture();
+    data.moisture_temperature=soilSensor41.getMoistureTemp();
+    data.precipitation=rainGauge1.getInstantaneousPrecipitation();
+    return data;
+}
diff --git a/code-snippets/client/main_client_station/lib/rs485/rs485.hpp b/code-snippets/client/main_client_station/lib/rs485/rs485.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f4d64585dc7a08e8dc522876fdeab18eb912b92e
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/rs485/rs485.hpp
@@ -0,0 +1,33 @@
+#ifndef _RS485
+#define _RS485
+
+#include "SPI.h"
+// RTC (I2C)
+#include "RTClib.h"
+#include "forte_sensor.hpp"
+#include "SentecSensors.h"
+
+#define SERIAL_COMMUNICATION_CONTROL_PIN 3
+
+// power control
+#define TransistorPin 10 // controls the transistor for power consumption (on/off)
+
+struct out_data_rs485{
+    word solarRadiation;
+    float moisture;
+    word moisture_temperature;
+    word precipitation;
+};
+
+
+class Forte_RS485 : public Forte_Sensor<out_data_rs485> {
+
+    public:
+	    void setup();
+	    out_data_rs485 read_data();
+    
+    private:
+     out_data_rs485 data;
+};
+
+#endif 
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/lib/time/src/Time.cpp b/code-snippets/client/main_client_station/lib/time/src/Time.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8fdf4b4decd253a197cda680a6613529daa2b127
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/time/src/Time.cpp
@@ -0,0 +1,28 @@
+#include "Time.hpp"
+
+#include <utility>
+void Time::setTime(long epoch, int ms)
+{
+	this->rtc.setTime(epoch, ms);
+}
+tm Time::getTimeStruct()
+{
+	return this->rtc.getTimeStruct();
+}
+String Time::getDateTime(bool mode)
+{
+	return this->rtc.getDateTime(mode);
+}
+String Time::getTime(String format)
+{
+	return this->rtc.getTime(std::move(format));
+}
+long Time::getEpochSeconds()
+{
+	return this->rtc.getEpoch();
+}
+
+long Time::getMillis()
+{
+	return this->rtc.getMillis();
+}
diff --git a/code-snippets/client/main_client_station/lib/time/src/Time.hpp b/code-snippets/client/main_client_station/lib/time/src/Time.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d53adb159f8f5deb092bcffa3977f550f59fcd1d
--- /dev/null
+++ b/code-snippets/client/main_client_station/lib/time/src/Time.hpp
@@ -0,0 +1,75 @@
+#ifndef ESPTIME
+#define ESPTIME
+
+#include <ESP32Time.h>
+
+class Time {
+  public:
+	static Time &getInstance()
+	{
+		static Time instance; // Guaranteed to be destroyed.
+		                      // Instantiated on first use.
+		return instance;
+	}
+
+	/*!
+	@brief  set the internal RTC time
+	@param  epoch
+	        epoch time in seconds
+	@param  ms
+	        microseconds (optional)
+	*/
+	void setTime(long epoch, int ms = 0);
+
+	/*!
+	@brief  get the internal RTC time as a tm struct
+	*/
+	tm getTimeStruct();
+
+	/*!
+	@brief  get the time and date as an Arduino String object
+	@param  mode
+	        true = Long date format
+	        false = Short date format
+	*/
+	String getDateTime(bool mode);
+
+	/*!
+	@brief  get the time as an Arduino String object with the specified format
+	@param	format
+	        time format
+	        http://www.cplusplus.com/reference/ctime/strftime/
+	*/
+	String getTime(String format = "%H:%M:%S");
+
+	/*!
+	@brief  get the current epoch seconds as long
+	*/
+	long getEpochSeconds();
+
+	/*!
+	@brief  get the current milliseconds as long
+	*/
+	long getMillis();
+
+  private:
+	Time() {} // Constructor? (the {} brackets) are needed here.
+
+	ESP32Time rtc = ESP32Time{};
+
+	// C++ 11
+	// =======
+	// We can use the better technique of deleting the methods
+	// we don't want.
+  public:
+	Time(Time const &) = delete;
+	void operator=(Time const &) = delete;
+
+	// Note: Scott Meyers mentions in his Effective Modern
+	//       C++ book, that deleted functions should generally
+	//       be public as it results in better error messages
+	//       due to the compilers behavior to check accessibility
+	//       before deleted status
+};
+
+#endif
\ No newline at end of file
diff --git a/code-snippets/client/main_client_station/platformio.ini b/code-snippets/client/main_client_station/platformio.ini
new file mode 100644
index 0000000000000000000000000000000000000000..e372635876a999ffe1f6f5fa720e6888020d1b13
--- /dev/null
+++ b/code-snippets/client/main_client_station/platformio.ini
@@ -0,0 +1,23 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:esp32-c3-devkitm-1]
+platform = espressif32
+board = esp32-c3-devkitm-1
+;board = esp32dev
+framework = arduino
+lib_deps = 
+	plerup/EspSoftwareSerial@^6.16.1
+	4-20ma/ModbusMaster@^2.0.1
+	adafruit/RTClib@^2.1.1
+	sensirion/arduino-sht@^1.2.2
+	robtillaart/SHT85@^0.3.2
+	fbiego/ESP32Time@^2.0.0
+monitor_speed = 115200
diff --git a/code-snippets/client/main_client_station/src/main.cpp b/code-snippets/client/main_client_station/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1b54f6cb59d64730a6e366509db2c849d7a28f35
--- /dev/null
+++ b/code-snippets/client/main_client_station/src/main.cpp
@@ -0,0 +1,40 @@
+#include "Arduino.h"
+#include "rs485.hpp"
+#include "ESPNow.hpp"
+
+Forte_RS485 rs485;
+void setup(){
+    rs485.setup();
+    espnow_setup();
+}
+
+void loop()
+{
+  out_data_rs485 data_rs485= rs485.read_data();
+  Serial.println("***********************************************************");
+  Serial.print("Moisture: ");
+  Serial.println(data_rs485.moisture);
+  Serial.print("Moisture Temperature: ");
+  Serial.println(data_rs485.moisture_temperature);
+  Serial.print("Solar: ");
+  Serial.println(data_rs485.solarRadiation);
+  Serial.print("Rain mm: ");
+  Serial.println(data_rs485.precipitation);
+
+    //send the data
+    //moistere 1
+    //moisture_temperature 2
+    //solarRadiation 3
+    //precipitation 4
+
+   Message new_data = Message{};
+   new_data.add_data(data_rs485.moisture,1);
+   new_data.add_data(data_rs485.moisture_temperature, 2);
+   new_data.add_data(data_rs485.solarRadiation,3);
+   new_data.add_data(data_rs485.precipitation,4);
+   espnow_send_message(new_data);
+  
+
+  delay(5000);
+
+}
diff --git a/code-snippets/client/main_client_station/test/README b/code-snippets/client/main_client_station/test/README
new file mode 100644
index 0000000000000000000000000000000000000000..9b1e87bc67c90e7f09a92a3e855444b085c655a6
--- /dev/null
+++ b/code-snippets/client/main_client_station/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Test Runner and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html