From 0f1abb57d47a5618727abbf8ea5be1b517514b1f Mon Sep 17 00:00:00 2001 From: Marcel Kummer Date: Wed, 4 Oct 2023 11:59:32 +0200 Subject: [PATCH 1/2] Make sml_workarounds a bitmap, expose in public API The struct `sml_workarounds` has become a bitmap now, and it is part of the public API. I rephrased the DZG workaround explanation a bit to match the new information: The workaround can't be applied automatically because we need meter model *and* serial number, but only get the latter via SML. The serial numbers that have the fix apparently differ between DZG meter models. This is also why `sml_workarounds` needs to be public: There needs to be a way for the libsml user to configure at least this workaround for the meter at hand. Implementing it as a bitmap is memory-efficient in contrast to passing an additional struct around everywhere. Adding `sml_workarounds` to `sml_buffer` makes it possible to configure workarounds individually for multiple meters that may be handled by the user, each of which will have their own buffer. --- examples/sml_server.c | 3 +- sml/include/sml/sml_file.h | 3 +- sml/include/sml/sml_shared.h | 38 +++++++++++++++++++++++++ sml/src/sml_file.c | 5 +++- sml/src/sml_list.c | 55 ++++-------------------------------- sml/src/sml_shared.c | 8 ++++-- test/src/sml_file_test.c | 3 +- 7 files changed, 60 insertions(+), 55 deletions(-) diff --git a/examples/sml_server.c b/examples/sml_server.c index 1de47bc..7e29c20 100644 --- a/examples/sml_server.c +++ b/examples/sml_server.c @@ -41,6 +41,7 @@ // globals int sflag = false; // flag to process only a single OBIS data stream int vflag = false; // verbose flag +sml_workarounds workarounds = 0; int serial_port_open(const char* device) { @@ -88,7 +89,7 @@ void transport_receiver(unsigned char *buffer, size_t buffer_len) { int i; // the buffer contains the whole message, with transport escape sequences. // these escape sequences are stripped here. - sml_file *file = sml_file_parse(buffer + 8, buffer_len - 16); + sml_file *file = sml_file_parse(buffer + 8, buffer_len - 16, workarounds); // the sml file is parsed now // this prints some information about the file diff --git a/sml/include/sml/sml_file.h b/sml/include/sml/sml_file.h index d864d40..42383b3 100644 --- a/sml/include/sml/sml_file.h +++ b/sml/include/sml/sml_file.h @@ -36,7 +36,8 @@ typedef struct { sml_file *sml_file_init(); // parses a SML file. -sml_file *sml_file_parse(unsigned char *buffer, size_t buffer_len); +sml_file *sml_file_parse(unsigned char *buffer, size_t buffer_len, + const sml_workarounds workarounds); void sml_file_add_message(sml_file *file, sml_message *message); void sml_file_write(sml_file *file); void sml_file_free(sml_file *file); diff --git a/sml/include/sml/sml_shared.h b/sml/include/sml/sml_shared.h index 86dec36..a80128e 100644 --- a/sml/include/sml/sml_shared.h +++ b/sml/include/sml/sml_shared.h @@ -56,6 +56,43 @@ typedef int64_t i64; #define SML_TYPE_NUMBER_32 sizeof(u32) #define SML_TYPE_NUMBER_64 sizeof(u64) +// A bitmap to configure workarounds. +typedef int16_t sml_workarounds; + +/* + * Workaround for certain DZG meters that encode the consumption wrongly. + * Affected: + * - Model TODO with serial numbers starting with 6 (in decimal) + * + * We only get the serial number, not the meter model via SML. Since model + * DWSB.2TH (serial starting with 4) does not exhibit this bug, we can't + * apply the workaround automatically. + * + * The value uses a scaler of -2, so e.g. 328.05 should be + * encoded as an unsigned int with 2 bytes (called Unsigned16 in the standard): + * 63 80 25 (0x8025 == 32805 corresponds to 328.05W) + * or as an unsigned int with 3 bytes (not named in the standard): + * 64 00 80 25 + * or as a signed int with 3 bytes (not named in the standard): + * 54 00 80 25 + * but they encode it as a signed int with 2 bytes (called Integer16 in the standard): + * 53 80 25 + * which reads as -32731 corresponding to -327.31W. + * + * Luckily, it doesn't attempt to do any compression on + * negative values, they're always encoded as, e.g. + * 55 ff fe 13 93 (== -126061 -> -1260.61W) + * + * Since we cannot have positive values >= 0x80000000 + * (that would be 21474836.48 W, yes, >21MW), we can + * assume that for 1, 2, 3 bytes, if they look signed, + * they really were intended to be unsigned. + * + * Note that this will NOT work if a meter outputs negative + * values compressed as well - but mine doesn't. + */ +#define SML_WORKAROUND_DZG_NEGATIVE 0x0001 + // This sml_buffer is used in two different use-cases. // // Parsing: the raw data is in the buffer field, @@ -74,6 +111,7 @@ typedef struct { size_t cursor; int error; char *error_msg; + sml_workarounds workarounds; } sml_buffer; sml_buffer *sml_buffer_init(size_t length); diff --git a/sml/src/sml_file.c b/sml/src/sml_file.c index dd7a78a..3a6e37d 100644 --- a/sml/src/sml_file.c +++ b/sml/src/sml_file.c @@ -27,11 +27,14 @@ // EDL meter must provide at least 250 bytes as a receive buffer #define SML_FILE_BUFFER_LENGTH 512 -sml_file *sml_file_parse(unsigned char *buffer, size_t buffer_len) { +sml_file *sml_file_parse(unsigned char *buffer, + size_t buffer_len, + const sml_workarounds workarounds) { sml_file *file = (sml_file *)malloc(sizeof(sml_file)); *file = (sml_file){.messages = NULL, .messages_len = 0, .buf = NULL}; sml_buffer *buf = sml_buffer_init(buffer_len); + buf->workarounds = workarounds; memcpy(buf->buffer, buffer, buffer_len); file->buf = buf; diff --git a/sml/src/sml_list.c b/sml/src/sml_list.c index 2c776e8..a1f64eb 100644 --- a/sml/src/sml_list.c +++ b/sml/src/sml_list.c @@ -112,16 +112,7 @@ sml_list *sml_list_init() { void sml_list_add(sml_list *list, sml_list *new_entry) { list->next = new_entry; } -struct workarounds { - unsigned int old_dzg_meter : 1; -}; - -sml_list *sml_list_entry_parse(sml_buffer *buf, struct workarounds *workarounds) { - static const unsigned char dzg_serial_name[] = {1, 0, 96, 1, 0, 255}; - static const unsigned char dzg_serial_start[] = {0x0a, 0x01, 'D', 'Z', 'G', 0x00}; - // this is "1 DZG 00 60000000" in the hex encoding, see comment below - static const unsigned char dzg_serial_fixed[] = - {0x0a, 0x01, 'D', 'Z', 'G', 0x00, 0x03, 0x93, 0x87, 0x00}; +sml_list *sml_list_entry_parse(sml_buffer *buf) { static const unsigned char dzg_power_name[] = {1, 0, 16, 7, 0, 255}; u8 value_tl, value_len_more; sml_list *l = NULL; @@ -171,43 +162,10 @@ sml_list *sml_list_entry_parse(sml_buffer *buf, struct workarounds *workarounds) if (sml_buf_has_errors(buf)) goto error; - /* - * Work around DZG meters before serial numbers starting with - * 6 (in decimal) - they encode the consumption wrongly: - * The value uses a scaler of -2, so e.g. 328.05 should be - * encoded as an unsigned int with 2 bytes (called Unsigned16 in the standard): - * 63 80 25 (0x8025 == 32805 corresponds to 328.05W) - * or as an unsigned int with 3 bytes (not named in the standard): - * 64 00 80 25 - * or as a signed int with 3 bytes (not named in the standard): - * 54 00 80 25 - * but they encode it as a signed int with 2 bytes (called Integer16 in the standard): - * 53 80 25 - * which reads as -32731 corresponding to -327.31W. - * - * Luckily, it doesn't attempt to do any compression on - * negative values, they're always encoded as, e.g. - * 55 ff fe 13 93 (== -126061 -> -1260.61W) - * - * Since we cannot have positive values >= 0x80000000 - * (that would be 21474836.48 W, yes, >21MW), we can - * assume that for 1, 2, 3 bytes, if they look signed, - * they really were intended to be unsigned. - * - * Note that this will NOT work if a meter outputs negative - * values compressed as well - but mine doesn't. - */ - if (l->obj_name && l->obj_name->len == sizeof(dzg_serial_name) && - memcmp(l->obj_name->str, dzg_serial_name, sizeof(dzg_serial_name)) == 0 && l->value && - l->value->type == SML_TYPE_OCTET_STRING && - l->value->data.bytes->len >= (int)sizeof(dzg_serial_start) && - memcmp(l->value->data.bytes->str, dzg_serial_start, sizeof(dzg_serial_start)) == 0 && - memcmp(l->value->data.bytes->str, dzg_serial_fixed, sizeof(dzg_serial_fixed)) < 0) { - workarounds->old_dzg_meter = 1; - } else if (workarounds->old_dzg_meter && l->obj_name && - l->obj_name->len == sizeof(dzg_power_name) && - memcmp(l->obj_name->str, dzg_power_name, sizeof(dzg_power_name)) == 0 && l->value && - (value_len_more == 1 || value_len_more == 2 || value_len_more == 3)) { + if ((buf->workarounds & SML_WORKAROUND_DZG_NEGATIVE) && l->obj_name && + l->obj_name->len == sizeof(dzg_power_name) && + memcmp(l->obj_name->str, dzg_power_name, sizeof(dzg_power_name)) == 0 && l->value && + (value_len_more == 1 || value_len_more == 2 || value_len_more == 3)) { l->value->type &= ~SML_TYPE_FIELD; l->value->type |= SML_TYPE_UNSIGNED; } @@ -223,7 +181,6 @@ sml_list *sml_list_entry_parse(sml_buffer *buf, struct workarounds *workarounds) } sml_list *sml_list_parse(sml_buffer *buf) { - struct workarounds workarounds = {0}; sml_list *ret = NULL, **pos = &ret; int elems; @@ -239,7 +196,7 @@ sml_list *sml_list_parse(sml_buffer *buf) { elems = sml_buf_get_next_length(buf); while (elems > 0) { - *pos = sml_list_entry_parse(buf, &workarounds); + *pos = sml_list_entry_parse(buf); if (sml_buf_has_errors(buf)) goto error; pos = &(*pos)->next; diff --git a/sml/src/sml_shared.c b/sml/src/sml_shared.c index d7cad9d..01c39f8 100644 --- a/sml/src/sml_shared.c +++ b/sml/src/sml_shared.c @@ -118,8 +118,12 @@ void sml_buf_update_bytes_read(sml_buffer *buf, int bytes) { buf->cursor += byte sml_buffer *sml_buffer_init(size_t length) { sml_buffer *buf = (sml_buffer *)malloc(sizeof(sml_buffer)); - *buf = - (sml_buffer){.buffer = NULL, .buffer_len = 0, .cursor = 0, .error = 0, .error_msg = NULL}; + *buf = (sml_buffer){.buffer = NULL, + .buffer_len = 0, + .cursor = 0, + .error = 0, + .error_msg = NULL, + .workarounds = 0}; buf->buffer = (unsigned char *)malloc(length); buf->buffer_len = length; memset(buf->buffer, 0, buf->buffer_len); diff --git a/test/src/sml_file_test.c b/test/src/sml_file_test.c index 9ded16f..94be7c1 100644 --- a/test/src/sml_file_test.c +++ b/test/src/sml_file_test.c @@ -63,8 +63,9 @@ TEST(sml_file, parse_crash_report1) { // sadly not... just 202 TEST_ASSERT_EQUAL(388, sizeof buffer2); memcpy(buffer, buffer2, sizeof buffer2); size_t bytes = 388; + sml_workarounds workarounds = 0; // bytes and buffer like from sml_transport_read - sml_file *file = sml_file_parse(buffer + 8, bytes - 16); + sml_file *file = sml_file_parse(buffer + 8, bytes - 16, workarounds); TEST_ASSERT_NOT_NULL(file); TEST_ASSERT_EQUAL(1, file->messages_len); sml_file_free(file); From e4201d3392e74a3050c51c41f41a85adba666fb3 Mon Sep 17 00:00:00 2001 From: Marcel Kummer Date: Wed, 4 Oct 2023 13:02:39 +0200 Subject: [PATCH 2/2] sml_server: Add option `-d` to enable DZG workaround This is necessary since we can't decide automatically if the workaround needs to be enabled. (See previous commits for details.) --- examples/sml_server.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/sml_server.c b/examples/sml_server.c index 7e29c20..c1d0ab6 100644 --- a/examples/sml_server.c +++ b/examples/sml_server.c @@ -162,13 +162,17 @@ int main(int argc, char *argv[]) { while ((c = getopt(argc, argv, "+hsv")) != -1) { switch (c) { case 'h': - printf("usage: %s [-h] [-s] [-v] device\n", argv[0]); + printf("usage: %s [-d] [-h] [-s] [-v] device\n", argv[0]); printf("device - serial device of connected power meter e.g. /dev/cu.usbserial, or - for stdin\n"); + printf("-d - Enable negative values workaround for certain DZG meters\n"); printf("-h - help\n"); printf("-s - process only one OBIS data stream (single)\n"); printf("-v - verbose\n"); exit(0); // exit here break; + case 'd': + workarounds |= SML_WORKAROUND_DZG_NEGATIVE; + break; case 's': sflag = true; break;