From bb49b9f867dff85052bb1c46efcef25fe77d7915 Mon Sep 17 00:00:00 2001 From: Zoe Pfister <zoe.pfister@uibk.ac.at> Date: Fri, 10 Mar 2023 11:46:21 +0100 Subject: [PATCH] Renamed Sentec Sensors, made RS485 serial connection a singleton shared pointer, allow resetting of precipitation, added error type for non-ability to reset precipitation --- .../SentecSensors/SentecRainGaugeSensor.cpp | 111 +++++--- .../SentecSensors/SentecRainGaugeSensor.h | 37 +-- client/libs/SentecSensors/SentecSensors.cpp | 266 ------------------ client/libs/SentecSensors/SentecSensors.h | 54 ---- .../libs/SentecSensors/SentecSensorsRS485.cpp | 248 ++++++++++++++++ .../libs/SentecSensors/SentecSensorsRS485.h | 60 ++++ .../SentecSolarRadiationSensor.cpp | 38 ++- .../SentecSolarRadiationSensor.h | 30 +- client/libs/rs485/RS485HardwareSerial.h | 47 ++++ client/libs/rs485/rs485.cpp | 84 +++--- client/libs/rs485/rs485.hpp | 9 +- shared-libs/DataTransfer/ErrorTypes.cpp | 66 ++--- shared-libs/DataTransfer/ErrorTypes.h | 31 +- 13 files changed, 573 insertions(+), 508 deletions(-) delete mode 100644 client/libs/SentecSensors/SentecSensors.cpp delete mode 100644 client/libs/SentecSensors/SentecSensors.h create mode 100644 client/libs/SentecSensors/SentecSensorsRS485.cpp create mode 100644 client/libs/SentecSensors/SentecSensorsRS485.h create mode 100644 client/libs/rs485/RS485HardwareSerial.h diff --git a/client/libs/SentecSensors/SentecRainGaugeSensor.cpp b/client/libs/SentecSensors/SentecRainGaugeSensor.cpp index b785fd7..befb26d 100644 --- a/client/libs/SentecSensors/SentecRainGaugeSensor.cpp +++ b/client/libs/SentecSensors/SentecRainGaugeSensor.cpp @@ -4,51 +4,70 @@ #include "SentecRainGaugeSensor.h" -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 -} -out_data_rain_gauge RainGaugeSensor::getInstantaneousPrecipitation() { - // gets tips of scale since the last reset, i.e. total precipitation (I THINK) - // manual says this is current precipitation - is it? - auto error = readRegister(0, 0x01); - precipitation = word(answerFrame[3], answerFrame[4]); - - ESP_LOGI("RainGauge", "Precipitation: %.1f mm", precipitation / 10.0); - // resetPrecipitation(); - return {static_cast<float>(precipitation), error}; -} -String RainGaugeSensor::getPrecipitationStr() { - return getValueStr((float) (precipitation / 10.0)); -} - -SensorInformation RainGaugeSensor::getSensorInformation() const { - return SensorInformation(HardwareName::SEM404, SensorProtocol::RS485); -} -std::list<Message> RainGaugeSensor::buildMessages() { - auto messages = std::list<Message>(); - auto data = getInstantaneousPrecipitation(); - Measurement precipitationMeasurement{data.precipitation, address, NO_I2C_ADDRESS, - MeasurementType::PRECIPITATION, data.precipitationError}; - messages.emplace_back(Message{precipitationMeasurement, getSensorInformation(), - Time::getInstance().getEpochSeconds()}); - return messages; -} -out_data_rain_gauge RainGaugeSensor::readData() { - return getInstantaneousPrecipitation(); -} -void RainGaugeSensor::setup() { +static const char *TAG = "SEM404"; +bool SEM404::isAnswerFrameEqualToQueryFrame(byte answerFrame[8], byte queryFrame[8]) { + for (int i = 0; i < 8; i++) { + if (answerFrame[i] != queryFrame[i]) { + ESP_LOGD(TAG, "Response: 0x%s", formatBytes(answerFrame, 8).c_str()); + ESP_LOGD(TAG, "Response: 0x%s", formatBytes(queryFrame, 8).c_str()); + ESP_LOGE("TEST", "Error: answerFrame[%d] != ret.writtenQuery[%d]", i, i); + return false; + } + } + return true; } + +ErrorType SEM404::resetPrecipitation() { + // clears rainfall rata from the rain gauge + // delay resetting after the last register reading + delay(100); + Serial.println("Resetting precipitation sum"); + auto writeRegisterReturn = writeRegister(0x00, 0x5A); + + // other errors like not connected will be shared when reading out the sensor + if (!isAnswerFrameEqualToQueryFrame(answerFrame, writeRegisterReturn.writtenQuery)) { + ESP_LOGE(TAG, "Could not reset precipitation"); + return ErrorType::SEM404_COULD_NOT_RESET_PRECIPITATION; + } + + return writeRegisterReturn.errorType; +} + +void SEM404::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 +} +out_data_rain_gauge SEM404::getInstantaneousPrecipitation() { + // gets tips of scale since the last reset, i.e. total precipitation (I THINK) + // manual says this is current precipitation - is it? + auto error = readRegister(0, 0x01); + precipitation = word(answerFrame[3], answerFrame[4]); + + ESP_LOGI("RainGauge", "Precipitation: %s mm", getPrecipitationStr().c_str()); + // resetPrecipitation(); + return {static_cast<float>(precipitation), error}; +} +String SEM404::getPrecipitationStr() { + return getValueStr((float)(precipitation / 10.0)); +} + +SensorInformation SEM404::getSensorInformation() const { + return {HardwareName::SEM404, SensorProtocol::RS485}; +} +std::list<Message> SEM404::buildMessages() { + auto messages = std::list<Message>(); + auto data = getInstantaneousPrecipitation(); + Measurement precipitationMeasurement{data.precipitation, address, NO_I2C_ADDRESS, MeasurementType::PRECIPITATION, + data.precipitationError}; + messages.emplace_back(precipitationMeasurement, getSensorInformation(), Time::getInstance().getEpochSeconds()); + return messages; +} +out_data_rain_gauge SEM404::readData() { + return getInstantaneousPrecipitation(); +} +void SEM404::setup() {} diff --git a/client/libs/SentecSensors/SentecRainGaugeSensor.h b/client/libs/SentecSensors/SentecRainGaugeSensor.h index c329fb0..0425285 100644 --- a/client/libs/SentecSensors/SentecRainGaugeSensor.h +++ b/client/libs/SentecSensors/SentecRainGaugeSensor.h @@ -5,34 +5,35 @@ #ifndef CLIENT_CENTRAL_MAST_SENTECRAINGAUGESENSOR_H #define CLIENT_CENTRAL_MAST_SENTECRAINGAUGESENSOR_H -#include "SentecSensors.h" +#include "SentecSensorsRS485.h" #include <ForteSensor.hpp> #include <Message.hpp> struct out_data_rain_gauge { - float precipitation; - ErrorType precipitationError; + float precipitation; + ErrorType precipitationError; }; -class RainGaugeSensor - : public SentecSensorRS485, ForteSensor<out_data_rain_gauge> { - public: - using SentecSensorRS485::SentecSensorRS485; - // precipitation values [mm] - unsigned int precipitation = 0; // prcp since reset? (I THINK!!!) +class SEM404 : public SentecSensorRS485, ForteSensor<out_data_rain_gauge> { + public: + using SentecSensorRS485::SentecSensorRS485; + SEM404() : SentecSensorRS485(0) {} + // precipitation values [mm] + unsigned int precipitation = 0; // prcp since reset? (I THINK!!!) - void resetPrecipitation(); + ErrorType resetPrecipitation(); - void resetSensor(); + void resetSensor(); - out_data_rain_gauge getInstantaneousPrecipitation(); + out_data_rain_gauge getInstantaneousPrecipitation(); - String getPrecipitationStr(); + String getPrecipitationStr(); - void setup() override; - out_data_rain_gauge readData() override; - std::list<Message> buildMessages() override; - [[nodiscard]] SensorInformation getSensorInformation() const override; + void setup() override; + out_data_rain_gauge readData() override; + std::list<Message> buildMessages() override; + [[nodiscard]] SensorInformation getSensorInformation() const override; + bool isAnswerFrameEqualToQueryFrame(byte *answerFrame, byte *queryFrame); }; -#endif //CLIENT_CENTRAL_MAST_SENTECRAINGAUGESENSOR_H +#endif // CLIENT_CENTRAL_MAST_SENTECRAINGAUGESENSOR_H diff --git a/client/libs/SentecSensors/SentecSensors.cpp b/client/libs/SentecSensors/SentecSensors.cpp deleted file mode 100644 index 090a0d5..0000000 --- a/client/libs/SentecSensors/SentecSensors.cpp +++ /dev/null @@ -1,266 +0,0 @@ -#include "NoDataAvailableException.hpp" -#include "SentecRainGaugeSensor.h" -#include "SentecSolarRadiationSensor.h" -#include <SentecSensors.h> - -/*************************************** - * RS485 SENSOR READOUT - ****************************************/ -static const char *TAG = "SENTEC"; - -SentecSensorRS485::SentecSensorRS485(HardwareSerial *ser, byte add) { - address = add; - RS485 = ser; -} - -SentecSensorRS485::SentecSensorRS485(HardwareSerial *ser, - byte add, - uint8_t serialControlPin) { - address = add; - RS485 = ser; - serialCommunicationControlPin = serialControlPin; -} - -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"); - } -} - -ErrorType 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 - ErrorType tmp = readRegister(word(0x07, 0xD0), 2); - address = tmp_addr; // set the original address back - return tmp; -} - -ErrorType SentecSensorRS485::readRegister(int registerStartAddress) { - return readRegister(registerStartAddress, 1); -} - -ErrorType -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 - return getResponse(); -} - -ErrorType 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); - ESP_LOGD(TAG, "Query (settings): "); - printBytes(query, 8); - write(query, sizeof(query)); - return getResponse(); -} - -ErrorType SentecSensorRS485::setAddress(byte add) { - // change the address of a sensor - ErrorType tmp = writeRegister(word(0x07, 0xD0), add); - if (tmp == ErrorType::DATA_OK) - address = add; - // TODO check response: matches the sent message exactly - return tmp; -} - -void SentecSensorRS485::resetAnswerFrame() { - for (unsigned char &i: answerFrame) { - i = 0; - } -} - -ErrorType SentecSensorRS485::getResponse() { - // reads the response of a sensor - ErrorType returnCode = ErrorType::DATA_OK; - 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 = 1; // #editet to q - size_t tries = 1; - // it doesn't seem to help to request multiple times if first time goes wrong - 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) { - ESP_LOGE(TAG, - "Invalid byte. First byte needs to be address 0x%02X but got 0x%02Xinstead"); - } 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 code, data length, CRC_H, CRC_L - responseLength = 5 + byteReceived; - } - idx++; - } - } - } - ESP_LOGD(TAG, - "-----------------------------------------------------------------------------------------"); - ESP_LOGD(TAG, - "Response: 0x%s", - formatBytes(answerFrame, responseLength).c_str()); - // ESP_LOGD(TAG, "Tries: %d", tries); - // ESP_LOGD(TAG, "Bytes received: %d", idx); - if (answerFrame[0] == 0) { - ESP_LOGE(TAG, "Check sensor connection. First byte is 0x00."); - valid = false; - returnCode = ErrorType::SENSOR_NOT_CONNECTED; - } else if (idx < responseLength) { - ESP_LOGE(TAG, - "Response too short: %d bytes < %d bytes. Unfinished transmission.", - idx, - responseLength); - valid = false; - returnCode = ErrorType::CONNECTION_ENDED_PREMATURELY; - } - word crc_received = - word(answerFrame[responseLength - 2], answerFrame[responseLength - 1]); - word crc = calculateCRC(answerFrame, responseLength - 2); - if (valid && crc_received != word(crc)) { - ESP_LOGE(TAG, - "CRC wrong: Expected 0x%s got 0x%s", - formatBytes(crc).c_str(), - formatBytes(crc_received).c_str()); - valid = false; - returnCode = ErrorType::WRONG_CRC; - } - // breaking after first successfull try - if (returnCode == ErrorType::DATA_OK) { - break; - } - } - - // ESP_LOGE(TAG, "Returncode: %d", returnCode); - return returnCode; -} - -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); -} - -std::string SentecSensorRS485::formatBytes(byte *data, int length) { - // pls don't hate me for building strings like that. - // I also wish that I would be good at programming - std::string tmp; - // prints 8-bit data in hex with leading zeroes - char buff[4]; - for (int i = 0; i < length; i++) { - snprintf(buff, sizeof(buff), "%02X ", data[i]); - tmp += buff; - } - return tmp; -} - -std::string SentecSensorRS485::formatBytes(word data) { - byte arr[2] = {static_cast<byte>(data >> 8), static_cast<byte>(data & 0xFF)}; - return formatBytes(arr, 2); -} - diff --git a/client/libs/SentecSensors/SentecSensors.h b/client/libs/SentecSensors/SentecSensors.h deleted file mode 100644 index fbef376..0000000 --- a/client/libs/SentecSensors/SentecSensors.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef SENTECSENSORS_H -#define SENTECSENSORS_H - -#include <Arduino.h> -#include <ErrorTypes.h> -#include <HardwareNames.h> -#include <SensorInformation.hpp> - -class SentecSensorRS485 { - public: - byte address; - uint8_t serialCommunicationControlPin = 19; - HardwareSerial *RS485; - byte answerFrame[10]; - // TODO use valid flag to log None - bool valid = false; - - SentecSensorRS485(HardwareSerial *ser, byte add); - - SentecSensorRS485(HardwareSerial *ser, byte add, uint8_t serialControlPin); - - void write(byte queryFrame[], int length); - - String getValueStr(float value); - - String getValueStr(int value); - - ErrorType queryAddress(); - - ErrorType readRegister(int registerStartAddress); - - ErrorType readRegister(int registerStartAddress, int registerLength); - - ErrorType writeRegister(int registerAddress, int value); - - ErrorType setAddress(byte add); - - void resetAnswerFrame(); - - ErrorType getResponse(); - - unsigned int calculateCRC(byte query[], int length); - - void printBytes(byte *data, int length); - - void printBytes(word data); - - std::string formatBytes(byte *data, int length); - - std::string formatBytes(word data); - -}; - -#endif diff --git a/client/libs/SentecSensors/SentecSensorsRS485.cpp b/client/libs/SentecSensors/SentecSensorsRS485.cpp new file mode 100644 index 0000000..583c799 --- /dev/null +++ b/client/libs/SentecSensors/SentecSensorsRS485.cpp @@ -0,0 +1,248 @@ +#include "NoDataAvailableException.hpp" +#include <SentecSensorsRS485.h> + +/*************************************** + * RS485 SENSOR READOUT + ****************************************/ +static const char *TAG = "SENTEC"; + +SentecSensorRS485::SentecSensorRS485(byte add) { + address = add; +} + +SentecSensorRS485::SentecSensorRS485(byte add, uint8_t serialControlPin) { + address = add; + serialCommunicationControlPin = serialControlPin; +} + +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 {"null"}; + } +} + +String SentecSensorRS485::getValueStr(int value) { + if (valid) { + return String(value); + } else { + return {"null"}; + } +} + +ErrorType 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 + ErrorType tmp = readRegister(word(0x07, 0xD0), 2); + address = tmp_addr; // set the original address back + return tmp; +} + +ErrorType SentecSensorRS485::readRegister(int registerStartAddress) { + return readRegister(registerStartAddress, 1); +} + +ErrorType 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 + return getResponse(); +} + +WriteRegisterReturnType 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); + ESP_LOGD(TAG, "Query (settings): "); + printBytes(query, 8); + write(query, sizeof(query)); + return {getResponse(), {query[0], query[1], query[2], query[3], query[4], query[5], query[6], query[7]}}; +} + +ErrorType SentecSensorRS485::setAddress(byte add) { + // change the address of a sensor + WriteRegisterReturnType tmp = writeRegister(word(0x07, 0xD0), add); + if (tmp.errorType == ErrorType::DATA_OK) + address = add; + // TODO check response: matches the sent message exactly + return tmp.errorType; +} + +void SentecSensorRS485::resetAnswerFrame() { + for (unsigned char &i : answerFrame) { + i = 0; + } +} + +ErrorType SentecSensorRS485::getResponse() { + // reads the response of a sensor + ErrorType returnCode = ErrorType::DATA_OK; + 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 = 2000; + const int retries = 1; // #editet to q + size_t tries = 1; + // it doesn't seem to help to request multiple times if first time goes wrong + 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) { + ESP_LOGE(TAG, "Invalid byte. First byte needs to be address 0x%02X but got 0x%02Xinstead"); + } 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 code, data length, CRC_H, CRC_L + responseLength = 5 + byteReceived; + } + idx++; + } + } + } + ESP_LOGD(TAG, "-----------------------------------------------------------------------------------------"); + ESP_LOGD(TAG, "Response: 0x%s", formatBytes(answerFrame, responseLength).c_str()); + // ESP_LOGD(TAG, "Tries: %d", tries); + // ESP_LOGD(TAG, "Bytes received: %d", idx); + if (answerFrame[0] == 0) { + ESP_LOGE(TAG, "Check sensor connection. First byte is 0x00."); + valid = false; + returnCode = ErrorType::SENSOR_NOT_CONNECTED; + } else if (idx < responseLength) { + ESP_LOGE(TAG, "Response too short: %d bytes < %d bytes. Unfinished transmission.", idx, responseLength); + valid = false; + returnCode = ErrorType::CONNECTION_ENDED_PREMATURELY; + } + word crc_received = word(answerFrame[responseLength - 2], answerFrame[responseLength - 1]); + word crc = calculateCRC(answerFrame, responseLength - 2); + if (valid && crc_received != word(crc)) { + ESP_LOGE(TAG, "CRC wrong: Expected 0x%s got 0x%s", formatBytes(crc).c_str(), + formatBytes(crc_received).c_str()); + valid = false; + returnCode = ErrorType::WRONG_CRC; + } + // breaking after first successfull try + if (returnCode == ErrorType::DATA_OK) { + break; + } + } + + // ESP_LOGE(TAG, "Returncode: %d", returnCode); + return returnCode; +} + +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); +} + +std::string SentecSensorRS485::formatBytes(byte *data, int length) { + // pls don't hate me for building strings like that. + // I also wish that I would be good at programming + std::string tmp; + // prints 8-bit data in hex with leading zeroes + char buff[4]; + for (int i = 0; i < length; i++) { + snprintf(buff, sizeof(buff), "%02X ", data[i]); + tmp += buff; + } + return tmp; +} + +std::string SentecSensorRS485::formatBytes(word data) { + byte arr[2] = {static_cast<byte>(data >> 8), static_cast<byte>(data & 0xFF)}; + return formatBytes(arr, 2); +} diff --git a/client/libs/SentecSensors/SentecSensorsRS485.h b/client/libs/SentecSensors/SentecSensorsRS485.h new file mode 100644 index 0000000..bedb9b9 --- /dev/null +++ b/client/libs/SentecSensors/SentecSensorsRS485.h @@ -0,0 +1,60 @@ +#ifndef SENTECSENSORS_H +#define SENTECSENSORS_H + +#include <Arduino.h> +#include <ErrorTypes.h> +#include <HardwareNames.h> +#include <RS485HardwareSerial.h> +#include <SensorInformation.hpp> +#include <memory> + +struct WriteRegisterReturnType { + ErrorType errorType; + byte writtenQuery[8]; +}; + +class SentecSensorRS485 { + public: + byte address; + uint8_t serialCommunicationControlPin = 19; + std::shared_ptr<HardwareSerial> RS485 = RS485HardwareSerial::getInstance().getRS485Serial(); + byte answerFrame[10] = {0}; + // TODO use valid flag to log None + bool valid = false; + + SentecSensorRS485(byte add); + + SentecSensorRS485(byte add, uint8_t serialControlPin); + + void write(byte queryFrame[], int length); + + String getValueStr(float value); + + String getValueStr(int value); + + ErrorType queryAddress(); + + ErrorType readRegister(int registerStartAddress); + + ErrorType readRegister(int registerStartAddress, int registerLength); + + WriteRegisterReturnType writeRegister(int registerAddress, int value); + + ErrorType setAddress(byte add); + + void resetAnswerFrame(); + + ErrorType getResponse(); + + unsigned int calculateCRC(byte query[], int length); + + void printBytes(byte *data, int length); + + void printBytes(word data); + + std::string formatBytes(byte *data, int length); + + std::string formatBytes(word data); +}; + +#endif diff --git a/client/libs/SentecSensors/SentecSolarRadiationSensor.cpp b/client/libs/SentecSensors/SentecSolarRadiationSensor.cpp index 68fd38c..7082a2c 100644 --- a/client/libs/SentecSensors/SentecSolarRadiationSensor.cpp +++ b/client/libs/SentecSensors/SentecSolarRadiationSensor.cpp @@ -4,31 +4,29 @@ #include "SentecSolarRadiationSensor.h" - -String SolarRadiationSensor::getSolarRadiationStr() { - return getValueStr((int) glob); +String SEM228A::getSolarRadiationStr() { + return getValueStr((int)glob); } -void SolarRadiationSensor::setup() { - // TODO: check if rs485 serial is active +void SEM228A::setup() { + // TODO: check if rs485 serial is active } -out_data_solar_radiation SolarRadiationSensor::readData() { - auto error = readRegister(0, 1); - glob = word(answerFrame[3], answerFrame[4]); - ESP_LOGI("SolarRadiation", "Global solar radiation: %d W/m^2", glob); - return {static_cast<float>(glob), error}; +out_data_solar_radiation SEM228A::readData() { + auto error = readRegister(0, 1); + glob = word(answerFrame[3], answerFrame[4]); + ESP_LOGI("SolarRadiation", "Global solar radiation: %d W/m^2", glob); + return {static_cast<float>(glob), error}; } -std::list<Message> SolarRadiationSensor::buildMessages() { - auto messages = std::list<Message>(); - auto data = readData(); - Measurement solarRadiation{data.solarRadiation, address, NO_I2C_ADDRESS, - MeasurementType::SOLAR_RADIATION, data.solarError}; - messages.emplace_back(Message{solarRadiation, getSensorInformation(), - Time::getInstance().getEpochSeconds()}); - return messages; +std::list<Message> SEM228A::buildMessages() { + auto messages = std::list<Message>(); + auto data = readData(); + Measurement solarRadiation{data.solarRadiation, address, NO_I2C_ADDRESS, MeasurementType::SOLAR_RADIATION, + data.solarError}; + messages.emplace_back(Message{solarRadiation, getSensorInformation(), Time::getInstance().getEpochSeconds()}); + return messages; } -SensorInformation SolarRadiationSensor::getSensorInformation() const { - return SensorInformation(HardwareName::SEM228A, SensorProtocol::RS485); +SensorInformation SEM228A::getSensorInformation() const { + return SensorInformation(HardwareName::SEM228A, SensorProtocol::RS485); } diff --git a/client/libs/SentecSensors/SentecSolarRadiationSensor.h b/client/libs/SentecSensors/SentecSolarRadiationSensor.h index de4614d..3942914 100644 --- a/client/libs/SentecSensors/SentecSolarRadiationSensor.h +++ b/client/libs/SentecSensors/SentecSolarRadiationSensor.h @@ -5,28 +5,28 @@ #ifndef CLIENT_CENTRAL_MAST_SENTECSOLARRADIATIONSENSOR_H #define CLIENT_CENTRAL_MAST_SENTECSOLARRADIATIONSENSOR_H +#include "SentecSensorsRS485.h" #include <ForteSensor.hpp> -#include "SentecSensors.h" struct out_data_solar_radiation { - float solarRadiation; - ErrorType solarError; + float solarRadiation; + ErrorType solarError; }; -class SolarRadiationSensor: public SentecSensorRS485, public ForteSensor<out_data_solar_radiation> { - public: - using SentecSensorRS485::SentecSensorRS485; +class SEM228A : public SentecSensorRS485, public ForteSensor<out_data_solar_radiation> { + public: + using SentecSensorRS485::SentecSensorRS485; - // global radiation [W/m^2] - word glob = 0; + SEM228A() : SentecSensorRS485(0) {} + // global radiation [W/m^2] + word glob = 0; - void setup() override; - out_data_solar_radiation readData() override; - std::list<Message> buildMessages() override; - [[nodiscard]] SensorInformation getSensorInformation() const override; - - String getSolarRadiationStr(); + void setup() override; + out_data_solar_radiation readData() override; + std::list<Message> buildMessages() override; + [[nodiscard]] SensorInformation getSensorInformation() const override; + String getSolarRadiationStr(); }; -#endif //CLIENT_CENTRAL_MAST_SENTECSOLARRADIATIONSENSOR_H +#endif // CLIENT_CENTRAL_MAST_SENTECSOLARRADIATIONSENSOR_H diff --git a/client/libs/rs485/RS485HardwareSerial.h b/client/libs/rs485/RS485HardwareSerial.h new file mode 100644 index 0000000..0b6fbd1 --- /dev/null +++ b/client/libs/rs485/RS485HardwareSerial.h @@ -0,0 +1,47 @@ +// +// Created by zoe on 3/8/23. +// + +#ifndef CLIENT_CENTRAL_MAST_RS485HARDWARESERIAL_H +#define CLIENT_CENTRAL_MAST_RS485HARDWARESERIAL_H + + +#include <HardwareSerial.h> +#include <memory> +class RS485HardwareSerial { + public: + static RS485HardwareSerial &getInstance() + { + static RS485HardwareSerial instance; // Guaranteed to be destroyed. + // Instantiated on first use. + return instance; + } + + std::shared_ptr<HardwareSerial> getRS485Serial() { + return RS485Serial; + } + + private: + RS485HardwareSerial() {} // Constructor? (the {} brackets) are needed here. + + const std::shared_ptr<HardwareSerial> RS485Serial = std::make_shared<HardwareSerial>(Serial2); + + + // C++ 11 + // ======= + // We can use the better technique of deleting the methods + // we don't want. + public: + RS485HardwareSerial(RS485HardwareSerial const &) = delete; + void operator=(RS485HardwareSerial 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 //CLIENT_CENTRAL_MAST_RS485HARDWARESERIAL_H diff --git a/client/libs/rs485/rs485.cpp b/client/libs/rs485/rs485.cpp index 20ec4fa..b2ec8b6 100644 --- a/client/libs/rs485/rs485.cpp +++ b/client/libs/rs485/rs485.cpp @@ -2,7 +2,7 @@ #include "SentecRainGaugeSensor.h" #include "SentecSolarRadiationSensor.h" // RS485 control -#define RS485Serial Serial2 +//#define RS485Serial Serial2 #define RXPin 14 // Serial Receive pin #define TXPin 15 // Serial Transmit pin @@ -14,59 +14,61 @@ static const char *TAG = "RS485"; // Configure sensors -SolarRadiationSensor solarSensor(&RS485Serial, 1, RE_DE_PIN); -RainGaugeSensor rainGauge = RainGaugeSensor(&RS485Serial, - 2, - RE_DE_PIN); // Give 2 Sensor Adress 2 +SEM228A solarSensor(1, RE_DE_PIN); +SEM404 rainGauge = SEM404(2, RE_DE_PIN); void Forte_RS485::setup() { - // configure the pins to be output only - pinMode(RE_DE_PIN, OUTPUT); - pinMode(POWER_SWITCH_PIN_12V, OUTPUT); - pinMode(POWER_SWITCH_PIN_5V, OUTPUT); - RS485Serial.begin(4800, SERIAL_8N1, TXPin, RXPin); + RS485Serial = RS485HardwareSerial::getInstance().getRS485Serial(); + // configure the pins to be output only + pinMode(RE_DE_PIN, OUTPUT); + pinMode(POWER_SWITCH_PIN_12V, OUTPUT); + pinMode(POWER_SWITCH_PIN_5V, OUTPUT); + RS485Serial->begin(4800, SERIAL_8N1, TXPin, RXPin); +} + +void Forte_RS485::teardown() { + RS485Serial->end(); } out_data_rs485 Forte_RS485::readData() { - // Power on sensor - digitalWrite(POWER_SWITCH_PIN_12V, HIGH); - digitalWrite(POWER_SWITCH_PIN_5V, HIGH); - // Wait for sensors to power up - // TODO minimize delay - delay(300); - out_data_rs485 output; - unsigned long ts = millis(); - output.solar = solarSensor.readData(); - output.precipitation = rainGauge.readData(); - digitalWrite(POWER_SWITCH_PIN_12V, LOW); - digitalWrite(POWER_SWITCH_PIN_5V, LOW); + powerOnRS485Sensors(); - gpio_hold_en((gpio_num_t) POWER_SWITCH_PIN_12V); - gpio_hold_en((gpio_num_t) POWER_SWITCH_PIN_5V); - return output; + out_data_rs485 output{}; + output.solar = solarSensor.readData(); + output.precipitation = rainGauge.readData(); + + powerOffRS485Sensors(); + return output; } +void Forte_RS485::powerOffRS485Sensors() { + digitalWrite(POWER_SWITCH_PIN_12V, LOW); + digitalWrite(POWER_SWITCH_PIN_5V, LOW); -std::list<Message> Forte_RS485::buildMessages() { - std::list<Message> messages; - out_data_rs485 output = readData(); + gpio_hold_en((gpio_num_t)POWER_SWITCH_PIN_12V); + gpio_hold_en((gpio_num_t)POWER_SWITCH_PIN_5V); +} +void Forte_RS485::powerOnRS485Sensors() { // Power on sensor + digitalWrite(POWER_SWITCH_PIN_12V, HIGH); + digitalWrite(POWER_SWITCH_PIN_5V, HIGH); // Wait for sensors to power up + // TODO minimize delay + delay(2000); +} - Measurement solarRadiation{output.solar.solarRadiation, 1, NO_I2C_ADDRESS, - MeasurementType::SOLAR_RADIATION, - output.solar.solarError}; - Measurement precipitation - {output.precipitation.precipitation, 2, NO_I2C_ADDRESS, - MeasurementType::PRECIPITATION, output.precipitation.precipitationError}; +std::list<Message> Forte_RS485::buildMessages() { + std::list<Message> messages; + out_data_rs485 output = readData(); + Measurement solarRadiation{output.solar.solarRadiation, 1, NO_I2C_ADDRESS, MeasurementType::SOLAR_RADIATION, + output.solar.solarError}; + Measurement precipitation{output.precipitation.precipitation, 2, NO_I2C_ADDRESS, MeasurementType::PRECIPITATION, + output.precipitation.precipitationError}; - messages.emplace_back(solarRadiation, sensorInformation, - Time::getInstance().getEpochSeconds()); - messages.emplace_back(precipitation, - sensorInformation, - Time::getInstance().getEpochSeconds()); + messages.emplace_back(solarRadiation, sensorInformation, Time::getInstance().getEpochSeconds()); + messages.emplace_back(precipitation, sensorInformation, Time::getInstance().getEpochSeconds()); - return messages; + return messages; } SensorInformation Forte_RS485::getSensorInformation() const { - return sensorInformation; + return sensorInformation; } \ No newline at end of file diff --git a/client/libs/rs485/rs485.hpp b/client/libs/rs485/rs485.hpp index 01c39b4..00d4802 100644 --- a/client/libs/rs485/rs485.hpp +++ b/client/libs/rs485/rs485.hpp @@ -3,10 +3,11 @@ #include <MeasurementTypes.h> #include <SentecSolarRadiationSensor.h> +#include <RS485HardwareSerial.h> #include <SentecRainGaugeSensor.h> #include "Message.hpp" #include "ForteSensor.hpp" -#include "SentecSensors.h" +#include "SentecSensorsRS485.h" struct out_data_rs485 { out_data_solar_radiation solar; @@ -19,8 +20,14 @@ class Forte_RS485: public ForteSensor<out_data_rs485> { out_data_rs485 readData() override; std::list<Message> buildMessages() override; [[nodiscard]] SensorInformation getSensorInformation() const override; + void teardown(); + static void powerOnRS485Sensors(); + static void powerOffRS485Sensors(); private: + + std::shared_ptr<HardwareSerial> RS485Serial; + const SensorInformation sensorInformation{HardwareName::RS485, SensorProtocol::RS485}; diff --git a/shared-libs/DataTransfer/ErrorTypes.cpp b/shared-libs/DataTransfer/ErrorTypes.cpp index 8526b71..726808c 100644 --- a/shared-libs/DataTransfer/ErrorTypes.cpp +++ b/shared-libs/DataTransfer/ErrorTypes.cpp @@ -5,37 +5,39 @@ #include "ErrorTypes.h" namespace ErrorTypes { - // INFO: If you add a new error type, add it here and to the documentation at https://git.uibk.ac.at/informatik/qe/forte/sensor-system/-/wikis/Error-Types as well - std::string errorTypeToString(ErrorType errorType) { - switch (errorType) { - case ErrorType::SENSOR_NOT_FOUND: - return "SENSOR_NOT_FOUND"; - case ErrorType::SENSOR_NOT_CONNECTED: - return "SENSOR_NOT_CONNECTED"; - case ErrorType::NO_DATA: - return "NO_DATA"; - case ErrorType::DATA_OK: - return "DATA_OK"; - case ErrorType::NULL_MESSAGE: - return "NULL_MESSAGE"; - case ErrorType::SENSOR_DOES_NOT_RETURN_DATA: - return "SENSOR_DOES_NOT_RETURN_DATA"; - case ErrorType::BATTERY_VOLTAGE_TOO_LOW: - return "BATTERY_VOLTAGE_TOO_LOW"; - case ErrorType::INVALID_VALUE: - return "INVALID_VALUE"; - case ErrorType::SENSOR_INIT_FAILED: - return "SENSOR_INIT_FAILED"; - case ErrorType::INA219_OVERFLOW: - return "INA219_OVERFLOW"; - case ErrorType::CONNECTION_ENDED_PREMATURELY: - return "CONNECTION_ENDED_PREMATURELY"; - case ErrorType::WRONG_CRC: - return "WRONG_CRC"; - case ErrorType::UNKNOWN: - default: - return "UNKNOWN_ERROR_TYPE"; - } - } +// INFO: If you add a new error type, add it here and to the documentation at https://git.uibk.ac.at/informatik/qe/forte/sensor-system/-/wikis/Error-Types as well +std::string errorTypeToString(ErrorType errorType) { + switch (errorType) { + case ErrorType::SENSOR_NOT_FOUND: + return "SENSOR_NOT_FOUND"; + case ErrorType::SENSOR_NOT_CONNECTED: + return "SENSOR_NOT_CONNECTED"; + case ErrorType::NO_DATA: + return "NO_DATA"; + case ErrorType::DATA_OK: + return "DATA_OK"; + case ErrorType::NULL_MESSAGE: + return "NULL_MESSAGE"; + case ErrorType::SENSOR_DOES_NOT_RETURN_DATA: + return "SENSOR_DOES_NOT_RETURN_DATA"; + case ErrorType::BATTERY_VOLTAGE_TOO_LOW: + return "BATTERY_VOLTAGE_TOO_LOW"; + case ErrorType::INVALID_VALUE: + return "INVALID_VALUE"; + case ErrorType::SENSOR_INIT_FAILED: + return "SENSOR_INIT_FAILED"; + case ErrorType::INA219_OVERFLOW: + return "INA219_OVERFLOW"; + case ErrorType::CONNECTION_ENDED_PREMATURELY: + return "CONNECTION_ENDED_PREMATURELY"; + case ErrorType::WRONG_CRC: + return "WRONG_CRC"; + case ErrorType::SEM404_COULD_NOT_RESET_PRECIPITATION: + return "SEM404_COULD_NOT_RESET_PRECIPITATION"; + case ErrorType::UNKNOWN: + default: + return "UNKNOWN_ERROR_TYPE"; + } +} } \ No newline at end of file diff --git a/shared-libs/DataTransfer/ErrorTypes.h b/shared-libs/DataTransfer/ErrorTypes.h index 3d12cbd..f24944a 100644 --- a/shared-libs/DataTransfer/ErrorTypes.h +++ b/shared-libs/DataTransfer/ErrorTypes.h @@ -7,24 +7,25 @@ #include <string> -enum class ErrorType : char { - SENSOR_NOT_FOUND, - SENSOR_INIT_FAILED, - SENSOR_NOT_CONNECTED, - NO_DATA, - DATA_OK, - NULL_MESSAGE, // message that is sent as padding, should be thrown away - SENSOR_DOES_NOT_RETURN_DATA, - BATTERY_VOLTAGE_TOO_LOW, - INVALID_VALUE, - INA219_OVERFLOW, - CONNECTION_ENDED_PREMATURELY, // connection ended prematurely - WRONG_CRC, // corrupted data package - UNKNOWN, +enum class ErrorType: char { + SENSOR_NOT_FOUND, + SENSOR_INIT_FAILED, + SENSOR_NOT_CONNECTED, + SEM404_COULD_NOT_RESET_PRECIPITATION, + NO_DATA, + DATA_OK, + NULL_MESSAGE, // message that is sent as padding, should be thrown away + SENSOR_DOES_NOT_RETURN_DATA, + BATTERY_VOLTAGE_TOO_LOW, + INVALID_VALUE, + INA219_OVERFLOW, + CONNECTION_ENDED_PREMATURELY, // connection ended prematurely + WRONG_CRC, // corrupted data package + UNKNOWN, }; namespace ErrorTypes { - std::string errorTypeToString(ErrorType errorType); +std::string errorTypeToString(ErrorType errorType); } -- GitLab