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