From bd908ee1421c64bc9d707833259b4dcea5646735 Mon Sep 17 00:00:00 2001 From: Roar Fredriksen Date: Mon, 25 Sep 2017 22:51:22 +0200 Subject: [PATCH] Preparing project for use as a library --- Code/Arduino/HanReader/Crc16.cpp | 37 ++++ Code/Arduino/HanReader/Crc16.h | 23 +++ Code/Arduino/HanReader/DlmsReader.cpp | 154 ++++++++++++++ Code/Arduino/HanReader/DlmsReader.h | 42 ++++ Code/Arduino/HanReader/HanReader.cpp | 63 ++++++ Code/Arduino/HanReader/HanReader.h | 44 ++++ Code/Arduino/HanReader/KaifaHan.cpp | 104 ++++++++++ Code/Arduino/HanReader/KaifaHan.h | 73 +++++++ .../reporting_han_data_to_mqtt.pde | 191 ++++++++++++++++++ 9 files changed, 731 insertions(+) create mode 100644 Code/Arduino/HanReader/Crc16.cpp create mode 100644 Code/Arduino/HanReader/Crc16.h create mode 100644 Code/Arduino/HanReader/DlmsReader.cpp create mode 100644 Code/Arduino/HanReader/DlmsReader.h create mode 100644 Code/Arduino/HanReader/HanReader.cpp create mode 100644 Code/Arduino/HanReader/HanReader.h create mode 100644 Code/Arduino/HanReader/KaifaHan.cpp create mode 100644 Code/Arduino/HanReader/KaifaHan.h create mode 100644 Code/Arduino/HanReader/examples/reporting_han_data_to_mqtt/reporting_han_data_to_mqtt.pde diff --git a/Code/Arduino/HanReader/Crc16.cpp b/Code/Arduino/HanReader/Crc16.cpp new file mode 100644 index 00000000..bfb7141f --- /dev/null +++ b/Code/Arduino/HanReader/Crc16.cpp @@ -0,0 +1,37 @@ +#include "Crc16.h" + +Crc16Class::Crc16Class() +{ + unsigned short value; + unsigned short temp; + for (unsigned short i = 0; i < 256; ++i) + { + value = 0; + temp = i; + for (byte j = 0; j < 8; ++j) + { + if (((value ^ temp) & 0x0001) != 0) + { + value = (ushort)((value >> 1) ^ polynomial); + } + else + { + value >>= 1; + } + temp >>= 1; + } + table[i] = value; + } +} + +unsigned short Crc16Class::ComputeChecksum(byte *data, int start, int length) +{ + ushort fcs = 0xffff; + for (int i = start; i < (start + length); i++) + { + byte index = (fcs ^ data[i]) & 0xff; + fcs = (ushort)((fcs >> 8) ^ table[index]); + } + fcs ^= 0xffff; + return fcs; +} diff --git a/Code/Arduino/HanReader/Crc16.h b/Code/Arduino/HanReader/Crc16.h new file mode 100644 index 00000000..406c0af2 --- /dev/null +++ b/Code/Arduino/HanReader/Crc16.h @@ -0,0 +1,23 @@ +#ifndef _CRC16_h +#define _CRC16_h + +#if defined(ARDUINO) && ARDUINO >= 100 + #include "arduino.h" +#else + #include "WProgram.h" +#endif + +class Crc16Class +{ + public: + Crc16Class(); + unsigned short ComputeChecksum(byte *data, int start, int length); + protected: + private: + const unsigned short polynomial = 0x8408; + unsigned short table[256]; +}; + +#endif + + diff --git a/Code/Arduino/HanReader/DlmsReader.cpp b/Code/Arduino/HanReader/DlmsReader.cpp new file mode 100644 index 00000000..cce664b4 --- /dev/null +++ b/Code/Arduino/HanReader/DlmsReader.cpp @@ -0,0 +1,154 @@ +#include "DlmsReader.h" + +DlmsReader::DlmsReader() +{ + //this->Clear(); +} + +void DlmsReader::Clear() +{ + this->position = 0; + this->dataLength = 0; + this->destinationAddressLength = 0; + this->sourceAddressLength = 0; + this->frameFormatType = 0; +} + +bool DlmsReader::Read(byte data) +{ + if (position == 0 && data != 0x7E) + { + // we haven't started yet, wait for the start flag (no need to capture any data yet) + return false; + } + else + { + // We have completed reading of one package, so clear and be ready for the next + if (dataLength > 0 && position >= dataLength + 2) + Clear(); + + // Check if we're about to run into a buffer overflow + if (position >= DLMS_READER_BUFFER_SIZE) + Clear(); + + // Check if this is a second start flag, which indicates the previous one was a stop from the last package + if (position == 1 && data == 0x7E) + { + // just return, we can keep the one byte we had in the buffer + return false; + } + + // We have started, so capture every byte + buffer[position++] = data; + + if (position == 1) + { + // This was the start flag, we're not done yet + return false; + } + else if (position == 2) + { + // Capture the Frame Format Type + frameFormatType = (byte)(data & 0xF0); + if (!IsValidFrameFormat(frameFormatType)) + Clear(); + return false; + } + else if (position == 3) + { + // Capture the length of the data package + dataLength = ((buffer[1] & 0x0F) << 8) | buffer[2]; + return false; + } + else if (destinationAddressLength == 0) + { + // Capture the destination address + destinationAddressLength = GetAddress(3, destinationAddress, 0, DLMS_READER_MAX_ADDRESS_SIZE); + if (destinationAddressLength > 3) + Clear(); + return false; + } + else if (sourceAddressLength == 0) + { + // Capture the source address + sourceAddressLength = GetAddress(3 + destinationAddressLength, sourceAddress, 0, DLMS_READER_MAX_ADDRESS_SIZE); + if (sourceAddressLength > 3) + Clear(); + return false; + } + else if (position == 4 + destinationAddressLength + sourceAddressLength + 2) + { + // Verify the header checksum + ushort headerChecksum = GetChecksum(position - 3); + if (headerChecksum != Crc16.ComputeChecksum(buffer, 1, position - 3)) + Clear(); + return false; + } + else if (position == dataLength + 1) + { + // Verify the data package checksum + ushort checksum = this->GetChecksum(position - 3); + if (checksum != Crc16.ComputeChecksum(buffer, 1, position - 3)) + Clear(); + return false; + } + else if (position == dataLength + 2) + { + // We're done, check the stop flag and signal we're done + if (data == 0x7E) + return true; + else + { + Clear(); + return false; + } + } + } + return false; +} + +bool DlmsReader::IsValidFrameFormat(byte frameFormatType) +{ + return frameFormatType == 0xA0; +} + +int DlmsReader::GetRawData(byte *dataBuffer, int start, int length) +{ + if (dataLength > 0 && position == dataLength + 2) + { + int headerLength = 3 + destinationAddressLength + sourceAddressLength + 2; + int bytesWritten = 0; + for (int i = headerLength + 1; i < dataLength - 1; i++) + { + dataBuffer[i + start - headerLength - 1] = buffer[i]; + bytesWritten++; + } + return bytesWritten; + } + else + return 0; +} + +int DlmsReader::GetAddress(int addressPosition, byte* addressBuffer, int start, int length) +{ + int addressBufferPos = start; + for (int i = addressPosition; i < position; i++) + { + addressBuffer[addressBufferPos++] = buffer[i]; + + // LSB=1 means this was the last address byte + if ((buffer[i] & 0x01) == 0x01) + break; + + // See if we've reached last byte, try again when we've got more data + else if (i == position - 1) + return 0; + } + return addressBufferPos - start; +} + +ushort DlmsReader::GetChecksum(int checksumPosition) +{ + return (ushort)(buffer[checksumPosition + 2] << 8 | + buffer[checksumPosition + 1]); +} diff --git a/Code/Arduino/HanReader/DlmsReader.h b/Code/Arduino/HanReader/DlmsReader.h new file mode 100644 index 00000000..c457602c --- /dev/null +++ b/Code/Arduino/HanReader/DlmsReader.h @@ -0,0 +1,42 @@ +#ifndef _DLMSREADER_h +#define _DLMSREADER_h + +#include "Crc16.h" + +#if defined(ARDUINO) && ARDUINO >= 100 + #include "arduino.h" +#else + #include "WProgram.h" +#endif + +#define DLMS_READER_BUFFER_SIZE 256 +#define DLMS_READER_MAX_ADDRESS_SIZE 5 + +class DlmsReader +{ + public: + DlmsReader(); + bool Read(byte data); + int GetRawData(byte *buffer, int start, int length); + + protected: + Crc16Class Crc16; + + private: + byte buffer[DLMS_READER_BUFFER_SIZE]; + int position; + int dataLength; + byte frameFormatType; + byte destinationAddress[DLMS_READER_MAX_ADDRESS_SIZE]; + byte destinationAddressLength; + byte sourceAddress[DLMS_READER_MAX_ADDRESS_SIZE]; + byte sourceAddressLength; + + void Clear(); + int GetAddress(int addressPosition, byte* buffer, int start, int length); + unsigned short GetChecksum(int checksumPosition); + bool IsValidFrameFormat(byte frameFormatType); + void WriteBuffer(); +}; + +#endif diff --git a/Code/Arduino/HanReader/HanReader.cpp b/Code/Arduino/HanReader/HanReader.cpp new file mode 100644 index 00000000..6b793d1c --- /dev/null +++ b/Code/Arduino/HanReader/HanReader.cpp @@ -0,0 +1,63 @@ +#include "HanReader.h" + +HanReader::HanReader() +{ + +} + +void HanReader::setup(HardwareSerial *hanPort) +{ + // Initialize H/W serial port for MBus communication + hanPort->begin(2400, SERIAL_8E1); + while (!hanPort) {} + bytesRead = 0; +} + +void HanReader::setup(HardwareSerial *hanPort, Stream *debugPort) +{ + setup(hanPort); + debug = debugPort; + if (debug) debug->println("MBUS serial setup complete"); +} + +bool HanReader::read() +{ + if (han->available()) + { + byte newByte = han->read(); + if (reader.Read(newByte)) + { + bytesRead = reader.GetRawData(buffer, 0, 512); + list = (List)kaifa.GetListID(buffer, 0, bytesRead); + return true; + } + } + + return false; +} + +List HanReader::getList() +{ + return list; +} + +time_t HanReader::getPackageTime() +{ + return kaifa.GetPackageTime(buffer, 0, bytesRead); +} + +int HanReader::getInt(List1_ObisObjects objectId) { return getInt((int)objectId); } +int HanReader::getInt(List2_ObisObjects objectId) { return getInt((int)objectId); } +int HanReader::getInt(List3_ObisObjects objectId) { return getInt((int)objectId); } +int HanReader::getInt(int objectId) +{ + return kaifa.GetInt(objectId, buffer, 0, bytesRead); +} + +String HanReader::getString(List1_ObisObjects objectId) { return getString((int)objectId); } +String HanReader::getString(List2_ObisObjects objectId) { return getString((int)objectId); } +String HanReader::getString(List3_ObisObjects objectId) { return getString((int)objectId); } +String HanReader::getString(int objectId) +{ + return kaifa.GetString(objectId, buffer, 0, bytesRead); +} diff --git a/Code/Arduino/HanReader/HanReader.h b/Code/Arduino/HanReader/HanReader.h new file mode 100644 index 00000000..34740bfb --- /dev/null +++ b/Code/Arduino/HanReader/HanReader.h @@ -0,0 +1,44 @@ +#ifndef _HANREADER_h +#define _HANREADER_h + +#if defined(ARDUINO) && ARDUINO >= 100 + #include "arduino.h" +#else + #include "WProgram.h" +#endif + + +#include "KaifaHan.h" +#include "DlmsReader.h" + + +class HanReader +{ + public: + HanReader(); + void setup(HardwareSerial *hanPort); + void setup(HardwareSerial *hanPort, Stream *debugPort); + bool read(); + List getList(); + time_t getPackageTime(); + int getInt(List1_ObisObjects objectId); + int getInt(List2_ObisObjects objectId); + int getInt(List3_ObisObjects objectId); + int getInt(int objectId); + String getString(List1_ObisObjects objectId); + String getString(List2_ObisObjects objectId); + String getString(List3_ObisObjects objectId); + String getString(int objectId); + + private: + Stream *debug; + Stream *han; + byte buffer[512]; + int bytesRead; + KaifaHan kaifa; + DlmsReader reader; + List list; +}; + + +#endif diff --git a/Code/Arduino/HanReader/KaifaHan.cpp b/Code/Arduino/HanReader/KaifaHan.cpp new file mode 100644 index 00000000..9eb5727e --- /dev/null +++ b/Code/Arduino/HanReader/KaifaHan.cpp @@ -0,0 +1,104 @@ +#include "KaifaHan.h" + + +byte KaifaHan::GetListID(byte *buffer, int start, int length) +{ + if (length > 23) + { + byte list = buffer[start + 23]; + if (list == (byte)List::List1) return (byte)List::List1; + if (list == (byte)List::List2) return (byte)List::List2; + if (list == (byte)List::List3) return (byte)List::List3; + } + return (byte)List::ListUnknown; +} + +long KaifaHan::GetPackageTime(byte *buffer, int start, int length) +{ + const int timeStart = 10; + int year = buffer[start + timeStart] << 8 | + buffer[start + timeStart + 1]; + + int month = buffer[start + timeStart + 2]; + int day = buffer[start + timeStart + 3]; + int hour = buffer[start + timeStart + 5]; + int minute = buffer[start + timeStart + 6]; + int second = buffer[start + timeStart + 7]; + + return toUnixTime(year, month, day, hour, minute, second); +} + +int KaifaHan::GetInt(int dataPosition, byte *buffer, int start, int length) +{ + int valuePosition = findValuePosition(dataPosition, buffer, start, length); + if (valuePosition > 0) + { + int value = 0; + for (int i = valuePosition + 1; i < valuePosition + 5; i++) + { + value = value << 8 | buffer[i]; + } + return value; + } + return 0; +} + +int KaifaHan::findValuePosition(int dataPosition, byte *buffer, int start, int length) +{ + const int dataStart = 24; + for (int i=start + dataStart; i 0) + { + String value = String(""); + for (int i = valuePosition + 2; i < valuePosition + buffer[valuePosition + 1]; i++) + { + value += String((char)buffer[i]); + } + return value; + } + return String(""); +} + +time_t KaifaHan::toUnixTime(int year, int month, int day, int hour, int minute, int second) +{ + byte daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + long secondsPerMinute = 60; + long secondsPerHour = secondsPerMinute * 60; + long secondsPerDay = secondsPerHour * 24; + + long time = (year - 1970) * secondsPerDay * 365L; + + for (int yearCounter = 1970; yearCounter 2 && (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))) + time += secondsPerDay; + + for (int monthCounter = 1; monthCounter= 100 + #include "arduino.h" +#else + #include "WProgram.h" +#endif + +class KaifaHan +{ + public: + byte GetListID(byte *buffer, int start, int length); + long GetPackageTime(byte *buffer, int start, int length); + int GetInt(int dataPosition, byte *buffer, int start, int length); + String GetString(int dataPosition, byte *buffer, int start, int length); + protected: + + private: + int findValuePosition(int dataPosition, byte *buffer, int start, int length); + time_t toUnixTime(int year, int month, int day, int hour, int minute, int second); +}; + +enum class List : byte { + ListUnknown = 0x00, + List1 = 0x01, + List2 = 0x0D, + List3 = 0x12 +}; + + +enum class List1_ObisObjects { + ActivePowerImported +}; + +enum class List2_ObisObjects { + ObisListVersionIdentifier, + MeterID, + MeterType, + ActivePowerImported, + ActivePowerExported, + ReactivePowerImported, + ReactivePowerExported, + CurrentPhaseL1, + CurrentPhaseL2, + CurrentPhaseL3, + VoltagePhaseL1, + VoltagePhaseL2, + VoltagePhaseL3 +}; + +enum class List3_ObisObjects { + ObisListVersionIdentifier, + MeterID, + MeterType, + ActivePowerImported, + ActivePowerExported, + ReactivePowerImported, + ReactivePowerExported, + CurrentPhaseL1, + CurrentPhaseL2, + CurrentPhaseL3, + VoltagePhaseL1, + VoltagePhaseL2, + VoltagePhaseL3, + ClockAndDate, + TotalActiveEnergyImported, + TotalActiveEnergyExported, + TotalReactiveEnergyImported, + TotalReactiveEnergyExported +}; + +#endif diff --git a/Code/Arduino/HanReader/examples/reporting_han_data_to_mqtt/reporting_han_data_to_mqtt.pde b/Code/Arduino/HanReader/examples/reporting_han_data_to_mqtt/reporting_han_data_to_mqtt.pde new file mode 100644 index 00000000..5e025372 --- /dev/null +++ b/Code/Arduino/HanReader/examples/reporting_han_data_to_mqtt/reporting_han_data_to_mqtt.pde @@ -0,0 +1,191 @@ +/* + * Simple sketch to read MBus data from electrical meter + * As the protocol requires "Even" parity, and this is + * only supported on the hardware port of the ESP8266, + * we'll have to use Serial1 for debugging. + * + * This means you'll have to program the ESP using the + * regular RX/TX port, and then you must remove the FTDI + * and connect the MBus signal from the meter to the + * RS pin. The FTDI/RX can be moved to Pin2 for debugging + * + * Created 14. september 2017 by Roar Fredriksen + */ + +#include +#include +#include +#include "HanReader.h" + +// The HAN Port reader +HanReader hanReader; + +// WiFi and MQTT endpoints +const char* ssid = "Roar_Etne"; +const char* password = "**********"; +const char* mqtt_server = "192.168.10.203"; + +WiFiClient espClient; +PubSubClient client(espClient); + +void setup() { + setupDebugPort(); + setupWiFi(); + setupMqtt(); + + // initialize the HanReader + // (passing Serial as the HAN port and Serial1 for debugging) + hanReader.setup(&Serial, &Serial1); +} + +void setupMqtt() +{ + client.setServer(mqtt_server, 1883); +} + +void setupDebugPort() +{ + // Initialize the Serial1 port for debugging + // (This port is fixed to Pin2 of the ESP8266) + Serial1.begin(115200); + while (!Serial1) {} + Serial1.setDebugOutput(true); + Serial1.println("Serial1"); + Serial1.println("Serial debugging port initialized"); +} + +void setupWiFi() +{ + // Initialize wifi + Serial1.print("Connecting to "); + Serial1.println(ssid); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial1.print("."); + } + + Serial1.println(""); + Serial1.println("WiFi connected"); + Serial1.println("IP address: "); + Serial1.println(WiFi.localIP()); +} + +void loop() { + loopMqtt(); + + // Read one byt from the port, and see if we got a full package + if (hanReader.read()) + { + // Get the list identifier + List list = hanReader.getList(); + Serial1.println(""); + Serial1.print("List #"); + Serial1.print((byte)list, HEX); + Serial1.print(": "); + + // Make sure we got a valid list + if (list == List::ListUnknown) + { + Serial1.println("Invalid list"); + return; + } + + // Get the timestamp (as unix time) from the package + time_t time = hanReader.getPackageTime(); + + // Define a json object to keep the data + StaticJsonBuffer<500> jsonBuffer; + JsonObject& root = jsonBuffer.createObject(); + + // Any generic useful info here + root["id"] = "espdebugger"; + root["up"] = millis(); + root["t"] = time; + + // Add a sub-structure to the json object, + // to keep the data from the meter itself + JsonObject& data = root.createNestedObject("data"); + + // Based on the list number, get all details + // according to OBIS specifications for the meter + if (list == List::List1) + { + data["P"] = hanReader.getInt(List1_ObisObjects::ActivePowerImported); + } + else if (list == List::List2) + { + data["lv"] = hanReader.getString(List2_ObisObjects::ObisListVersionIdentifier); + data["id"] = hanReader.getString(List2_ObisObjects::MeterID); + data["type"] = hanReader.getString(List2_ObisObjects::MeterType); + data["P"] = hanReader.getInt(List2_ObisObjects::ActivePowerImported); + data["Q"] = hanReader.getInt(List2_ObisObjects::ReactivePowerImported); + data["I1"] = hanReader.getInt(List2_ObisObjects::CurrentPhaseL1); + data["I2"] = hanReader.getInt(List2_ObisObjects::CurrentPhaseL2); + data["I3"] = hanReader.getInt(List2_ObisObjects::CurrentPhaseL3); + data["U1"] = hanReader.getInt(List2_ObisObjects::VoltagePhaseL1); + data["U2"] = hanReader.getInt(List2_ObisObjects::VoltagePhaseL1); + data["U3"] = hanReader.getInt(List2_ObisObjects::VoltagePhaseL1); + } + else if (list == List::List3) + { + data["lv"] = hanReader.getString(List3_ObisObjects::ObisListVersionIdentifier);; + data["id"] = hanReader.getString(List3_ObisObjects::MeterID); + data["type"] = hanReader.getString(List3_ObisObjects::MeterType); + data["P"] = hanReader.getInt(List3_ObisObjects::ActivePowerImported); + data["Q"] = hanReader.getInt(List3_ObisObjects::ReactivePowerImported); + data["I1"] = hanReader.getInt(List3_ObisObjects::CurrentPhaseL1); + data["I2"] = hanReader.getInt(List3_ObisObjects::CurrentPhaseL2); + data["I3"] = hanReader.getInt(List3_ObisObjects::CurrentPhaseL3); + data["U1"] = hanReader.getInt(List3_ObisObjects::VoltagePhaseL1); + data["U2"] = hanReader.getInt(List3_ObisObjects::VoltagePhaseL1); + data["U3"] = hanReader.getInt(List3_ObisObjects::VoltagePhaseL1); + data["tPI"] = hanReader.getInt(List3_ObisObjects::TotalActiveEnergyImported); + data["tPO"] = hanReader.getInt(List3_ObisObjects::TotalActiveEnergyExported); + data["tQI"] = hanReader.getInt(List3_ObisObjects::TotalReactiveEnergyImported); + data["tQO"] = hanReader.getInt(List3_ObisObjects::TotalReactiveEnergyExported); + } + + // Write the json to the debug port + root.printTo(Serial1); + Serial1.println(); + + // Publish the json to the MQTT server + char msg[1024]; + root.printTo(msg, 1024); + client.publish("sensors/out/espdebugger", msg); + } +} + +// Ensure the MQTT lirary gets some attention too +void loopMqtt() +{ + if (!client.connected()) { + reconnectMqtt(); + } + client.loop(); +} + +void reconnectMqtt() { + // Loop until we're reconnected + while (!client.connected()) { + Serial1.print("Attempting MQTT connection..."); + // Attempt to connect + if (client.connect("ESP8266Client")) { + Serial1.println("connected"); + // Once connected, publish an announcement... + // client.publish("sensors", "hello world"); + // ... and resubscribe + // client.subscribe("inTopic"); + } + else { + Serial1.print("failed, rc="); + Serial1.print(client.state()); + Serial1.println(" try again in 5 seconds"); + // Wait 5 seconds before retrying + delay(5000); + } + } +}