Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added HDLC support #70

Merged
merged 1 commit into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ uart:

## Installation
Clone the repository and create a companion `secrets.yaml` file with the following fields:

If your electricity supplier is using Aidon 6442SE or Aidon 653X, you might also need to change the instantiation of the meter_sensor.
As these meters are sometimes using the HDLC-protocol instead of ASCII. Open [p1reader.yaml](./p1reader.yaml)
```c
// Change
auto meter_sensor = new P1Reader(id(uart_bus));
// To
auto meter_sensor = new P1ReaderHDLC(id(uart_bus));
```

Create a companion `secrets.yaml` file with the following fields:
```
wifi_ssid: <your wifi SSID>
wifi_password: <your wifi password>
Expand Down
217 changes: 214 additions & 3 deletions p1reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//
// History
// 0.1.0 2020-11-05: Initial release
// 0.x.0 2023-04-18: Added HDLC support
//
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
Expand Down Expand Up @@ -68,7 +69,9 @@ class ParsedMessage {
class P1Reader : public Component, public UARTDevice {
const char* DELIMITERS = "()*:";
const char* DATA_ID = "1-0";
char buffer[BUF_SIZE];

protected:
char buffer[BUF_SIZE];

public:
Sensor *cumulativeActiveImport = new Sensor();
Expand Down Expand Up @@ -113,11 +116,12 @@ class P1Reader : public Component, public UARTDevice {

void setup() override { }

void loop() override {
void loop() override
{
readP1Message();
}

private:
protected:
uint16_t crc16_update(uint16_t crc, uint8_t a) {
int i;
crc ^= a;
Expand Down Expand Up @@ -252,6 +256,7 @@ class P1Reader : public Component, public UARTDevice {
currentL3->publish_state(parsed->currentL3);
}

private:
void readP1Message() {
if (available()) {
uint16_t crc = 0x0000;
Expand Down Expand Up @@ -311,4 +316,210 @@ class P1Reader : public Component, public UARTDevice {
}
}
}


};

class P1ReaderHDLC : public P1Reader {
const int8_t OUTSIDE_FRAME = 0;
const int8_t FOUND_FRAME = 1;


int8_t parseHDLCState = OUTSIDE_FRAME;

public:
P1ReaderHDLC(UARTComponent *parent) : P1Reader(parent) {}

void loop() override {
readHDLCMessage();
}

private:
bool readHDLCStruct(ParsedMessage* parsed) {
if(Serial.readBytes(buffer, 3) != 3)
return false;

if(buffer[0] != 0x02) {
return false;
}

char obis[7];

uint8_t struct_len = buffer[1];
//ESP_LOGD("hdlc", "Struct length is %d", struct_len);

uint8_t tag = buffer[2];

if(tag != 0x09) {
ESP_LOGE("hdlc", "Unexpected tag %X in struct, bailing out", tag);
return false;
}

uint8_t str_length = read();
if(Serial.readBytes(buffer, str_length) != str_length) {
ESP_LOGE("hdlc", "Unable to read %d bytes of OBIS code", str_length);
return false;
}
buffer[str_length] = 0; // Null-terminate
sprintf(obis, "%d.%d.%d", buffer[2], buffer[3], buffer[4]);

tag = read();

char value_buf[24];
bool is_signed = false;
uint32_t uvalue = 0;
int32_t value = 0;
if (tag == 0x09) {
str_length = read();
if(Serial.readBytes(buffer, str_length) != str_length) {
ESP_LOGE("hdlc", "Unable to read %d bytes of string", str_length);
return false;
}

buffer[str_length] = 0;
//ESP_LOGD("hdlc", "Read string length %d", str_length);
} else if(tag == 0x06) {
Serial.readBytes(buffer, 4);
//uvalue = buffer[0] | buffer[1] << 8 | buffer[2] << 16 | buffer[3] << 24;
uvalue = buffer[3] | buffer[2] << 8 | buffer[1] << 16 | buffer[0] << 24;
//ESP_LOGD("hdlc", "Value of uvalue is %u", uvalue);
} else if (tag == 0x10) {
Serial.readBytes(buffer, 2);
//value = buffer[0] | buffer[1] << 8; //
is_signed = true;
value = buffer[1] | buffer[0] << 8;
//ESP_LOGD("hdlc", "(Signed) Value of value is %d", value);
} else if (tag == 0x12) {
Serial.readBytes(buffer, 2);
//uvalue = buffer[0] | buffer[1] << 8; //
uvalue = buffer[1] | buffer[0] << 8;
//ESP_LOGD("hdlc", "(Unsigned) Value of uvalue is %u", uvalue);
} else {
ESP_LOGE("hdlc", "unknown tag %X", tag);
}

int8_t scaler;
uint8_t unit;
if(struct_len == 3) {
Serial.readBytes(buffer, 6);
scaler = buffer[3];
unit = buffer[5];
//ESP_LOGD("hdlc", "Scaler %u", scaler);
//ESP_LOGD("hdlc", "Unit %d", buffer[5]);

if(!is_signed)
value = uvalue;

double scaled_value = pow(10, scaler) * value;

// Volt and Ampere are the only two units where p1reader.yaml doesn't specify
// we should report in 1/1000, all others should be divided.
if(unit != 33 && unit != 35)
scaled_value = scaled_value / 1000;

snprintf(value_buf, 24, "%f", scaled_value);
parseRow(parsed, obis, value_buf);
}




return true;
}

/* Reads messages formatted according to "Branschrekommendation v1.2", which
at the time of writing (20210207) is used by Tekniska Verken's Aidon 6442SE
meters. This is a binary format, with a HDLC Frame.

This code is in no way a generic HDLC Frame parser, but it does the job
of decoding this particular data stream.
*/
void readHDLCMessage()
{
if (available())
{
uint8_t data = 0;
uint16_t crc = 0x0000;
ParsedMessage parsed = ParsedMessage();

while (parseHDLCState == OUTSIDE_FRAME)
{
data = read();
if (data == 0x7e)
{
// ESP_LOGD("hdlc", "Found start of frame");
parseHDLCState = FOUND_FRAME;
break;

int8_t next = peek();

// ESP_LOGD("hdlc", "Next is %d", next);

if (next == 0x7e)
{
read(); // We were actually at the end flag, consume the start flag of the next frame.
} else if (next == -1) {
ESP_LOGE("hdlc", "No char available after flag, out of sync. Returning");
parseHDLCState = OUTSIDE_FRAME;
return;
}
}
}

if (parseHDLCState == FOUND_FRAME)
{
// Read various static HDLC Frame information we don't care about
int len = Serial.readBytes(buffer, 12);
if (len != 12) {
ESP_LOGE("hdlc", "Expected 12 bytes, got %d bytes - out of sync. Returning", len);
parseHDLCState = OUTSIDE_FRAME;
return;
}
// ESP_LOGD("hdlc", "Got %d HDLC bytes, now reading 4 Invoke ID And Priority bytes", len);
len = Serial.readBytes(buffer, 4);
if (len != 4 || buffer[0] != 0x40 || buffer[1] != 0x00 || buffer[2] != 0x00 || buffer[3] != 0x00)
{
ESP_LOGE("hdlc", "Expected 0x40 0x00 0x00 0x00, got %X %X %X %X - out of sync, returning.", buffer[0], buffer[1], buffer[2], buffer[3]);
parseHDLCState = OUTSIDE_FRAME;
return;
}
}

data = read(); // Expect length of time field, usually 0
//ESP_LOGD("hdlc", "Length of datetime field is %d", data);
Serial.readBytes(buffer, data);

data = read();
ESP_LOGD("hdlc", "Expect 0x01 (array tag), got 0x%02x", data);
if(data != 0x01) {
parseHDLCState = OUTSIDE_FRAME;
return;
}

uint8_t array_length = read();
ESP_LOGD("hdlc", "Array length is %d", array_length);

for(int i=0;i<array_length;i++) {
if(!readHDLCStruct(&parsed)) {
parseHDLCState = OUTSIDE_FRAME;
return;
}
}

publishSensors(&parsed);


while (true)
{
data = read();
//ESP_LOGD("hdlc", "Read char %02X", data);
if (data == 0x7e)
{
ESP_LOGD("hdlc", "Found end of frame");
parseHDLCState = OUTSIDE_FRAME;
return;
}
}
}
}
};
8 changes: 7 additions & 1 deletion p1reader.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ uart:
tx_pin: TX
rx_pin: RX
baud_rate: 115200


# If your electricity meter is an Aidon, which use the older
# Branschstandard (1.2) where the data is HDLC-formatted
# Change the first line after lambda to
# auto_meter_sensor = new P1ReaderHDLC(id(uart_bus));
# else
# auto meter_sensor = new P1Reader(id(uart_bus));
sensor:
- platform: custom
lambda: |-
Expand Down