From 368b01373cf7f2921fd5b62954ac80cb3ea4ee88 Mon Sep 17 00:00:00 2001 From: Florin Mihut Date: Tue, 16 Dec 2025 08:51:13 +0100 Subject: [PATCH 1/5] feat(Carlo Gavazzi EM580) - Implementation of the powermeter device driver for EM580 Signed-off-by: Florin Mihut --- .gitignore | 3 +- config/bringup/config-bringup-CGEM580.yaml | 32 + config/bringup/run_tmux_helper.sh | 2 +- modules/BringUp/BUPowermeter/BUPowermeter.cpp | 4 + .../PowerMeters/CMakeLists.txt | 1 + .../CarloGavazzi_EM580/CMakeLists.txt | 22 + .../CarloGavazzi_EM580/CarloGavazzi_EM580.cpp | 16 + .../CarloGavazzi_EM580/CarloGavazzi_EM580.hpp | 64 ++ .../PowerMeters/CarloGavazzi_EM580/doc.rst | 117 +++ .../CarloGavazzi_EM580/main/base64.hpp | 539 +++++++++++ .../main/powermeterImpl.cpp | 852 ++++++++++++++++++ .../main/powermeterImpl.hpp | 95 ++ .../CarloGavazzi_EM580/main/transport.cpp | 80 ++ .../CarloGavazzi_EM580/main/transport.hpp | 133 +++ .../CarloGavazzi_EM580/main/utils.hpp | 117 +++ .../CarloGavazzi_EM580/manifest.yaml | 51 ++ 16 files changed, 2125 insertions(+), 3 deletions(-) create mode 100644 config/bringup/config-bringup-CGEM580.yaml create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CMakeLists.txt create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.cpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.hpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/doc.rst create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml diff --git a/.gitignore b/.gitignore index 35d4aefae8..13cdbe8c93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -*build -*build-cross +*build* .cache/ workspace.yaml .vscode/ diff --git a/config/bringup/config-bringup-CGEM580.yaml b/config/bringup/config-bringup-CGEM580.yaml new file mode 100644 index 0000000000..51a47104a8 --- /dev/null +++ b/config/bringup/config-bringup-CGEM580.yaml @@ -0,0 +1,32 @@ +settings: + telemetry_enabled: false +active_modules: + cgem580: + module: CarloGavazzi_EM580 + config_implementation: + main: + powermeter_device_id: 1 + timezone_offset_minutes: 60 + connections: + modbus: # required interface: serial_communication_hub + - module_id: comm_hub + implementation_id: main + + comm_hub: + module: SerialCommHub + config_implementation: + main: + serial_port: /dev/ttyUSB0 # adjust to your device path + baudrate: 115200 + parity: 0 # 0=None,1=Odd,2=Even (match your device) + + cli: + config_module: + evse_id: "DE*ENBW*BER001*EVSE01" + tariff_text: "This is just a long string to test the tariff text functionality. The kWh price is 0.30 EUR/kWh - just joking - it is 2.30 EUR/kWh" + module: BUPowermeter + standalone: true + connections: + powermeter: + - module_id: cgem580 + implementation_id: main diff --git a/config/bringup/run_tmux_helper.sh b/config/bringup/run_tmux_helper.sh index f20149da62..d2538bd253 100755 --- a/config/bringup/run_tmux_helper.sh +++ b/config/bringup/run_tmux_helper.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash PREFIX=$2 EVEREST_CONFIG_FILE=$1 diff --git a/modules/BringUp/BUPowermeter/BUPowermeter.cpp b/modules/BringUp/BUPowermeter/BUPowermeter.cpp index 455d34fc14..068056ba60 100644 --- a/modules/BringUp/BUPowermeter/BUPowermeter.cpp +++ b/modules/BringUp/BUPowermeter/BUPowermeter.cpp @@ -205,6 +205,7 @@ void BUPowermeter::ready() { types::units::Voltage vd = {}; types::units_signed::SignedMeterValue svd = {}; types::units::Current cd = {}; + types::units::Frequency fd = {}; optional_add(table_content, "Transaction start response: status", types::powermeter::transaction_request_status_to_string(tr_start.status)); @@ -256,6 +257,9 @@ void BUPowermeter::ready() { optional_add(table_content, "Powermeter: current in A: L1", powermeter.current_A.value_or(cd).L1); optional_add(table_content, "Powermeter: current in A: L2", powermeter.current_A.value_or(cd).L2); optional_add(table_content, "Powermeter: current in A: L3", powermeter.current_A.value_or(cd).L3); + optional_add(table_content, "Powermeter: frequency in Hz: L1", powermeter.frequency_Hz.value_or(fd).L1); + optional_add(table_content, "Powermeter: frequency in Hz: L2", powermeter.frequency_Hz.value_or(fd).L2); + optional_add(table_content, "Powermeter: frequency in Hz: L3", powermeter.frequency_Hz.value_or(fd).L3); optional_add(table_content, "Public key", public_key); size_t max_width = 120; diff --git a/modules/HardwareDrivers/PowerMeters/CMakeLists.txt b/modules/HardwareDrivers/PowerMeters/CMakeLists.txt index 9621d10ace..fb8764963f 100644 --- a/modules/HardwareDrivers/PowerMeters/CMakeLists.txt +++ b/modules/HardwareDrivers/PowerMeters/CMakeLists.txt @@ -4,6 +4,7 @@ ev_add_module(DZG_GSH01) ev_add_module(GenericPowermeter) ev_add_module(IsabellenhuetteIemDcr) ev_add_module(LemDCBM400600) +ev_add_module(CarloGavazzi_EM580) if(${EVEREST_ENABLE_RS_SUPPORT}) ev_add_module(RsIskraMeter) diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CMakeLists.txt b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CMakeLists.txt new file mode 100644 index 0000000000..5c9a822212 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CMakeLists.txt @@ -0,0 +1,22 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +# insert your custom targets and additional config variables here +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/powermeterImpl.cpp" + "main/transport.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.cpp new file mode 100644 index 0000000000..ccde97b715 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include "CarloGavazzi_EM580.hpp" + +namespace module { + +void CarloGavazzi_EM580::init() { + invoke_init(*p_main); +} + +void CarloGavazzi_EM580::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.hpp new file mode 100644 index 0000000000..0f9901c018 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/CarloGavazzi_EM580.hpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#ifndef CARLO_GAVAZZI_EM580_HPP +#define CARLO_GAVAZZI_EM580_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// headers for required interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf {}; + +class CarloGavazzi_EM580 : public Everest::ModuleBase { +public: + CarloGavazzi_EM580() = delete; + CarloGavazzi_EM580(const ModuleInfo& info, std::unique_ptr p_main, + std::unique_ptr r_modbus, Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), r_modbus(std::move(r_modbus)), config(config) {}; + + const std::unique_ptr p_main; + const std::unique_ptr r_modbus; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // CARLO_GAVAZZI_EM580_HPP diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/doc.rst b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/doc.rst new file mode 100644 index 0000000000..10053367c5 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/doc.rst @@ -0,0 +1,117 @@ +.. _everest_modules_handwritten_CarloGavazzi_EM580: + +******************************************* +CarloGavazzi_EM580 +******************************************* + +The module ``CarloGavazzi_EM580`` implements powermeter interface to read data from +EM580 device that is connected via Modbus RTU. + + +Testing the module +================== +We use the MQTT Explorer to interact with the CarloGavazzi_EM580 module. + +``start_transaction`` command +----------------------------- +Publish a command to the topic: ``everest/power_meter_1/meter/cmd`` + +``Request JSON:`` + + .. code-block:: JSON + + { + "data": { + "args": { + "value": { + "evse_id": "evse_id", + "transaction_id": "2995e453-6ba4-4d0f-8030-bec4396d8a63", + "client_id": "client_id", + "tariff_id": 0, + "cable_id": 0, + "user_data": "" + } + }, + "id": "00000000-0000-0000-0000-000000000042", + "origin": "manual_test" + }, + "name": "start_transaction", + "type": "call" + } + +``Response JSON:`` + + .. code-block:: JSON + + { + "data": { + "id": "00000000-0000-0000-0000-000000000042", + "origin": "power_meter_1", + "retval": { + "status": "OK" + } + }, + "name": "start_transaction", + "type": "result" + } + +``stop_transaction`` command +----------------------------- + +Publish a command to the topic: ``everest/power_meter_1/meter/cmd`` + +``Request JSON:`` + + .. code-block:: JSON + + { + "data": { + "args": { + "transaction_id": "2995e453-6ba4-4d0f-8030-bec4396d8a63" + }, + "id": "00000000-0000-0000-0000-000000000042", + "origin": "manual_test" + }, + "name": "stop_transaction", + "type": "call" + } + +``Response JSON:`` + + .. code-block:: JSON + + { + "data": { + "id": "00000000-0000-0000-0000-000000000042", + "origin": "power_meter_1", + "retval": { + "signed_meter_value": { + "encoding_method": "", + "public_key": "0464F9F9447A00672486A6D4625AA5FDB2E8D44B5705347316E7975BC8F9B29FAA11BBF44E8E1E82270267C52D1896AB240C7B4000B9BA2152DE5CCE822E3290A0B1376BFAFE4FB3956B1777EC9EE91EE0671A046BC3433F1409E44B229B5C71E9", + "signed_meter_data": "AQABCAD/AAAAHgAAAAAAAAAAAAABAAIIAP8AAAAeAAAAAAAAAAAAAAEAAQcA/wAAABv//wAAAAABAAwHAP8AAAAj//8AAAAAAQALBwD/AAAAIf/9AAAAAAEAAAoC/wAAACb//QAAAABEQ1QxQTMwVjEwTFMzRUMAAAAAAEJZMDUyMDAwMTAwMkwARENUMUEzMFYxMExTM0VDAHUjlewVGaSa37FIO2S4nVls1wH34HXM/VhqjCmVe2Dy8k/GEaa9zuMj2HY9uPlDwQ0bmq3qlIHesNgBbvcIiP7PXx/fJYrIn1/kgh/sLrUN5YkKefVBqQIkBK7vXk8KOw==", + "signing_method": "384 bit ECDSA SHA 384, using curve brainpoolP384r1" + }, + "start_signed_meter_value": { + "encoding_method": "", + "public_key": "0464F9F9447A00672486A6D4625AA5FDB2E8D44B5705347316E7975BC8F9B29FAA11BBF44E8E1E82270267C52D1896AB240C7B4000B9BA2152DE5CCE822E3290A0B1376BFAFE4FB3956B1777EC9EE91EE0671A046BC3433F1409E44B229B5C71E9", + "signed_meter_data": "AQABCAD/AAAAHgAAAAAAAAAAAAABAAIIAP8AAAAeAAAAAAAAAAAAAAEAAQcA/wAAABv//wAAAAABAAwHAP8AAAAj//8AAAAAAQALBwD/AAAAIf/9AAAAAAEAAAoC/wAAACb//QAAAABEQ1QxQTMwVjEwTFMzRUMAAAAAAEJZMDUyMDAwMTAwMkwARENUMUEzMFYxMExTM0VDACiWQqialjeOBZEW8AonMrpBmsBRnAGVsS/dya+GyhYCxq7JWfz8mywhJi9udAvJRHgspjtQYofqmM0y3c3eJSz3TFfC9TsQNOpBZ2BC+CZRJ+C2ewIsE9D1EIdJyAGG1A==", + "signing_method": "384 bit ECDSA SHA 384, using curve brainpoolP384r1" + }, + "status": "OK" + } + }, + "name": "stop_transaction", + "type": "result" + } + +Publish powermeter variables +---------------------------- +The module reads the following powermeter parameters and publishs them to the EVerest system: + +* energy_Wh_import +* energy_Wh_export +* power_W +* voltage_V +* current_A + +Publish the values ​​every second. diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp new file mode 100644 index 0000000000..fafa67d327 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef BASE64_HPP_ +#define BASE64_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_bit_cast) +#include // For std::bit_cast. +#endif + +namespace base64 { + +namespace detail { + +#if defined(__cpp_lib_bit_cast) +using std::bit_cast; +#else +template +std::enable_if_t && std::is_trivially_copyable_v, + To> +bit_cast(const From& src) noexcept { + static_assert(std::is_trivially_constructible_v, "This implementation additionally requires " + "destination type to be trivially constructible"); + + To dst; + std::memcpy(&dst, &src, sizeof(To)); + return dst; +} +#endif + +inline constexpr char padding_char{'='}; +inline constexpr uint32_t bad_char{0x01FFFFFF}; + +#if !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) +#if (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || \ + (defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN) || (defined(_BYTE_ORDER) && _BYTE_ORDER == _BIG_ENDIAN) || \ + (defined(BYTE_ORDER) && BYTE_ORDER == BIG_ENDIAN) || \ + (defined(__sun) && defined(__SVR4) && defined(_BIG_ENDIAN)) || defined(__ARMEB__) || defined(__THUMBEB__) || \ + defined(__AARCH64EB__) || defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__) || defined(_M_PPC) +#define __BIG_ENDIAN__ +#elif (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || /* gcc */ \ + (defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN) /* linux header */ \ + || (defined(_BYTE_ORDER) && _BYTE_ORDER == _LITTLE_ENDIAN) || \ + (defined(BYTE_ORDER) && BYTE_ORDER == LITTLE_ENDIAN) /* mingw header */ || \ + (defined(__sun) && defined(__SVR4) && defined(_LITTLE_ENDIAN)) || /* solaris */ \ + defined(__ARMEL__) || defined(__THUMBEL__) || defined(__AARCH64EL__) || defined(_MIPSEL) || defined(__MIPSEL) || \ + defined(__MIPSEL__) || defined(_M_IX86) || defined(_M_X64) || defined(_M_IA64) || /* msvc for intel processors */ \ + defined(_M_ARM) /* msvc code on arm executes in little endian mode */ +#define __LITTLE_ENDIAN__ +#endif +#endif + +#if !defined(__LITTLE_ENDIAN__) & !defined(__BIG_ENDIAN__) +#error "UNKNOWN Platform / endianness. Configure endianness explicitly." +#endif + +#if defined(__LITTLE_ENDIAN__) +std::array constexpr decode_table_0 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x000000f8, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x000000fc, 0x000000d0, 0x000000d4, 0x000000d8, 0x000000dc, 0x000000e0, 0x000000e4, + 0x000000e8, 0x000000ec, 0x000000f0, 0x000000f4, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00000004, 0x00000008, 0x0000000c, 0x00000010, 0x00000014, 0x00000018, + 0x0000001c, 0x00000020, 0x00000024, 0x00000028, 0x0000002c, 0x00000030, 0x00000034, 0x00000038, 0x0000003c, + 0x00000040, 0x00000044, 0x00000048, 0x0000004c, 0x00000050, 0x00000054, 0x00000058, 0x0000005c, 0x00000060, + 0x00000064, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000068, 0x0000006c, + 0x00000070, 0x00000074, 0x00000078, 0x0000007c, 0x00000080, 0x00000084, 0x00000088, 0x0000008c, 0x00000090, + 0x00000094, 0x00000098, 0x0000009c, 0x000000a0, 0x000000a4, 0x000000a8, 0x000000ac, 0x000000b0, 0x000000b4, + 0x000000b8, 0x000000bc, 0x000000c0, 0x000000c4, 0x000000c8, 0x000000cc, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +std::array constexpr decode_table_1 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000e003, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x0000f003, 0x00004003, 0x00005003, 0x00006003, 0x00007003, 0x00008003, 0x00009003, + 0x0000a003, 0x0000b003, 0x0000c003, 0x0000d003, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, + 0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000, 0x0000d000, 0x0000e000, 0x0000f000, + 0x00000001, 0x00001001, 0x00002001, 0x00003001, 0x00004001, 0x00005001, 0x00006001, 0x00007001, 0x00008001, + 0x00009001, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000a001, 0x0000b001, + 0x0000c001, 0x0000d001, 0x0000e001, 0x0000f001, 0x00000002, 0x00001002, 0x00002002, 0x00003002, 0x00004002, + 0x00005002, 0x00006002, 0x00007002, 0x00008002, 0x00009002, 0x0000a002, 0x0000b002, 0x0000c002, 0x0000d002, + 0x0000e002, 0x0000f002, 0x00000003, 0x00001003, 0x00002003, 0x00003003, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +std::array constexpr decode_table_2 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00800f00, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00c00f00, 0x00000d00, 0x00400d00, 0x00800d00, 0x00c00d00, 0x00000e00, 0x00400e00, + 0x00800e00, 0x00c00e00, 0x00000f00, 0x00400f00, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00400000, 0x00800000, 0x00c00000, 0x00000100, 0x00400100, 0x00800100, + 0x00c00100, 0x00000200, 0x00400200, 0x00800200, 0x00c00200, 0x00000300, 0x00400300, 0x00800300, 0x00c00300, + 0x00000400, 0x00400400, 0x00800400, 0x00c00400, 0x00000500, 0x00400500, 0x00800500, 0x00c00500, 0x00000600, + 0x00400600, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00800600, 0x00c00600, + 0x00000700, 0x00400700, 0x00800700, 0x00c00700, 0x00000800, 0x00400800, 0x00800800, 0x00c00800, 0x00000900, + 0x00400900, 0x00800900, 0x00c00900, 0x00000a00, 0x00400a00, 0x00800a00, 0x00c00a00, 0x00000b00, 0x00400b00, + 0x00800b00, 0x00c00b00, 0x00000c00, 0x00400c00, 0x00800c00, 0x00c00c00, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +std::array constexpr decode_table_3 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x003e0000, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x003f0000, 0x00340000, 0x00350000, 0x00360000, 0x00370000, 0x00380000, 0x00390000, + 0x003a0000, 0x003b0000, 0x003c0000, 0x003d0000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00010000, 0x00020000, 0x00030000, 0x00040000, 0x00050000, 0x00060000, + 0x00070000, 0x00080000, 0x00090000, 0x000a0000, 0x000b0000, 0x000c0000, 0x000d0000, 0x000e0000, 0x000f0000, + 0x00100000, 0x00110000, 0x00120000, 0x00130000, 0x00140000, 0x00150000, 0x00160000, 0x00170000, 0x00180000, + 0x00190000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x001a0000, 0x001b0000, + 0x001c0000, 0x001d0000, 0x001e0000, 0x001f0000, 0x00200000, 0x00210000, 0x00220000, 0x00230000, 0x00240000, + 0x00250000, 0x00260000, 0x00270000, 0x00280000, 0x00290000, 0x002a0000, 0x002b0000, 0x002c0000, 0x002d0000, + 0x002e0000, 0x002f0000, 0x00300000, 0x00310000, 0x00320000, 0x00330000, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +// TODO fix decoding tables to avoid the need for different indices in big +// endian? +inline constexpr size_t decidx0{0}; +inline constexpr size_t decidx1{1}; +inline constexpr size_t decidx2{2}; + +#elif defined(__BIG_ENDIAN__) + +std::array constexpr decode_table_0 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00f80000, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00fc0000, 0x00d00000, 0x00d40000, 0x00d80000, 0x00dc0000, 0x00e00000, 0x00e40000, + 0x00e80000, 0x00ec0000, 0x00f00000, 0x00f40000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00040000, 0x00080000, 0x000c0000, 0x00100000, 0x00140000, 0x00180000, + 0x001c0000, 0x00200000, 0x00240000, 0x00280000, 0x002c0000, 0x00300000, 0x00340000, 0x00380000, 0x003c0000, + 0x00400000, 0x00440000, 0x00480000, 0x004c0000, 0x00500000, 0x00540000, 0x00580000, 0x005c0000, 0x00600000, + 0x00640000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00680000, 0x006c0000, + 0x00700000, 0x00740000, 0x00780000, 0x007c0000, 0x00800000, 0x00840000, 0x00880000, 0x008c0000, 0x00900000, + 0x00940000, 0x00980000, 0x009c0000, 0x00a00000, 0x00a40000, 0x00a80000, 0x00ac0000, 0x00b00000, 0x00b40000, + 0x00b80000, 0x00bc0000, 0x00c00000, 0x00c40000, 0x00c80000, 0x00cc0000, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +std::array constexpr decode_table_1 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0003e000, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x0003f000, 0x00034000, 0x00035000, 0x00036000, 0x00037000, 0x00038000, 0x00039000, + 0x0003a000, 0x0003b000, 0x0003c000, 0x0003d000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, + 0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000, 0x0000d000, 0x0000e000, 0x0000f000, + 0x00010000, 0x00011000, 0x00012000, 0x00013000, 0x00014000, 0x00015000, 0x00016000, 0x00017000, 0x00018000, + 0x00019000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0001a000, 0x0001b000, + 0x0001c000, 0x0001d000, 0x0001e000, 0x0001f000, 0x00020000, 0x00021000, 0x00022000, 0x00023000, 0x00024000, + 0x00025000, 0x00026000, 0x00027000, 0x00028000, 0x00029000, 0x0002a000, 0x0002b000, 0x0002c000, 0x0002d000, + 0x0002e000, 0x0002f000, 0x00030000, 0x00031000, 0x00032000, 0x00033000, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +std::array constexpr decode_table_2 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000f80, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000fc0, 0x00000d00, 0x00000d40, 0x00000d80, 0x00000dc0, 0x00000e00, 0x00000e40, + 0x00000e80, 0x00000ec0, 0x00000f00, 0x00000f40, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00000040, 0x00000080, 0x000000c0, 0x00000100, 0x00000140, 0x00000180, + 0x000001c0, 0x00000200, 0x00000240, 0x00000280, 0x000002c0, 0x00000300, 0x00000340, 0x00000380, 0x000003c0, + 0x00000400, 0x00000440, 0x00000480, 0x000004c0, 0x00000500, 0x00000540, 0x00000580, 0x000005c0, 0x00000600, + 0x00000640, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000680, 0x000006c0, + 0x00000700, 0x00000740, 0x00000780, 0x000007c0, 0x00000800, 0x00000840, 0x00000880, 0x000008c0, 0x00000900, + 0x00000940, 0x00000980, 0x000009c0, 0x00000a00, 0x00000a40, 0x00000a80, 0x00000ac0, 0x00000b00, 0x00000b40, + 0x00000b80, 0x00000bc0, 0x00000c00, 0x00000c40, 0x00000c80, 0x00000cc0, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +std::array constexpr decode_table_3 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000003e, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x0000003f, 0x00000034, 0x00000035, 0x00000036, 0x00000037, 0x00000038, 0x00000039, + 0x0000003a, 0x0000003b, 0x0000003c, 0x0000003d, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x00000000, 0x00000001, 0x00000002, 0x00000003, 0x00000004, 0x00000005, 0x00000006, + 0x00000007, 0x00000008, 0x00000009, 0x0000000a, 0x0000000b, 0x0000000c, 0x0000000d, 0x0000000e, 0x0000000f, + 0x00000010, 0x00000011, 0x00000012, 0x00000013, 0x00000014, 0x00000015, 0x00000016, 0x00000017, 0x00000018, + 0x00000019, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000001a, 0x0000001b, + 0x0000001c, 0x0000001d, 0x0000001e, 0x0000001f, 0x00000020, 0x00000021, 0x00000022, 0x00000023, 0x00000024, + 0x00000025, 0x00000026, 0x00000027, 0x00000028, 0x00000029, 0x0000002a, 0x0000002b, 0x0000002c, 0x0000002d, + 0x0000002e, 0x0000002f, 0x00000030, 0x00000031, 0x00000032, 0x00000033, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; + +// TODO fix decoding tables to avoid the need for different indices in big +// endian? +inline constexpr size_t decidx0{1}; +inline constexpr size_t decidx1{2}; +inline constexpr size_t decidx2{3}; + +#endif + +std::array constexpr encode_table_0 = { + 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'C', 'C', 'C', 'C', 'D', 'D', 'D', 'D', 'E', 'E', 'E', 'E', 'F', 'F', + 'F', 'F', 'G', 'G', 'G', 'G', 'H', 'H', 'H', 'H', 'I', 'I', 'I', 'I', 'J', 'J', 'J', 'J', 'K', 'K', 'K', 'K', + 'L', 'L', 'L', 'L', 'M', 'M', 'M', 'M', 'N', 'N', 'N', 'N', 'O', 'O', 'O', 'O', 'P', 'P', 'P', 'P', 'Q', 'Q', + 'Q', 'Q', 'R', 'R', 'R', 'R', 'S', 'S', 'S', 'S', 'T', 'T', 'T', 'T', 'U', 'U', 'U', 'U', 'V', 'V', 'V', 'V', + 'W', 'W', 'W', 'W', 'X', 'X', 'X', 'X', 'Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z', 'a', 'a', 'a', 'a', 'b', 'b', + 'b', 'b', 'c', 'c', 'c', 'c', 'd', 'd', 'd', 'd', 'e', 'e', 'e', 'e', 'f', 'f', 'f', 'f', 'g', 'g', 'g', 'g', + 'h', 'h', 'h', 'h', 'i', 'i', 'i', 'i', 'j', 'j', 'j', 'j', 'k', 'k', 'k', 'k', 'l', 'l', 'l', 'l', 'm', 'm', + 'm', 'm', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'o', 'p', 'p', 'p', 'p', 'q', 'q', 'q', 'q', 'r', 'r', 'r', 'r', + 's', 's', 's', 's', 't', 't', 't', 't', 'u', 'u', 'u', 'u', 'v', 'v', 'v', 'v', 'w', 'w', 'w', 'w', 'x', 'x', + 'x', 'x', 'y', 'y', 'y', 'y', 'z', 'z', 'z', 'z', '0', '0', '0', '0', '1', '1', '1', '1', '2', '2', '2', '2', + '3', '3', '3', '3', '4', '4', '4', '4', '5', '5', '5', '5', '6', '6', '6', '6', '7', '7', '7', '7', '8', '8', + '8', '8', '9', '9', '9', '9', '+', '+', '+', '+', '/', '/', '/', '/'}; + +std::array constexpr encode_table_1 = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', + 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', + 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; + +} // namespace detail + +template +inline OutputBuffer encode_into(InputIterator begin, InputIterator end) { + typedef std::decay_t input_value_type; + static_assert(std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v); + typedef typename OutputBuffer::value_type output_value_type; + static_assert(std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v); + const size_t binarytextsize = end - begin; + const size_t encodedsize = (binarytextsize / 3 + (binarytextsize % 3 > 0)) << 2; + OutputBuffer encoded(encodedsize, detail::padding_char); + + const uint8_t* bytes = reinterpret_cast(&*begin); + char* currEncoding = reinterpret_cast(&encoded[0]); + + for (size_t i = binarytextsize / 3; i; --i) { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + const uint8_t t3 = *bytes++; + *currEncoding++ = detail::encode_table_0[t1]; + *currEncoding++ = detail::encode_table_1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)]; + *currEncoding++ = detail::encode_table_1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)]; + *currEncoding++ = detail::encode_table_1[t3]; + } + + switch (binarytextsize % 3) { + case 0: { + break; + } + case 1: { + const uint8_t t1 = bytes[0]; + *currEncoding++ = detail::encode_table_0[t1]; + *currEncoding++ = detail::encode_table_1[(t1 & 0x03) << 4]; + // *currEncoding++ = detail::padding_char; + // *currEncoding++ = detail::padding_char; + break; + } + case 2: { + const uint8_t t1 = bytes[0]; + const uint8_t t2 = bytes[1]; + *currEncoding++ = detail::encode_table_0[t1]; + *currEncoding++ = detail::encode_table_1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)]; + *currEncoding++ = detail::encode_table_1[(t2 & 0x0F) << 2]; + // *currEncoding++ = detail::padding_char; + break; + } + default: { + throw std::runtime_error{"Invalid base64 encoded data"}; + } + } + + return encoded; +} + +template inline OutputBuffer encode_into(std::string_view data) { + return encode_into(std::begin(data), std::end(data)); +} + +inline std::string to_base64(std::string_view data) { + return encode_into(std::begin(data), std::end(data)); +} + +template inline OutputBuffer decode_into(std::string_view base64Text) { + typedef typename OutputBuffer::value_type output_value_type; + static_assert(std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v); + if (base64Text.empty()) { + return OutputBuffer(); + } + + if ((base64Text.size() & 3) != 0) { + throw std::runtime_error{"Invalid base64 encoded data - Size not divisible by 4"}; + } + + const size_t numPadding = std::count(base64Text.rbegin(), base64Text.rbegin() + 4, '='); + if (numPadding > 2) { + throw std::runtime_error{"Invalid base64 encoded data - Found more than 2 padding signs"}; + } + + const size_t decodedsize = (base64Text.size() * 3 >> 2) - numPadding; + OutputBuffer decoded(decodedsize, '.'); + + const uint8_t* bytes = reinterpret_cast(&base64Text[0]); + char* currDecoding = reinterpret_cast(&decoded[0]); + + for (size_t i = (base64Text.size() >> 2) - (numPadding != 0); i; --i) { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + const uint8_t t3 = *bytes++; + const uint8_t t4 = *bytes++; + + const uint32_t d1 = detail::decode_table_0[t1]; + const uint32_t d2 = detail::decode_table_1[t2]; + const uint32_t d3 = detail::decode_table_2[t3]; + const uint32_t d4 = detail::decode_table_3[t4]; + + const uint32_t temp = d1 | d2 | d3 | d4; + + if (temp >= detail::bad_char) { + throw std::runtime_error{"Invalid base64 encoded data - Invalid character"}; + } + + // Use bit_cast instead of union and type punning to avoid + // undefined behaviour risk: + // https://en.wikipedia.org/wiki/Type_punning#Use_of_union + const std::array tempBytes = detail::bit_cast, uint32_t>(temp); + + *currDecoding++ = tempBytes[detail::decidx0]; + *currDecoding++ = tempBytes[detail::decidx1]; + *currDecoding++ = tempBytes[detail::decidx2]; + } + + switch (numPadding) { + case 0: { + break; + } + case 1: { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + const uint8_t t3 = *bytes++; + + const uint32_t d1 = detail::decode_table_0[t1]; + const uint32_t d2 = detail::decode_table_1[t2]; + const uint32_t d3 = detail::decode_table_2[t3]; + + const uint32_t temp = d1 | d2 | d3; + + if (temp >= detail::bad_char) { + throw std::runtime_error{"Invalid base64 encoded data - Invalid character"}; + } + + // Use bit_cast instead of union and type punning to avoid + // undefined behaviour risk: + // https://en.wikipedia.org/wiki/Type_punning#Use_of_union + const std::array tempBytes = detail::bit_cast, uint32_t>(temp); + *currDecoding++ = tempBytes[detail::decidx0]; + *currDecoding++ = tempBytes[detail::decidx1]; + break; + } + case 2: { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + + const uint32_t d1 = detail::decode_table_0[t1]; + const uint32_t d2 = detail::decode_table_1[t2]; + + const uint32_t temp = d1 | d2; + + if (temp >= detail::bad_char) { + throw std::runtime_error{"Invalid base64 encoded data - Invalid character"}; + } + + const std::array tempBytes = detail::bit_cast, uint32_t>(temp); + *currDecoding++ = tempBytes[detail::decidx0]; + break; + } + default: { + throw std::runtime_error{"Invalid base64 encoded data - Invalid padding number"}; + } + } + + return decoded; +} + +template +inline OutputBuffer decode_into(InputIterator begin, InputIterator end) { + typedef std::decay_t input_value_type; + static_assert(std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v); + std::string_view data(reinterpret_cast(&*begin), end - begin); + return decode_into(data); +} + +inline std::string from_base64(std::string_view data) { + return decode_into(data); +} + +} // namespace base64 + +#endif // BASE64_HPP_ diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp new file mode 100644 index 0000000000..004367f83e --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp @@ -0,0 +1,852 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "powermeterImpl.hpp" +#include "utils.hpp" + +const int MODBUS_BASE_ADDRESS = 300001; + +const int MODBUS_SIGNATURE_TYPE_ADDRESS = 309472; +const int MODBUS_PUBLIC_KEY_ADDRESS = 309473; + +const int MODBUS_SIGNED_MAP_ADDRESS = 302049; +const int MODBUS_SIGNED_MAP_SIGNATURE_ADDRESS = 302126; + +const int MODBUS_REAL_TIME_VALUES_ADDRESS = 300001; +const int MODBUS_REAL_TIME_VALUES_COUNT = 55; // Registers 300001-300055 + +const int MODBUS_ENERGY_EXPORT_ADDRESS = 300053; // kWh (-) TOT +const int MODBUS_ENERGY_IMPORT_ADDRESS = 300079; // kWh (+) TOT +const int MODBUS_TEMPERATURE_ADDRESS = 300776; // Internal Temperature + +const int MODBUS_FIRMWARE_MEASURE_MODULE_ADDRESS = 300771; // Measure module firmware version/revision +const int MODBUS_FIRMWARE_COMMUNICATION_MODULE_ADDRESS = 300772; // Communication module firmware version/revision +const int MODBUS_SERIAL_NUMBER_START_ADDRESS = 320481; // Serial number (7 registers: 320481-320487) +const int MODBUS_SERIAL_NUMBER_REGISTER_COUNT = 7; // 7 UINT16 registers = 14 bytes +const int MODBUS_PRODUCTION_YEAR_ADDRESS = 320488; // Production year (1 UINT16 register) + +// Time synchronization registers +const int MODBUS_UTC_TIMESTAMP_ADDRESS = 328723; // UTC Timestamp for synchronization (INT64, 4 words) +const int MODBUS_TIMEZONE_OFFSET_ADDRESS = 328722; // Local time delta in minutes (INT16, 1 word) + +// OCMF Transaction registers (Table 4.34) +const int MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS = 328673; // 7000h: OCMF Identification Status (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS = 328674; // 7001h: OCMF Identification Level (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS = 328675; // 7002h: OCMF Identification Flags 1-4 (4 UINT16) +const int MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT = 4; // 4 flags +const int MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS = 328679; // 7006h: OCMF Identification Type (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS = + 328680; // 7007h: OCMF Identification Data (CHAR[40] = 20 words) +const int MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT = 20; // 40 bytes = 20 words +const int MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS = 328700; // 701Bh: OCMF Charging point identifier type (UINT16) +const int MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS = + 328701; // 701Ch: OCMF Charging point identifier (CHAR[40] = 20 words) +const int MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT = 20; // 40 bytes = 20 words +const int MODBUS_OCMF_SESSION_MODALITY_ADDRESS = 328727; // 7036h: OCMF Session Modality (UINT16) +const uint16_t MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE = 0; // Charging vehicle +const uint16_t MODBUS_OCMF_SESSION_MODALITY_VEHICLE_TO_GRID = 1; // Vehicle to grid +const uint16_t MODBUS_OCMF_SESSION_MODALITY_BIDIRECTIONAL = 2; // Bidirectional + +// Tariff text register (Table 4.32) +// 326881 (6900h): Tariff text (CHAR[252] = 126 words) +const int MODBUS_OCMF_TARIFF_TEXT_ADDRESS = 326881; // 6900h: Tariff text (CHAR[252] = 126 words) +const int MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT = 123; // 246 bytes = 123 words +const int MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS = 328417; // 6F00h: OCMF Transaction ID Generation (UINT16) + +// Tariff update register (Table 4.33) +const int MODBUS_OCMF_TARIFF_UPDATE_ADDRESS = 327085; // 69CCh: Tariff update (UINT16) + +// OCMF Command register (Table 4.35) +// The device uses CHAR semantics for the command register: the ASCII character is stored in the MSB of the UINT16. +// Example: 'B' -> 0x4200. +const int MODBUS_OCMF_COMMAND_ADDRESS = 328737; // 7040h: OCMF Command Data (UINT16) +const uint16_t MODBUS_OCMF_COMMAND_START = 0x42; // Start transaction ('B' in MSB) +const uint16_t MODBUS_OCMF_COMMAND_END = 0x45; // End transaction ('E' in MSB) +const uint16_t MODBUS_OCMF_COMMAND_ABORT = 0x41; // Abort transaction ('A' in MSB) + +// OCMF State / status registers (Table 4.39 and related) +const int MODBUS_OCMF_STATE_ADDRESS = 328929; // 7100h: OCMF State (UINT16) +const uint16_t MODBUS_OCMF_STATE_NOT_READY = 0; // Not ready +const uint16_t MODBUS_OCMF_STATE_RUNNING = 1; // Running +const uint16_t MODBUS_OCMF_STATE_READY = 2; // Ready +const uint16_t MODBUS_OCMF_STATE_CORRUPTED = 3; // Corrupted +const int MODBUS_OCMF_STATE_SIZE_ADDRESS = 328930; // 7101h: OCMF Size (UINT16) +const int MODBUS_OCMF_STATE_FILE_ADDRESS = 328945; // 7110h: OCMF File (max theoretically 2031 words) +const int MODBUS_OCMF_STATE_FILE_WORD_COUNT = 2031; // 2031 words = 4062 bytes +const int MODBUS_OCMF_CHARGING_STATUS_ADDRESS = 328742; // 7045h: Charging status (UINT16) +const int MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS = 328723; // 7059h: Last transaction id (CHAR[]) +const int MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT = 20; // 40 bytes = 20 words +const int MODBUS_OCMF_TIME_SYNC_STATUS_ADDRESS = 328769; // 7060h: Time synchronization status (UINT16) + +// Byte offsets for Modbus register 300001-300055 (physical addresses 0000h-0036h) +// Each INT32 register is 4 bytes, each INT16 register is 2 bytes +namespace Offsets { +// Voltage registers (INT32, 4 bytes each) +constexpr size_t V_L1_N = 0; // 300001 (0000h) +constexpr size_t V_L2_N = 4; // 300003 (0002h) +constexpr size_t V_L3_N = 8; // 300005 (0004h) + +// Current registers (INT32, 4 bytes each) +constexpr size_t A_L1 = 24; // 300013 (000Ch) +constexpr size_t A_L2 = 28; // 300015 (000Eh) +constexpr size_t A_L3 = 32; // 300017 (0010h) + +// Power registers (INT32, 4 bytes each) +constexpr size_t W_L1 = 36; // 300019 (0012h) +constexpr size_t W_L2 = 40; // 300021 (0014h) +constexpr size_t W_L3 = 44; // 300023 (0016h) +constexpr size_t W_SYS = 80; // 300041 (0028h) + +// Reactive power registers (INT32, 4 bytes each) +constexpr size_t VAR_L1 = 60; // 300031 (001Eh) +constexpr size_t VAR_L2 = 64; // 300033 (0020h) +constexpr size_t VAR_L3 = 68; // 300035 (0022h) +constexpr size_t VAR_SYS = 88; // 300045 (002Ch) + +// Phase sequence register (INT16, 2 bytes) +constexpr size_t PHASE_SEQUENCE = 100; // 300051 (0032h) + +// Frequency register (INT16, 2 bytes) +constexpr size_t FREQUENCY = 102; // 300052 (0033h) +} // namespace Offsets + +// Scaling factors from Modbus document +namespace Factors { +constexpr float VOLTAGE = 0.1F; // Value weight: Volt*10 +constexpr float CURRENT = 0.001F; // Value weight: Ampere*1000 +constexpr float POWER = 0.1F; // Value weight: Watt*10 +constexpr float REACTIVE_POWER = 0.1F; // Value weight: var*10 +constexpr float FREQUENCY = 0.1F; // Value weight: Hz*10 +constexpr float ENERGY_KWH_TO_WH = 100.0F; // Value weight: kWh*10, convert to Wh (kWh*10 * 100 = Wh) +constexpr float TEMPERATURE = 0.1F; // Value weight: Temperature*10 +} // namespace Factors + +namespace module::main { + +void powermeterImpl::init() { + p_modbus_transport = move(std::make_unique( + *(mod->r_modbus.get()), config.powermeter_device_id, MODBUS_BASE_ADDRESS, config.initial_connection_retry_count, + config.initial_connection_retry_delay_ms, config.communication_retry_count, + config.communication_retry_delay_ms)); +} + +void powermeterImpl::read_signature_config() { + EVLOG_info << "Read the signature public key..."; + + enum SignatureType { + SIGNATURE_256_BIT, + SIGNATURE_384_BIT, + SIGNATURE_NONE + }; + + auto read_signature_type = [this]() { + transport::DataVector data = p_modbus_transport->fetch(MODBUS_SIGNATURE_TYPE_ADDRESS, 1); + return static_cast(modbus_utils::to_uint16(data, modbus_utils::ByteOffset{0})); + }; + + auto read_public_key = [this](int lengthInBits) { + const transport::DataVector data = + p_modbus_transport->fetch(MODBUS_PUBLIC_KEY_ADDRESS, (lengthInBits >> 3) + 1); + return modbus_utils::to_hex_string(data, modbus_utils::ByteOffset{0}, + modbus_utils::ByteLength{data.size() - 1}); + }; + + const SignatureType signature_type = read_signature_type(); + std::string signature_type_string; + + switch (signature_type) { + case SIGNATURE_256_BIT: + this->m_public_key_length_in_bits = 256; + signature_type_string = "256-bit"; + break; + case SIGNATURE_384_BIT: + this->m_public_key_length_in_bits = 384; + signature_type_string = "384-bit"; + break; + default: + signature_type_string = "none"; + throw std::runtime_error("no signature keys are configured, device is not eichrecht compliant"); + } + EVLOG_info << "Signature type detected: " << signature_type_string; + + this->m_public_key_hex = read_public_key(this->m_public_key_length_in_bits); + EVLOG_info << "Public key: " << this->m_public_key_hex; + this->publish_public_key_ocmf(this->m_public_key_hex); +} + +void powermeterImpl::read_firmware_versions() { + EVLOG_info << "Read the firmware versions..."; + + // Read measure module firmware version/revision (register 300771) + transport::DataVector measure_fw_data = p_modbus_transport->fetch(MODBUS_FIRMWARE_MEASURE_MODULE_ADDRESS, 1); + uint16_t measure_fw_value = modbus_utils::to_uint16(measure_fw_data, modbus_utils::ByteOffset{0}); + + // Parse firmware version: MSB bits 0-3 = Minor, bits 4-7 = Major, LSB = Revision + uint8_t major = (measure_fw_value >> 8) & 0xF0; + major = major >> 4; // Shift right to get actual major version (0-15) + uint8_t minor = (measure_fw_value >> 8) & 0x0F; + uint8_t revision = measure_fw_value & 0xFF; + + m_measure_module_firmware_version = fmt::format("{}.{}.{}", major, minor, revision); + EVLOG_info << "Measure module firmware version: " << m_measure_module_firmware_version; + + // Read communication module firmware version/revision (register 300772) + transport::DataVector comm_fw_data = p_modbus_transport->fetch(MODBUS_FIRMWARE_COMMUNICATION_MODULE_ADDRESS, 1); + uint16_t comm_fw_value = modbus_utils::to_uint16(comm_fw_data, modbus_utils::ByteOffset{0}); + + // Parse firmware version: MSB bits 0-3 = Minor, bits 4-7 = Major, LSB = Revision + major = (comm_fw_value >> 8) & 0xF0; + major = major >> 4; // Shift right to get actual major version (0-15) + minor = (comm_fw_value >> 8) & 0x0F; + revision = comm_fw_value & 0xFF; + + m_communication_module_firmware_version = fmt::format("{}.{}.{}", major, minor, revision); + EVLOG_info << "Communication module firmware version: " << m_communication_module_firmware_version; +} + +void powermeterImpl::read_serial_number() { + EVLOG_info << "Read the serial number..."; + // Read serial number (registers 320481-320487, 7 UINT16 registers = 14 bytes) + transport::DataVector serial_data = + p_modbus_transport->fetch(MODBUS_SERIAL_NUMBER_START_ADDRESS, MODBUS_SERIAL_NUMBER_REGISTER_COUNT); + + // Convert bytes to string (serial number is stored as ASCII) + // Modbus returns data in big-endian format: each UINT16 register is [MSB, LSB] + // So for 7 registers, we get: [reg0_MSB, reg0_LSB, reg1_MSB, reg1_LSB, ...] + // We assume the string contains only printable characters and null terminator is correctly set or at the end + std::string serial_str; + serial_str.reserve(14); + for (const auto& byte : serial_data) { + char byte_char = static_cast(byte); + // Stop at null terminator if present + if (byte_char == '\0') { + break; + } + serial_str += byte_char; + } + + // Read production year (register 320488, 1 UINT16 register) + transport::DataVector year_data = p_modbus_transport->fetch(MODBUS_PRODUCTION_YEAR_ADDRESS, 1); + uint16_t production_year = modbus_utils::to_uint16(year_data, modbus_utils::ByteOffset{0}); + + // Combine serial number and production year with a dot separator + m_serial_number = serial_str + "." + std::to_string(production_year); + EVLOG_info << "Serial number: " << m_serial_number; +} + +void powermeterImpl::configure_device() { + read_firmware_versions(); + read_serial_number(); + read_signature_config(); + // need a delay here because if the device comes from a power outage, the time sync will fail + std::this_thread::sleep_for(std::chrono::seconds(2)); + // Initial time synchronization + synchronize_time(); + // Set timezone offset + set_timezone(config.timezone_offset_minutes); + + // configure the device to use automtic transaction id generation + // write 1 to register 328672 (7000h) + EVLOG_info << "Configuring the device to use automtic transaction id generation"; + std::vector data = {0}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS, data); + + // TODO(fmihut): check if a transaction is active and if so, abort it + std::vector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1); + uint16_t state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); + if (state == MODBUS_OCMF_STATE_RUNNING) { + EVLOG_info << "A transaction is active, if it is different from the current transaction, abort it"; + std::vector last_transaction_id_data = p_modbus_transport->fetch(MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS, MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT); + auto null_pos = std::find(last_transaction_id_data.begin(), last_transaction_id_data.end(), 0); + std::string last_transaction_id(last_transaction_id_data.begin(), null_pos); + if (last_transaction_id != m_transaction_id) { + EVLOG_info << "A transaction is active, if it is different from the current transaction, abort it"; + std::vector command_data = {MODBUS_OCMF_COMMAND_ABORT}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_COMMAND_ADDRESS, command_data); + } + } +} + +void powermeterImpl::ready() { + // Retry logic is now handled by SerialCommHubTransport + std::thread live_measure_publisher_thread([this] { + std::atomic_bool device_not_configured = true; + while (true) { + try { + if (device_not_configured.load()) { + configure_device(); + device_not_configured = false; + } + read_powermeter_values(); + } catch (const std::exception& e) { + EVLOG_error << "Failed to communicate with the device, try again in 10 seconds: " << e.what(); + device_not_configured = true; + std::this_thread::sleep_for(std::chrono::seconds(10)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }); + live_measure_publisher_thread.detach(); + + // Start time synchronization thread + std::thread time_sync_thread_obj([this]() { time_sync_thread(); }); + time_sync_thread_obj.detach(); +} + +void powermeterImpl::write_transaction_registers(const types::powermeter::TransactionReq& transaction_req) { + // Helper function to convert string to Modbus CHAR array (null-terminated, padded with zeros) + // Modbus stores strings as big-endian words: [MSB, LSB] per word + auto string_to_modbus_char_array = [](const std::string& str, size_t word_count) -> std::vector { + std::vector data(word_count, 0); + size_t byte_count = word_count * 2; + size_t str_len = std::min(str.length(), byte_count - 1); // Leave space for null terminator + + // Convert string bytes to Modbus words (big-endian: MSB, LSB) + for (size_t i = 0; i < str_len; ++i) { + size_t word_idx = i / 2; + if (i % 2 == 0) { + // MSB of word + data[word_idx] = static_cast(str[i]) << 8; + } else { + // LSB of word + data[word_idx] |= static_cast(str[i]); + } + } + + // Null terminator is already handled by initialization (all zeros) + // The first byte after the string is 0, which identifies EOL + + return data; + }; + + // Helper function to convert OCMFIdentificationFlags enum to numeric value + auto flag_to_value = [](types::powermeter::OCMFIdentificationFlags flag) -> uint16_t { + switch (flag) { + case types::powermeter::OCMFIdentificationFlags::RFID_NONE: + return 0; + case types::powermeter::OCMFIdentificationFlags::RFID_PLAIN: + return 1; + case types::powermeter::OCMFIdentificationFlags::RFID_RELATED: + return 2; + case types::powermeter::OCMFIdentificationFlags::RFID_PSK: + return 3; + case types::powermeter::OCMFIdentificationFlags::OCPP_NONE: + return 0; + case types::powermeter::OCMFIdentificationFlags::OCPP_RS: + return 1; + case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH: + return 2; + case types::powermeter::OCMFIdentificationFlags::OCPP_RS_TLS: + return 3; + case types::powermeter::OCMFIdentificationFlags::OCPP_AUTH_TLS: + return 4; + case types::powermeter::OCMFIdentificationFlags::OCPP_CACHE: + return 5; + case types::powermeter::OCMFIdentificationFlags::OCPP_WHITELIST: + return 6; + case types::powermeter::OCMFIdentificationFlags::OCPP_CERTIFIED: + return 7; + case types::powermeter::OCMFIdentificationFlags::ISO15118_NONE: + return 0; + case types::powermeter::OCMFIdentificationFlags::ISO15118_PNC: + return 1; + case types::powermeter::OCMFIdentificationFlags::PLMN_NONE: + return 0; + case types::powermeter::OCMFIdentificationFlags::PLMN_RING: + return 1; + case types::powermeter::OCMFIdentificationFlags::PLMN_SMS: + return 2; + } + return 0; + }; + + // 1. Write OCMF Identification Status (register 328673, 7000h) + // 0 = NOT_ASSIGNED (False), 1 = ASSIGNED (True) + uint16_t identification_status_value = + (transaction_req.identification_status == types::powermeter::OCMFUserIdentificationStatus::ASSIGNED) ? 1 : 0; + std::vector status_data = {identification_status_value}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS, status_data); + + // 2. Write OCMF Identification Level (register 328674, 7001h) - optional + uint16_t identification_level_value = 0; // Default: NONE + if (transaction_req.identification_level.has_value()) { + switch (transaction_req.identification_level.value()) { + case types::powermeter::OCMFIdentificationLevel::NONE: + identification_level_value = 0; + break; + case types::powermeter::OCMFIdentificationLevel::HEARSAY: + identification_level_value = 1; + break; + case types::powermeter::OCMFIdentificationLevel::TRUSTED: + identification_level_value = 2; + break; + case types::powermeter::OCMFIdentificationLevel::VERIFIED: + identification_level_value = 3; + break; + case types::powermeter::OCMFIdentificationLevel::CERTIFIED: + identification_level_value = 4; + break; + case types::powermeter::OCMFIdentificationLevel::SECURE: + identification_level_value = 5; + break; + case types::powermeter::OCMFIdentificationLevel::MISMATCH: + identification_level_value = 6; + break; + case types::powermeter::OCMFIdentificationLevel::INVALID: + identification_level_value = 7; + break; + case types::powermeter::OCMFIdentificationLevel::OUTDATED: + identification_level_value = 8; + break; + case types::powermeter::OCMFIdentificationLevel::UNKNOWN: + identification_level_value = 9; + break; + } + } + std::vector level_data = {identification_level_value}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS, level_data); + + // 3. Write OCMF Identification Flags (registers 328675-328678, 7002h-7005h) - up to 4 flags + std::vector flags_data(MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT, 0); + for (size_t i = 0; i < transaction_req.identification_flags.size() && i < MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT; + ++i) { + flags_data[i] = flag_to_value(transaction_req.identification_flags[i]); + } + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS, flags_data); + + // 4. Write OCMF Identification Type (register 328679, 7006h) + uint16_t identification_type_value = 0; // Default: NONE + switch (transaction_req.identification_type) { + case types::powermeter::OCMFIdentificationType::NONE: + identification_type_value = 0; + break; + case types::powermeter::OCMFIdentificationType::DENIED: + identification_type_value = 1; + break; + case types::powermeter::OCMFIdentificationType::UNDEFINED: + identification_type_value = 2; + break; + case types::powermeter::OCMFIdentificationType::ISO14443: + identification_type_value = 10; + break; + case types::powermeter::OCMFIdentificationType::ISO15693: + identification_type_value = 11; + break; + case types::powermeter::OCMFIdentificationType::EMAID: + identification_type_value = 20; + break; + case types::powermeter::OCMFIdentificationType::EVCCID: + identification_type_value = 21; + break; + case types::powermeter::OCMFIdentificationType::EVCOID: + identification_type_value = 30; + break; + case types::powermeter::OCMFIdentificationType::ISO7812: + identification_type_value = 40; + break; + case types::powermeter::OCMFIdentificationType::CARD_TXN_NR: + identification_type_value = 50; + break; + case types::powermeter::OCMFIdentificationType::CENTRAL: + identification_type_value = 60; + break; + case types::powermeter::OCMFIdentificationType::CENTRAL_1: + identification_type_value = 61; + break; + case types::powermeter::OCMFIdentificationType::CENTRAL_2: + identification_type_value = 62; + break; + case types::powermeter::OCMFIdentificationType::LOCAL: + identification_type_value = 70; + break; + case types::powermeter::OCMFIdentificationType::LOCAL_1: + identification_type_value = 71; + break; + case types::powermeter::OCMFIdentificationType::LOCAL_2: + identification_type_value = 72; + break; + case types::powermeter::OCMFIdentificationType::PHONE_NUMBER: + identification_type_value = 80; + break; + case types::powermeter::OCMFIdentificationType::KEY_CODE: + identification_type_value = 90; + break; + } + std::vector type_data = {identification_type_value}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS, type_data); + + // 5. Write OCMF Identification Data (registers 328680-328699, 7007h-701Ah) - CHAR[40] = 20 words + // Format: identification_data + ',' + transaction_id + // Max length: 40 characters - the transaction_id is 36 characters max + std::string client_id_str = transaction_req.identification_data.value_or(""); + std::vector id_data = + string_to_modbus_char_array(client_id_str, MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT); + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS, id_data); + + // 6. Write OCMF Charging point identifier type (register 328700, 701Bh) + // 0 = EVSEID, 1 = CBIDC (default to EVSEID) + uint16_t charging_point_id_type = 0; // EVSEID + std::vector id_type_data = {charging_point_id_type}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS, id_type_data); + + // 7. Write OCMF Charging point identifier (registers 328701-328720, 701Ch-702Fh) - CHAR[40] = 20 words (evse_id) + std::vector evse_id_data = + string_to_modbus_char_array(transaction_req.evse_id, MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT); + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS, evse_id_data); + + // 8. Write tariff text (register 326881, 6900h) - CHAR[252] = 126 words + // The meter expects a null-terminated string; the helper initializes all words to 0, + // so if the string is shorter than the maximum length, the first byte after the content is 0. + const std::string tariff_text = transaction_req.tariff_text.value_or("") + '|' + transaction_req.transaction_id; + std::vector tariff_text_data = + string_to_modbus_char_array(tariff_text, MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT); + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_TARIFF_TEXT_ADDRESS, tariff_text_data); +} + +types::powermeter::TransactionStartResponse +powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) { + try { + EVLOG_info << "Starting transaction with transaction id: " << value.transaction_id + << " and evse id: " << value.evse_id << " and identification status: " << value.identification_status + << " and identification type: " + << types::powermeter::ocmfidentification_type_to_string(value.identification_type) + << " and identification level: " + << types::powermeter::ocmfidentification_level_to_string( + value.identification_level.value_or(types::powermeter::OCMFIdentificationLevel::NONE)) + << " and identification data: " << value.identification_data.value_or("") + << " and tariff text: " << value.tariff_text.value_or("none"); + // Write transaction registers first + write_transaction_registers(value); + + std::vector session_modality_data = {MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_SESSION_MODALITY_ADDRESS, session_modality_data); + + // Write 'B' command to start transaction (Table 4.35, register 328737) + std::vector command_data1 = {MODBUS_OCMF_COMMAND_START}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_COMMAND_ADDRESS, command_data1); + + // Track local state (only used internally, not in device dump) + m_transaction_active = true; + m_transaction_id = value.transaction_id; + return {types::powermeter::TransactionRequestStatus::OK}; + } catch (const std::exception& e) { + EVLOG_error << __PRETTY_FUNCTION__ << " Error: " << e.what() << std::endl; + return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "get_signed_meter_value_error"}; + } +} + +types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) { + try { + // Write 'E' command to end transaction (Table 4.35, register 328737) + std::vector command_data = {MODBUS_OCMF_COMMAND_END}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_COMMAND_ADDRESS, command_data); + + // check if the OCMF state is ready (Table 4.36, register 328742) + uint16_t state = MODBUS_OCMF_STATE_NOT_READY; + transport::DataVector state_data; + int retries = 0; + while (state != MODBUS_OCMF_STATE_READY) { + state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1); + state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); + if (state == MODBUS_OCMF_STATE_CORRUPTED || state == MODBUS_OCMF_STATE_RUNNING) { + return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, + {}, + {}, + "get_signed_meter_value_error"}; + } + if (state != MODBUS_OCMF_STATE_READY) { + EVLOG_info << "OCMF state: " << state; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + retries++; + if (retries > 10) { + return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, + {}, + {}, + "get_signed_meter_value_error"}; + } + } + } + + // read the size of the OCMF file + transport::DataVector size_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_SIZE_ADDRESS, 1); + uint16_t size = modbus_utils::to_uint16(size_data, modbus_utils::ByteOffset{0}); + if (size == 0) { + throw std::runtime_error("OCMF file size is 0"); + } + + // read the OCMF file + transport::DataVector file_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_FILE_ADDRESS, size); + std::string ocmf_data{file_data.begin(), file_data.end()}; + EVLOG_info << "OCMF file: " << ocmf_data; + auto signed_meter_value = types::units_signed::SignedMeterValue{ocmf_data, "", "OCMF"}; + signed_meter_value.public_key.emplace(m_public_key_hex); + + // write 0 to the OCMF state to confirm the reading of the OCMF file + std::vector ocmf_confirmation_data = {MODBUS_OCMF_STATE_NOT_READY}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_STATE_ADDRESS, ocmf_confirmation_data); + return types::powermeter::TransactionStopResponse{types::powermeter::TransactionRequestStatus::OK, + {}, // Empty start_signed_meter_value + signed_meter_value}; + } catch (const std::exception& e) { + EVLOG_error << __PRETTY_FUNCTION__ << " Error: " << e.what() << std::endl; + return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "get_signed_meter_value_error"}; + } +} + +void powermeterImpl::read_powermeter_values() { + try { + // Read registers 300001-300055 (physical addresses 0000h-0036h) + // Total: 55 words (0x37 = 55 decimal) + // According to Modbus document Table 4.1: + // - 300001-300011: Phase voltages (6 values, 12 words) + // - 300013-300017: Phase currents (3 values, 6 words) + // - 300019-300023: Phase powers (3 values, 6 words) + // - 300025-300029: Phase apparent powers (3 values, 6 words) + // - 300031-300035: Phase reactive powers (3 values, 6 words) + // - 300037-300045: System values (5 values, 10 words) + // - 300047-300050: Power factors (4 values, 4 words) + // - 300051: Phase sequence (1 word) + // - 300052-300055: Inductive/Capacitive load indicators (4 values, 4 words) + transport::DataVector data = + p_modbus_transport->fetch(MODBUS_REAL_TIME_VALUES_ADDRESS, MODBUS_REAL_TIME_VALUES_COUNT); + + types::powermeter::Powermeter powermeter{}; + powermeter.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); + powermeter.meter_id = std::move(std::string(this->mod->info.id)); + + // Voltage values (INT32, weight: Volt*10) + // 300001 (0000h): V L1-N + // 300003 (0002h): V L2-N + // 300005 (0004h): V L3-N + types::units::Voltage voltage_V; + voltage_V.L1 = Factors::VOLTAGE * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L1_N})); + voltage_V.L2 = Factors::VOLTAGE * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L2_N})); + voltage_V.L3 = Factors::VOLTAGE * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L3_N})); + powermeter.voltage_V = voltage_V; + + // Current values (INT32, weight: Ampere*1000) + // Values are already signed: positive = import, negative = export + // 300013 (000Ch): A L1 + // 300015 (000Eh): A L2 + // 300017 (0010h): A L3 + types::units::Current current_A; + current_A.L1 = Factors::CURRENT * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L1})); + current_A.L2 = Factors::CURRENT * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L2})); + current_A.L3 = Factors::CURRENT * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L3})); + powermeter.current_A = current_A; + + // Power values (INT32, weight: Watt*10) + // Values are already signed: positive = import, negative = export + // 300019 (0012h): W L1 + // 300021 (0014h): W L2 + // 300023 (0016h): W L3 + // 300041 (0028h): W sys + types::units::Power power_W; + power_W.L1 = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L1})); + power_W.L2 = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L2})); + power_W.L3 = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L3})); + power_W.total = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_SYS})); + powermeter.power_W = power_W; + + // Reactive power values (INT32, weight: var*10) + // Values are already signed: positive = import, negative = export + // 300031 (001Eh): var L1 + // 300033 (0020h): var L2 + // 300035 (0022h): var L3 + // 300045 (002Ch): var sys + types::units::ReactivePower VAR; + VAR.L1 = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L1})); + VAR.L2 = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L2})); + VAR.L3 = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L3})); + VAR.total = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_SYS})); + powermeter.VAR = VAR; + + // Frequency (INT16, weight: Hz*10) - register 300052 (0033h) + // Note: Frequency is also available at 300273 and 301341 as INT32 with different factors, + // but we use 300052 (INT16) to keep the bulk read compact (300001-300055) + types::units::Frequency frequency_Hz; + frequency_Hz.L1 = + Factors::FREQUENCY * + static_cast(modbus_utils::to_int16(data, modbus_utils::ByteOffset{Offsets::FREQUENCY})); + powermeter.frequency_Hz = frequency_Hz; + + // Phase sequence (INT16) - register 300051 (0032h) + // Value -1 = L1-L3-L2 sequence, value 1 = L1-L2-L3 sequence + int16_t phase_sequence = modbus_utils::to_int16(data, modbus_utils::ByteOffset{Offsets::PHASE_SEQUENCE}); + if (phase_sequence == -1) { + powermeter.phase_seq_error = true; // L1-L3-L2 is considered an error (counter-clockwise) + } else if (phase_sequence == 1) { + powermeter.phase_seq_error = false; // L1-L2-L3 is correct (clockwise) + } + // If phase_sequence is neither -1 nor 1, leave phase_seq_error unset + + // Read energy values (INT32, weight: kWh*10) - need separate reads as they're outside 300001-300055 range + // Energy export: register 300053 (kWh (-) TOT) - 2 words + transport::DataVector energy_export_data = p_modbus_transport->fetch(MODBUS_ENERGY_EXPORT_ADDRESS, 2); + types::units::Energy energy_Wh_export; + energy_Wh_export.total = + Factors::ENERGY_KWH_TO_WH * + static_cast(modbus_utils::to_int32(energy_export_data, modbus_utils::ByteOffset{0})); + powermeter.energy_Wh_export = energy_Wh_export; + + // Energy import: register 300079 (kWh (+) TOT) - 2 words + // Note: energy_Wh_import is a required field, not optional + transport::DataVector energy_import_data = p_modbus_transport->fetch(MODBUS_ENERGY_IMPORT_ADDRESS, 2); + powermeter.energy_Wh_import.total = + Factors::ENERGY_KWH_TO_WH * + static_cast(modbus_utils::to_int32(energy_import_data, modbus_utils::ByteOffset{0})); + + // Read internal temperature (INT16, weight: Temperature*10) - register 300776 (0307h) - 1 word + transport::DataVector temperature_data = p_modbus_transport->fetch(MODBUS_TEMPERATURE_ADDRESS, 1); + types::temperature::Temperature temperature; + temperature.temperature = + Factors::TEMPERATURE * + static_cast(modbus_utils::to_int16(temperature_data, modbus_utils::ByteOffset{0})); + temperature.location = "Internal"; + std::vector temperatures; + temperatures.push_back(temperature); + powermeter.temperatures = temperatures; + + this->publish_powermeter(powermeter); + } catch (const std::exception& e) { + // we catch std::exception& here since there may be other exceptions than std::runtime_error + EVLOG_warning << __PRETTY_FUNCTION__ << " Exception caught: " << e.what() << "\n"; + // Don't exit - continue trying in the next iteration + } +} + +void powermeterImpl::dump_device_state(void) { + try { + // 1. OCMF state + transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1); + uint16_t state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); + + // 2. Charging status (register 328742 / 7045h) + transport::DataVector charging_status_data = p_modbus_transport->fetch(MODBUS_OCMF_CHARGING_STATUS_ADDRESS, 1); + uint16_t charging_status = modbus_utils::to_uint16(charging_status_data, modbus_utils::ByteOffset{0}); + + // 3. Last transaction id (register 328723 / 7059h, CHAR[]) + transport::DataVector last_tx_data = p_modbus_transport->fetch(MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS, + MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT); + auto null_pos = std::find(last_tx_data.begin(), last_tx_data.end(), 0); + std::string last_tx_id(last_tx_data.begin(), null_pos); + + // 4. Time synchronization status (register 328769 / 7060h) + transport::DataVector time_sync_status_data = + p_modbus_transport->fetch(MODBUS_OCMF_TIME_SYNC_STATUS_ADDRESS, 1); + uint16_t time_sync_status = modbus_utils::to_uint16(time_sync_status_data, modbus_utils::ByteOffset{0}); + + // 5. OCMF command (last written command value) + transport::DataVector cmd_data = p_modbus_transport->fetch(MODBUS_OCMF_COMMAND_ADDRESS, 1); + uint16_t raw_cmd = modbus_utils::to_uint16(cmd_data, modbus_utils::ByteOffset{0}); + char cmd_char = static_cast((raw_cmd >> 8U) & 0xFFU); + + // 6. Transaction ID definition (OCMF transaction ID generation) + transport::DataVector tx_def_data = p_modbus_transport->fetch(MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS, 1); + uint16_t tx_def = modbus_utils::to_uint16(tx_def_data, modbus_utils::ByteOffset{0}); + + EVLOG_info << "EM580 device state dump:"; + EVLOG_info << " OCMF state: " << state; + EVLOG_info << " Charging status (device, raw): " << charging_status; + EVLOG_info << " Last transaction id (device): " << last_tx_id; + EVLOG_info << " Time synchronization status (device, raw): " << time_sync_status; + EVLOG_info << " Last OCMF command (raw): 0x" << std::hex << raw_cmd << " ('" << cmd_char << "')"; + EVLOG_info << " Transaction ID definition (OCMF): 0x" << std::hex << tx_def; + } catch (const std::exception& e) { + EVLOG_error << "Failed to dump EM580 device state: " << e.what(); + } +} + +bool powermeterImpl::is_transaction_active() const { + return m_transaction_active.load(); +} + +void powermeterImpl::synchronize_time() { + // Get current UTC time as seconds since Unix epoch + auto now_utc = date::utc_clock::now(); + // Convert to system_clock for time_t conversion + auto sys_now = std::chrono::system_clock::now(); + auto time_since_epoch = sys_now.time_since_epoch(); + int64_t seconds_since_epoch = std::chrono::duration_cast(time_since_epoch).count(); + + // Convert to UINT64 and split into 4 words (big-endian) + uint64_t timestamp = static_cast(seconds_since_epoch); + std::vector data; + data.push_back(static_cast((timestamp >> 48) & 0xFFFF)); + data.push_back(static_cast((timestamp >> 32) & 0xFFFF)); + data.push_back(static_cast((timestamp >> 16) & 0xFFFF)); + data.push_back(static_cast(timestamp & 0xFFFF)); + + // Write UTC timestamp to register 328723 (4 words for INT64) + p_modbus_transport->write_multiple_registers(MODBUS_UTC_TIMESTAMP_ADDRESS, data); + + EVLOG_info << "Time synchronized: " << Everest::Date::to_rfc3339(now_utc) + << " (Unix timestamp: " << seconds_since_epoch << ")"; +} + +void powermeterImpl::set_timezone(int offset_minutes) { + EVLOG_info << "Try to set the timezone ... "; + + // Convert to INT16 (signed 16-bit integer) + // Timezone offset range: -1440 to +1440 minutes is validated by the manifest. + int16_t offset_int16 = static_cast(offset_minutes); + std::vector data; + data.push_back(static_cast(offset_int16)); + p_modbus_transport->write_multiple_registers(MODBUS_TIMEZONE_OFFSET_ADDRESS, data); + + EVLOG_info << "Timezone set to: " << (offset_minutes >= 0 ? "+" : "") << offset_minutes << " minutes"; +} + +void powermeterImpl::time_sync_thread() { + const auto sync_interval = std::chrono::hours(1); + auto next_sync_time = std::chrono::steady_clock::now() + sync_interval; + + while (true) { + std::this_thread::sleep_until(next_sync_time); + + if (!is_transaction_active()) { + // No active transaction, perform time sync immediately + try { + synchronize_time(); + m_pending_time_sync = false; + } catch (const std::exception& e) { + EVLOG_error << "Time synchronization failed: " << e.what(); + // Mark as pending to retry when transaction ends + m_pending_time_sync = true; + } + } else { + // Transaction is active, mark sync as pending + EVLOG_info << "Time synchronization deferred: charging session in progress"; + m_pending_time_sync = true; + } + + // Schedule next sync attempt in 1 hour + next_sync_time += sync_interval; + } +} + +} // namespace module::main diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp new file mode 100644 index 0000000000..312b117ba4 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#ifndef MAIN_POWERMETER_IMPL_HPP +#define MAIN_POWERMETER_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../CarloGavazzi_EM580.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +#include "transport.hpp" +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace main { + +struct Conf { + int powermeter_device_id; + int communication_retry_count; + int communication_retry_delay_ms; + int initial_connection_retry_count; + int initial_connection_retry_delay_ms; + int timezone_offset_minutes; +}; + +class powermeterImpl : public powermeterImplBase { +public: + powermeterImpl() = delete; + powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + powermeterImplBase(ev, "main"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual types::powermeter::TransactionStartResponse + handle_start_transaction(types::powermeter::TransactionReq& value) override; + virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + std::unique_ptr p_modbus_transport; + + std::optional m_start_signed_meter_value; + + int m_public_key_length_in_bits; + std::string m_public_key_hex; + std::string m_transaction_id; + std::string m_measure_module_firmware_version; + std::string m_communication_module_firmware_version; + std::string m_serial_number; + + std::atomic_bool m_transaction_active{false}; + std::atomic_bool m_pending_time_sync{false}; + + virtual void init() override; + void configure_device(); + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + void read_signature_config(); + void read_powermeter_values(); + void dump_device_state(void); + void read_firmware_versions(); + void read_serial_number(); + void synchronize_time(); + void set_timezone(int offset_minutes); + void time_sync_thread(); + [[nodiscard]] bool is_transaction_active() const; + void write_transaction_registers(const types::powermeter::TransactionReq& transaction_req); + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace main +} // namespace module + +#endif // MAIN_POWERMETER_IMPL_HPP diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp new file mode 100644 index 0000000000..d88d8d1146 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include "transport.hpp" + +const int MAX_REGISTER_PER_MESSAGE = 125; + +namespace transport { + +transport::DataVector SerialCommHubTransport::fetch(int address, int register_count) { + return retry_with_config([this, address, register_count]() { + transport::DataVector response; + response.reserve(register_count * 2); // this is a uint 8 vector + + int remaining_register_to_read{register_count}; + int read_address{address - m_base_address}; + + while (remaining_register_to_read > 0) { + std::size_t register_to_read = remaining_register_to_read > MAX_REGISTER_PER_MESSAGE + ? MAX_REGISTER_PER_MESSAGE + : remaining_register_to_read; + + types::serial_comm_hub_requests::Result serial_com_hub_result = + m_serial_hub.call_modbus_read_input_registers(m_device_id, read_address, register_to_read); + + if (not serial_com_hub_result.value.has_value()) + throw std::runtime_error("no result from serial com hub!"); + + // make sure that returned vector is a int32 vector + static_assert(std::is_same_v); + + union { + int32_t val_32; + struct { + uint8_t v3; + uint8_t v2; + uint8_t v1; + uint8_t v0; + } val_8; + } swapit; + + static_assert(sizeof(swapit.val_32) == sizeof(swapit.val_8)); + + transport::DataVector tmp{}; + + for (auto item : serial_com_hub_result.value.value()) { + swapit.val_32 = item; + tmp.push_back(swapit.val_8.v2); + tmp.push_back(swapit.val_8.v3); + } + + response.insert(response.end(), tmp.begin(), tmp.end()); + + read_address += register_to_read; + remaining_register_to_read -= register_to_read; + } + + return response; + }); +} + +void SerialCommHubTransport::write_multiple_registers(int address, const std::vector& data) { + retry_with_config_void([this, address, &data]() { + int write_address = address - m_base_address; + types::serial_comm_hub_requests::VectorUint16 data_raw; + for (uint16_t value : data) { + data_raw.data.push_back(value); + } + + types::serial_comm_hub_requests::StatusCodeEnum status = + m_serial_hub.call_modbus_write_multiple_registers(m_device_id, write_address, data_raw); + + if (status != types::serial_comm_hub_requests::StatusCodeEnum::Success) { + throw std::runtime_error("Failed to write Modbus registers: " + std::to_string(static_cast(status))); + } + }); +} + +} // namespace transport diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp new file mode 100644 index 0000000000..314671429e --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#ifndef POWERMETER_TRANSPORT_HPP +#define POWERMETER_TRANSPORT_HPP + +/** + * Baseclass for transport classes. + * + * Transports are: + * - direct connection via modbus + * - connection via SerialComHub + */ + +#include +#include +#include +#include +#include + +namespace transport { + +using DataVector = std::vector; + +class AbstractModbusTransport { + +public: + virtual transport::DataVector fetch(int address, int register_count) = 0; + virtual void write_multiple_registers(int address, const std::vector& data) = 0; +}; + +/** + * data transport via SerialComHub + */ + +class SerialCommHubTransport : public AbstractModbusTransport { + +protected: + serial_communication_hubIntf& m_serial_hub; + int m_device_id; + int m_base_address; + + // Retry configuration + int m_initial_retry_count; + int m_initial_retry_delay_ms; + int m_normal_retry_count; + int m_normal_retry_delay_ms; + + // State tracking + std::atomic_bool m_initial_connection_mode{true}; + + // Internal retry helper for functions that return a value + template auto retry_with_config(Func&& func) -> decltype(std::forward(func)()) { + bool is_initial = m_initial_connection_mode.load(); + int max_retries = is_initial ? m_initial_retry_count : m_normal_retry_count; + int delay_ms = is_initial ? m_initial_retry_delay_ms : m_normal_retry_delay_ms; + + // For initial connection, 0 means infinite retries + int attempt = 1; + while (m_initial_retry_count == 0 ? true : attempt <= max_retries) { + try { + auto result = std::forward(func)(); + // First successful call - switch to normal mode + if (m_initial_connection_mode.exchange(false)) { + // Switched from initial to normal mode + } + return result; + } catch (const std::exception& e) { + bool should_retry = is_initial && m_initial_retry_count == 0 ? true : attempt < max_retries; + if (should_retry) { + EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries + << "): " << e.what() << ". Retrying in " << delay_ms << "ms..."; + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + } else { + EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what(); + rethrow_exception(std::current_exception()); + } + attempt++; + } + } + } + + // Internal retry helper for void functions + template void retry_with_config_void(Func&& func) { + bool is_initial = m_initial_connection_mode.load(); + int max_retries = is_initial ? m_initial_retry_count : m_normal_retry_count; + int delay_ms = is_initial ? m_initial_retry_delay_ms : m_normal_retry_delay_ms; + + // For initial connection, 0 means infinite retries + int attempt = 1; + while (m_initial_retry_count == 0 ? true : attempt <= max_retries) { + try { + std::forward(func)(); + // First successful call - switch to normal mode + if (m_initial_connection_mode.exchange(false)) { + // Switched from initial to normal mode + } + return; + } catch (const std::exception& e) { + bool should_retry = is_initial && m_initial_retry_count == 0 ? true : attempt < max_retries; + if (should_retry) { + EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries + << "): " << e.what() << ". Retrying in " << delay_ms << "ms..."; + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + } else { + EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what(); + rethrow_exception(std::current_exception()); + } + attempt++; + } + } + } + +public: + SerialCommHubTransport(serial_communication_hubIntf& serial_hub, int device_id, int base_address, + int initial_retry_count, int initial_retry_delay_ms, int normal_retry_count, + int normal_retry_delay_ms) : + m_serial_hub(serial_hub), + m_device_id(device_id), + m_base_address(base_address), + m_initial_retry_count(initial_retry_count), + m_initial_retry_delay_ms(initial_retry_delay_ms), + m_normal_retry_count(normal_retry_count), + m_normal_retry_delay_ms(normal_retry_delay_ms) { + } + + virtual transport::DataVector fetch(int address, int register_count) override; + virtual void write_multiple_registers(int address, const std::vector& data) override; +}; + +} // namespace transport + +#endif // POWERMETER_TRANSPORT_HPP diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp new file mode 100644 index 0000000000..12f70e340c --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#ifndef POWERMETER_UTILS_HPP +#define POWERMETER_UTILS_HPP + +#include +#include +#include +#include +#include + +#include "base64.hpp" +#include "transport.hpp" + +/** + * @brief Utility functions for converting Modbus data to various types + * + * These functions handle byte order conversion from Modbus (big-endian) format + * to native integer types. Modbus transmits data in big-endian format where + * the most significant byte comes first. + */ + +namespace modbus_utils { + +// Strong type wrappers to prevent parameter swapping +struct ByteOffset { + explicit ByteOffset(transport::DataVector::size_type v) : value(v) {} + operator transport::DataVector::size_type() const { return value; } + +private: + transport::DataVector::size_type value; +}; + +struct ByteLength { + explicit ByteLength(transport::DataVector::size_type v) : value(v) {} + operator transport::DataVector::size_type() const { return value; } + +private: + transport::DataVector::size_type value; +}; + +/** + * @brief Convert 4 bytes from Modbus data to int32_t + * @param data The Modbus data vector + * @param offset Byte offset into the data vector + * @return The converted 32-bit signed integer + * @note According to EM580 Modbus spec: byte order within word is MSB->LSB, + * but for INT32/UINT32/UINT64, word order is LSW->MSW. + * So bytes are arranged as: [LSW_MSB, LSW_LSB, MSW_MSB, MSW_LSB] + * which becomes: MSW_MSB MSW_LSB LSW_MSB LSW_LSB + */ +inline int32_t to_int32(const transport::DataVector& data, ByteOffset offset) { + // Original byte order: [byte0, byte1, byte2, byte3] = [LSW_MSB, LSW_LSB, MSW_MSB, MSW_LSB] + // Convert to: MSW_MSB MSW_LSB LSW_MSB LSW_LSB = byte2<<24 | byte3<<16 | byte0<<8 | byte1 + const auto off = static_cast(offset); + return static_cast(data[off + 2] << 24 | data[off + 3] << 16 | data[off] << 8 | data[off + 1]); +} + +/** + * @brief Convert 2 bytes from Modbus data to uint16_t (big-endian) + * @param data The Modbus data vector + * @param offset Byte offset into the data vector + * @return The converted 16-bit unsigned integer + */ +inline uint16_t to_uint16(const transport::DataVector& data, ByteOffset offset) { + const auto off = static_cast(offset); + return static_cast(data[off] << 8 | data[off + 1]); +} + +/** + * @brief Convert 2 bytes from Modbus data to int16_t (big-endian) + * @param data The Modbus data vector + * @param offset Byte offset into the data vector + * @return The converted 16-bit signed integer + */ +inline int16_t to_int16(const transport::DataVector& data, ByteOffset offset) { + uint16_t raw = to_uint16(data, offset); + return static_cast(raw); +} + +/** + * @brief Convert a range of bytes to a hexadecimal string representation + * @param data The Modbus data vector + * @param offset Byte offset into the data vector + * @param length Number of bytes to convert + * @return Hexadecimal string (uppercase, no separators) + */ +inline std::string to_hex_string(const transport::DataVector& data, ByteOffset offset, ByteLength length) { + const auto off = static_cast(offset); + const auto len = static_cast(length); + std::stringstream ss; + for (std::size_t index = 0; index < len; ++index) { + ss << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << static_cast(data[index + off]); + } + return ss.str(); +} + +/** + * @brief Convert a range of bytes to a Base64-encoded string + * @param data The Modbus data vector + * @param offset Byte offset into the data vector + * @param length Number of bytes to convert + * @return Base64-encoded string + */ +inline std::string to_base64_string(const transport::DataVector& data, ByteOffset offset, ByteLength length) { + const auto off = static_cast(offset); + const auto len = static_cast(length); + auto begin = std::begin(data) + off; + auto end = begin + len; + return base64::encode_into(begin, end); +} + +} // namespace modbus_utils + +#endif // POWERMETER_UTILS_HPP + diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml new file mode 100644 index 0000000000..3e21c6315f --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml @@ -0,0 +1,51 @@ +description: Carlo Gavazzi EM580 powermeter +provides: + main: + description: Implementation of the driver functionality + interface: powermeter + config: + powermeter_device_id: + description: The powermeter's address on the serial bus + type: integer + minimum: 0 + maximum: 255 + default: 1 + communication_retry_count: + description: Number of retries for communication operations before giving up. + type: integer + minimum: 1 + maximum: 100 + default: 3 + communication_retry_delay_ms: + description: Delay in milliseconds between retry attempts. + type: integer + minimum: 10 + maximum: 10000 + default: 500 + initial_connection_retry_count: + description: Number of retries for initial connection/signature config read during module initialization. 0 means no infinite retries. + type: integer + minimum: 0 + maximum: 100 + default: 10 + initial_connection_retry_delay_ms: + description: Delay in milliseconds between retry attempts during initialization. + type: integer + minimum: 100 + maximum: 60000 + default: 2000 + timezone_offset_minutes: + description: Timezone offset from UTC in minutes (e.g., 60 for UTC+1, -300 for UTC-5). Range -1440 to +1440 minutes. Default is 0 (UTC). + type: integer + minimum: -1440 + maximum: 1440 + default: 0 +requires: + modbus: + interface: serial_communication_hub +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - florin.mihut@pionix.com +enable_external_mqtt: false + From 21579deb695e461291dc920daa8efec21aa77d37 Mon Sep 17 00:00:00 2001 From: Florin Mihut Date: Wed, 17 Dec 2025 16:54:29 +0100 Subject: [PATCH 2/5] Starting error handling implementation Signed-off-by: Florin Mihut --- config/bringup/config-bringup-CGEM580.yaml | 3 +- modules/BringUp/BUPowermeter/BUPowermeter.cpp | 14 +- .../main/powermeterImpl.cpp | 593 +++++++++++------- .../main/powermeterImpl.hpp | 2 + .../CarloGavazzi_EM580/main/transport.cpp | 19 +- .../CarloGavazzi_EM580/main/transport.hpp | 77 ++- .../ocmf_validation/README.md | 179 ++++++ .../ocmf_validation/test_validation.sh | 29 + .../ocmf_validation/text.txt | 1 + .../validate_ocmf_signature.py | 365 +++++++++++ .../main/serial_communication_hubImpl.cpp | 20 +- types/serial_comm_hub_requests.yaml | 1 + 12 files changed, 1070 insertions(+), 233 deletions(-) create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/README.md create mode 100755 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/test_validation.sh create mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/text.txt create mode 100755 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/validate_ocmf_signature.py diff --git a/config/bringup/config-bringup-CGEM580.yaml b/config/bringup/config-bringup-CGEM580.yaml index 51a47104a8..382e3a61c8 100644 --- a/config/bringup/config-bringup-CGEM580.yaml +++ b/config/bringup/config-bringup-CGEM580.yaml @@ -23,7 +23,8 @@ active_modules: cli: config_module: evse_id: "DE*ENBW*BER001*EVSE01" - tariff_text: "This is just a long string to test the tariff text functionality. The kWh price is 0.30 EUR/kWh - just joking - it is 2.30 EUR/kWh" + tariff_text: "This-is-just-a-long-string-to-test-the-tariff-text-functionality.No-spaces-are-allowed.The-kWh-price-is-0.30-EUR/kWh-just-joking-it-is-2.30-EUR/kWh" + identification_data: "A1z */-+.()[]{}$%^&*_+-=[];'," module: BUPowermeter standalone: true connections: diff --git a/modules/BringUp/BUPowermeter/BUPowermeter.cpp b/modules/BringUp/BUPowermeter/BUPowermeter.cpp index 068056ba60..729c6f0304 100644 --- a/modules/BringUp/BUPowermeter/BUPowermeter.cpp +++ b/modules/BringUp/BUPowermeter/BUPowermeter.cpp @@ -237,17 +237,17 @@ void BUPowermeter::ready() { optional_add(table_content, "Powermeter: imported energy in Wh (from grid): L3", powermeter.energy_Wh_import.L3); - optional_add(table_content, "Powermeter: user defined meter ID", powermeter.meter_id); - optional_add(table_content, "Powermeter: 3 phase rotation error (ccw)", powermeter.phase_seq_error); - optional_add(table_content, "Powermeter: exported energy in Wh (to grid), total", - std::to_string(powermeter.energy_Wh_export.value_or(ed).total)); + std::to_string(powermeter.energy_Wh_export.value_or(ed).total)); optional_add(table_content, "Powermeter: exported energy in Wh (to grid): L1", - powermeter.energy_Wh_export.value_or(ed).L1); + powermeter.energy_Wh_export.value_or(ed).L1); optional_add(table_content, "Powermeter: exported energy in Wh (to grid): L2", - powermeter.energy_Wh_export.value_or(ed).L2); + powermeter.energy_Wh_export.value_or(ed).L2); optional_add(table_content, "Powermeter: exported energy in Wh (to grid): L3", - powermeter.energy_Wh_export.value_or(ed).L3); + powermeter.energy_Wh_export.value_or(ed).L3); + + optional_add(table_content, "Powermeter: user defined meter ID", powermeter.meter_id); + optional_add(table_content, "Powermeter: 3 phase rotation error (ccw)", powermeter.phase_seq_error); optional_add(table_content, "Powermeter: voltage in V, DC", powermeter.voltage_V.value_or(vd).DC); optional_add(table_content, "Powermeter: voltage in V: L1", powermeter.voltage_V.value_or(vd).L1); diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp index 004367f83e..bbceeafc0e 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ #include #include +#include "everest/logging.hpp" #include "powermeterImpl.hpp" #include "utils.hpp" @@ -23,11 +25,14 @@ const int MODBUS_SIGNED_MAP_ADDRESS = 302049; const int MODBUS_SIGNED_MAP_SIGNATURE_ADDRESS = 302126; const int MODBUS_REAL_TIME_VALUES_ADDRESS = 300001; -const int MODBUS_REAL_TIME_VALUES_COUNT = 55; // Registers 300001-300055 +const int MODBUS_REAL_TIME_VALUES_COUNT = 80; // Registers 300001-300080 (0x50 = 80 words) +// This range includes: +// - 300001-300052: Real-time values (52 words) +// - 300053-300054: kWh (+) TOT (energy import) - INT32, 2 words, byte offset 104 (52*2) +// - Gap: 300055-300078 (ignored) +// - 300079-300080: kWh (-) TOT (energy export) - INT32, 2 words, byte offset 156 (78*2) -const int MODBUS_ENERGY_EXPORT_ADDRESS = 300053; // kWh (-) TOT -const int MODBUS_ENERGY_IMPORT_ADDRESS = 300079; // kWh (+) TOT -const int MODBUS_TEMPERATURE_ADDRESS = 300776; // Internal Temperature +const int MODBUS_TEMPERATURE_ADDRESS = 300776; // Internal Temperature const int MODBUS_FIRMWARE_MEASURE_MODULE_ADDRESS = 300771; // Measure module firmware version/revision const int MODBUS_FIRMWARE_COMMUNICATION_MODULE_ADDRESS = 300772; // Communication module firmware version/revision @@ -35,24 +40,25 @@ const int MODBUS_SERIAL_NUMBER_START_ADDRESS = 320481; // Serial numbe const int MODBUS_SERIAL_NUMBER_REGISTER_COUNT = 7; // 7 UINT16 registers = 14 bytes const int MODBUS_PRODUCTION_YEAR_ADDRESS = 320488; // Production year (1 UINT16 register) +// Device state register (Table 4.30, Section 4.3.6) +const int MODBUS_DEVICE_STATE_ADDRESS = 320499; // 5012h: Device state (UINT16 bitfield) + // Time synchronization registers const int MODBUS_UTC_TIMESTAMP_ADDRESS = 328723; // UTC Timestamp for synchronization (INT64, 4 words) const int MODBUS_TIMEZONE_OFFSET_ADDRESS = 328722; // Local time delta in minutes (INT16, 1 word) // OCMF Transaction registers (Table 4.34) -const int MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS = 328673; // 7000h: OCMF Identification Status (UINT16) -const int MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS = 328674; // 7001h: OCMF Identification Level (UINT16) -const int MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS = 328675; // 7002h: OCMF Identification Flags 1-4 (4 UINT16) +const int MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS = 328673; // 7000h: OCMF Ident. Status (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS = 328674; // 7001h: OCMF Ident. Level (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS = 328675; // 7002h: OCMF Ident. Flags 1-4 (4 UINT16) const int MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT = 4; // 4 flags -const int MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS = 328679; // 7006h: OCMF Identification Type (UINT16) -const int MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS = - 328680; // 7007h: OCMF Identification Data (CHAR[40] = 20 words) -const int MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT = 20; // 40 bytes = 20 words -const int MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS = 328700; // 701Bh: OCMF Charging point identifier type (UINT16) -const int MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS = - 328701; // 701Ch: OCMF Charging point identifier (CHAR[40] = 20 words) -const int MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT = 20; // 40 bytes = 20 words -const int MODBUS_OCMF_SESSION_MODALITY_ADDRESS = 328727; // 7036h: OCMF Session Modality (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS = 328679; // 7006h: OCMF Ident. Type (UINT16) +const int MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS = 328680; // 7007h: OCMF Ident. Data (CHAR[40] = 20 words) +const int MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT = 20; // 40 bytes = 20 words +const int MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS = 328700; // 701Bh: OCMF Charging point identifier type (UINT16) +const int MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS = 328701; // 701Ch: OCMF CPI (CHAR[40] = 20 words) +const int MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT = 20; // 40 bytes = 20 words +const int MODBUS_OCMF_SESSION_MODALITY_ADDRESS = 328727; // 7036h: OCMF Session Modality (UINT16) const uint16_t MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE = 0; // Charging vehicle const uint16_t MODBUS_OCMF_SESSION_MODALITY_VEHICLE_TO_GRID = 1; // Vehicle to grid const uint16_t MODBUS_OCMF_SESSION_MODALITY_BIDIRECTIONAL = 2; // Bidirectional @@ -84,8 +90,8 @@ const int MODBUS_OCMF_STATE_SIZE_ADDRESS = 328930; // 7101h: OCMF Size const int MODBUS_OCMF_STATE_FILE_ADDRESS = 328945; // 7110h: OCMF File (max theoretically 2031 words) const int MODBUS_OCMF_STATE_FILE_WORD_COUNT = 2031; // 2031 words = 4062 bytes const int MODBUS_OCMF_CHARGING_STATUS_ADDRESS = 328742; // 7045h: Charging status (UINT16) -const int MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS = 328723; // 7059h: Last transaction id (CHAR[]) -const int MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT = 20; // 40 bytes = 20 words +const int MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS = 328762; // 7059h: Last transaction id (CHAR[]) +const int MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT = 14; // 14 bytes = 7 words const int MODBUS_OCMF_TIME_SYNC_STATUS_ADDRESS = 328769; // 7060h: Time synchronization status (UINT16) // Byte offsets for Modbus register 300001-300055 (physical addresses 0000h-0036h) @@ -118,6 +124,10 @@ constexpr size_t PHASE_SEQUENCE = 100; // 300051 (0032h) // Frequency register (INT16, 2 bytes) constexpr size_t FREQUENCY = 102; // 300052 (0033h) + +// Energy registers (INT32, 4 bytes each) - within extended read range (300001-300080) +constexpr size_t ENERGY_IMPORT = 104; // 300053 (0034h) - kWh (+) TOT, byte offset 104 (52*2) +constexpr size_t ENERGY_EXPORT = 156; // 300079 (004Eh) - kWh (-) TOT, byte offset 156 (78*2) } // namespace Offsets // Scaling factors from Modbus document @@ -134,10 +144,30 @@ constexpr float TEMPERATURE = 0.1F; // Value weight: Temperature*10 namespace module::main { void powermeterImpl::init() { + // Set up error handler for CommunicationFault + transport::ErrorHandler error_handler = [this](const std::string& error_message) { + // Check if error is already active to avoid duplicate errors + if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault", "CommunicationError")) { + EVLOG_error << "Raising CommunicationFault: " << error_message; + auto error = this->error_factory->create_error("powermeter/CommunicationFault", "CommunicationError", + error_message, Everest::error::Severity::High); + raise_error(error); + } + }; + + // Set up clear error handler for CommunicationFault + transport::ClearErrorHandler clear_error_handler = [this]() { + // Clear CommunicationFault error if it's active + if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault", "CommunicationError")) { + EVLOG_info << "Clearing CommunicationFault: Communication restored"; + clear_error("powermeter/CommunicationFault", "CommunicationError"); + } + }; + p_modbus_transport = move(std::make_unique( *(mod->r_modbus.get()), config.powermeter_device_id, MODBUS_BASE_ADDRESS, config.initial_connection_retry_count, config.initial_connection_retry_delay_ms, config.communication_retry_count, - config.communication_retry_delay_ms)); + config.communication_retry_delay_ms, error_handler, clear_error_handler)); } void powermeterImpl::read_signature_config() { @@ -245,6 +275,7 @@ void powermeterImpl::read_serial_number() { } void powermeterImpl::configure_device() { + EVLOG_info << "Configure the device..."; read_firmware_versions(); read_serial_number(); read_signature_config(); @@ -261,20 +292,8 @@ void powermeterImpl::configure_device() { std::vector data = {0}; p_modbus_transport->write_multiple_registers(MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS, data); - // TODO(fmihut): check if a transaction is active and if so, abort it - std::vector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1); - uint16_t state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); - if (state == MODBUS_OCMF_STATE_RUNNING) { - EVLOG_info << "A transaction is active, if it is different from the current transaction, abort it"; - std::vector last_transaction_id_data = p_modbus_transport->fetch(MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS, MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT); - auto null_pos = std::find(last_transaction_id_data.begin(), last_transaction_id_data.end(), 0); - std::string last_transaction_id(last_transaction_id_data.begin(), null_pos); - if (last_transaction_id != m_transaction_id) { - EVLOG_info << "A transaction is active, if it is different from the current transaction, abort it"; - std::vector command_data = {MODBUS_OCMF_COMMAND_ABORT}; - p_modbus_transport->write_multiple_registers(MODBUS_OCMF_COMMAND_ADDRESS, command_data); - } - } + EVLOG_info << "Device configured"; + // TODO(fmihut): check how to recover from a power outage } void powermeterImpl::ready() { @@ -288,6 +307,7 @@ void powermeterImpl::ready() { device_not_configured = false; } read_powermeter_values(); + read_device_state(); } catch (const std::exception& e) { EVLOG_error << "Failed to communicate with the device, try again in 10 seconds: " << e.what(); device_not_configured = true; @@ -303,31 +323,30 @@ void powermeterImpl::ready() { time_sync_thread_obj.detach(); } -void powermeterImpl::write_transaction_registers(const types::powermeter::TransactionReq& transaction_req) { - // Helper function to convert string to Modbus CHAR array (null-terminated, padded with zeros) +std::vector powermeterImpl::string_to_modbus_char_array(const std::string& str, size_t word_count) { + // Convert string to Modbus CHAR array (null-terminated, padded with zeros) // Modbus stores strings as big-endian words: [MSB, LSB] per word - auto string_to_modbus_char_array = [](const std::string& str, size_t word_count) -> std::vector { - std::vector data(word_count, 0); - size_t byte_count = word_count * 2; - size_t str_len = std::min(str.length(), byte_count - 1); // Leave space for null terminator - - // Convert string bytes to Modbus words (big-endian: MSB, LSB) - for (size_t i = 0; i < str_len; ++i) { - size_t word_idx = i / 2; - if (i % 2 == 0) { - // MSB of word - data[word_idx] = static_cast(str[i]) << 8; - } else { - // LSB of word - data[word_idx] |= static_cast(str[i]); - } + std::vector data(word_count, 0); + size_t byte_count = word_count * 2; + size_t str_len = std::min(str.length(), byte_count - 1); // Leave space for null terminator + + // Convert string bytes to Modbus words (big-endian: MSB, LSB) + for (size_t i = 0; i < str_len; ++i) { + size_t word_idx = i / 2; + if (i % 2 == 0) { + // MSB of word + data[word_idx] = static_cast(str[i]) << 8; + } else { + // LSB of word + data[word_idx] |= static_cast(str[i]); } + } + // Null terminator is already handled by initialization (all zeros) + // The first byte after the string is 0, which identifies EOL + return data; +} - // Null terminator is already handled by initialization (all zeros) - // The first byte after the string is 0, which identifies EOL - - return data; - }; +void powermeterImpl::write_transaction_registers(const types::powermeter::TransactionReq& transaction_req) { // Helper function to convert OCMFIdentificationFlags enum to numeric value auto flag_to_value = [](types::powermeter::OCMFIdentificationFlags flag) -> uint16_t { @@ -489,6 +508,7 @@ void powermeterImpl::write_transaction_registers(const types::powermeter::Transa // Format: identification_data + ',' + transaction_id // Max length: 40 characters - the transaction_id is 36 characters max std::string client_id_str = transaction_req.identification_data.value_or(""); + client_id_str; std::vector id_data = string_to_modbus_char_array(client_id_str, MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT); p_modbus_transport->write_multiple_registers(MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS, id_data); @@ -500,44 +520,123 @@ void powermeterImpl::write_transaction_registers(const types::powermeter::Transa p_modbus_transport->write_multiple_registers(MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS, id_type_data); // 7. Write OCMF Charging point identifier (registers 328701-328720, 701Ch-702Fh) - CHAR[40] = 20 words (evse_id) + transaction_req.evse_id; std::vector evse_id_data = string_to_modbus_char_array(transaction_req.evse_id, MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT); p_modbus_transport->write_multiple_registers(MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS, evse_id_data); - - // 8. Write tariff text (register 326881, 6900h) - CHAR[252] = 126 words - // The meter expects a null-terminated string; the helper initializes all words to 0, - // so if the string is shorter than the maximum length, the first byte after the content is 0. - const std::string tariff_text = transaction_req.tariff_text.value_or("") + '|' + transaction_req.transaction_id; - std::vector tariff_text_data = - string_to_modbus_char_array(tariff_text, MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT); - p_modbus_transport->write_multiple_registers(MODBUS_OCMF_TARIFF_TEXT_ADDRESS, tariff_text_data); } types::powermeter::TransactionStartResponse -powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) { +powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& treq) { + // Helper function to validate strings for EM580 device + // According to EM580 Modbus document (line 2376-2377 and APPENDIX), only specific ASCII characters are allowed + // Exact allowed characters from APPENDIX table: + // 33 (!), 36 ($), 37 (%), 39 ('), 40-47 (()*+,-./), 48-57 (0-9), 58 (:), 60-63 (<=>?), 65-90 (A-Z), 97-122 (a-z), + // 128 (€), 156 (£), 157 (¥) + // NOT allowed: space (32), pipe (124), quote (34), hash (35), ampersand (38), semicolon (59), and other characters + // Returns true if valid, false if invalid characters are found + auto validate_string_for_em580 = [](const std::string& str) -> bool { + // Build set of allowed character codes (exact list from APPENDIX) + static const std::set allowed = []() { + std::set a; + a.insert(33); // ! + a.insert(36); // $ + a.insert(37); // % + for (uint8_t c = 39; c <= 58; ++c) + a.insert(c); // ()*+,-./0-9: + for (uint8_t c = 60; c <= 63; ++c) + a.insert(c); // <=>? + for (uint8_t c = 65; c <= 90; ++c) + a.insert(c); // A-Z + for (uint8_t c = 97; c <= 122; ++c) + a.insert(c); // a-z + a.insert(128); // € + a.insert(156); // £ + a.insert(157); // ¥ + return a; + }(); + + for (size_t i = 0; i < str.length(); ++i) { + uint8_t code = static_cast(str[i]); + if (allowed.find(code) == allowed.end()) { + return false; + } + } + + return true; + }; + try { - EVLOG_info << "Starting transaction with transaction id: " << value.transaction_id - << " and evse id: " << value.evse_id << " and identification status: " << value.identification_status + EVLOG_info << "Starting transaction with transaction id: " << treq.transaction_id + << " and evse id: " << treq.evse_id << " and identification status: " << treq.identification_status << " and identification type: " - << types::powermeter::ocmfidentification_type_to_string(value.identification_type) + << types::powermeter::ocmfidentification_type_to_string(treq.identification_type) << " and identification level: " << types::powermeter::ocmfidentification_level_to_string( - value.identification_level.value_or(types::powermeter::OCMFIdentificationLevel::NONE)) - << " and identification data: " << value.identification_data.value_or("") - << " and tariff text: " << value.tariff_text.value_or("none"); + treq.identification_level.value_or(types::powermeter::OCMFIdentificationLevel::NONE)) + << " and identification data: " << treq.identification_data.value_or("") + << " and tariff text: " << treq.tariff_text.value_or("none"); // Write transaction registers first - write_transaction_registers(value); + EVLOG_info << "Write transaction registers..."; + write_transaction_registers(treq); + + // 8. Write tariff text (register 326881, 6900h) - CHAR[252] = 126 words + // The meter expects a null-terminated string; the helper initializes all words to 0, + // + // IMPORTANT: EM580 only allows specific ASCII characters (see APPENDIX). Space and special characters are NOT + // allowed. + // + EVLOG_info << "Write tariff text..."; + std::string tariff_text = treq.tariff_text.value_or("") + "<=>" + treq.transaction_id; + if (!validate_string_for_em580(tariff_text)) { + EVLOG_error << "String contains invalid characters for EM580 device: '" << tariff_text << "'"; + return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "Invalid tariff text (device supports only an subset of ASCII characters)"}; + } else { + std::vector tariff_text_data = + string_to_modbus_char_array(tariff_text, MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT); + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_TARIFF_TEXT_ADDRESS, tariff_text_data); + } + EVLOG_info << "Write session modality ... to charging vehicle"; std::vector session_modality_data = {MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE}; p_modbus_transport->write_multiple_registers(MODBUS_OCMF_SESSION_MODALITY_ADDRESS, session_modality_data); + // Check OCMF state and ensure it's NOT_READY before starting a transaction + // According to the Modbus document, the OCMF state must be NOT_READY (0) to start a new transaction + transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1); + uint16_t ocmf_state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); + EVLOG_info << "OCMF state before starting transaction: " << ocmf_state; + + if (ocmf_state != MODBUS_OCMF_STATE_NOT_READY) { + if (ocmf_state == MODBUS_OCMF_STATE_READY) { + // If state is READY, we need to reset it to NOT_READY to allow a new transaction + // This confirms the reading of the previous OCMF file (if any) and allows a new session + EVLOG_info << "OCMF state is READY, resetting to NOT_READY to allow new transaction"; + std::vector reset_state_data = {MODBUS_OCMF_STATE_NOT_READY}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_STATE_ADDRESS, reset_state_data); + // Wait a bit for the state to update + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else if (ocmf_state == MODBUS_OCMF_STATE_RUNNING) { + // If a transaction is already running, we cannot start a new one + throw std::runtime_error( + "Cannot start transaction: OCMF state is RUNNING (transaction already active)"); + } else if (ocmf_state == MODBUS_OCMF_STATE_CORRUPTED) { + // If state is CORRUPTED, we should reset it + EVLOG_warning << "OCMF state is CORRUPTED, resetting to NOT_READY"; + std::vector reset_state_data = {MODBUS_OCMF_STATE_NOT_READY}; + p_modbus_transport->write_multiple_registers(MODBUS_OCMF_STATE_ADDRESS, reset_state_data); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + // Write 'B' command to start transaction (Table 4.35, register 328737) std::vector command_data1 = {MODBUS_OCMF_COMMAND_START}; p_modbus_transport->write_multiple_registers(MODBUS_OCMF_COMMAND_ADDRESS, command_data1); + EVLOG_info << "Transaction " << treq.transaction_id << " started"; // Track local state (only used internally, not in device dump) - m_transaction_active = true; - m_transaction_id = value.transaction_id; + m_transaction_active.store(true); + m_transaction_id = treq.transaction_id; return {types::powermeter::TransactionRequestStatus::OK}; } catch (const std::exception& e) { EVLOG_error << __PRETTY_FUNCTION__ << " Error: " << e.what() << std::endl; @@ -550,6 +649,8 @@ types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transacti // Write 'E' command to end transaction (Table 4.35, register 328737) std::vector command_data = {MODBUS_OCMF_COMMAND_END}; p_modbus_transport->write_multiple_registers(MODBUS_OCMF_COMMAND_ADDRESS, command_data); + EVLOG_info << "Transaction " << transaction_id << " stopped"; + m_transaction_active.store(false); // check if the OCMF state is ready (Table 4.36, register 328742) uint16_t state = MODBUS_OCMF_STATE_NOT_READY; @@ -604,142 +705,128 @@ types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transacti } void powermeterImpl::read_powermeter_values() { - try { - // Read registers 300001-300055 (physical addresses 0000h-0036h) - // Total: 55 words (0x37 = 55 decimal) - // According to Modbus document Table 4.1: - // - 300001-300011: Phase voltages (6 values, 12 words) - // - 300013-300017: Phase currents (3 values, 6 words) - // - 300019-300023: Phase powers (3 values, 6 words) - // - 300025-300029: Phase apparent powers (3 values, 6 words) - // - 300031-300035: Phase reactive powers (3 values, 6 words) - // - 300037-300045: System values (5 values, 10 words) - // - 300047-300050: Power factors (4 values, 4 words) - // - 300051: Phase sequence (1 word) - // - 300052-300055: Inductive/Capacitive load indicators (4 values, 4 words) - transport::DataVector data = - p_modbus_transport->fetch(MODBUS_REAL_TIME_VALUES_ADDRESS, MODBUS_REAL_TIME_VALUES_COUNT); - - types::powermeter::Powermeter powermeter{}; - powermeter.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); - powermeter.meter_id = std::move(std::string(this->mod->info.id)); - - // Voltage values (INT32, weight: Volt*10) - // 300001 (0000h): V L1-N - // 300003 (0002h): V L2-N - // 300005 (0004h): V L3-N - types::units::Voltage voltage_V; - voltage_V.L1 = Factors::VOLTAGE * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L1_N})); - voltage_V.L2 = Factors::VOLTAGE * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L2_N})); - voltage_V.L3 = Factors::VOLTAGE * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L3_N})); - powermeter.voltage_V = voltage_V; - - // Current values (INT32, weight: Ampere*1000) - // Values are already signed: positive = import, negative = export - // 300013 (000Ch): A L1 - // 300015 (000Eh): A L2 - // 300017 (0010h): A L3 - types::units::Current current_A; - current_A.L1 = Factors::CURRENT * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L1})); - current_A.L2 = Factors::CURRENT * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L2})); - current_A.L3 = Factors::CURRENT * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L3})); - powermeter.current_A = current_A; - - // Power values (INT32, weight: Watt*10) - // Values are already signed: positive = import, negative = export - // 300019 (0012h): W L1 - // 300021 (0014h): W L2 - // 300023 (0016h): W L3 - // 300041 (0028h): W sys - types::units::Power power_W; - power_W.L1 = - Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L1})); - power_W.L2 = - Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L2})); - power_W.L3 = - Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L3})); - power_W.total = - Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_SYS})); - powermeter.power_W = power_W; - - // Reactive power values (INT32, weight: var*10) - // Values are already signed: positive = import, negative = export - // 300031 (001Eh): var L1 - // 300033 (0020h): var L2 - // 300035 (0022h): var L3 - // 300045 (002Ch): var sys - types::units::ReactivePower VAR; - VAR.L1 = Factors::REACTIVE_POWER * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L1})); - VAR.L2 = Factors::REACTIVE_POWER * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L2})); - VAR.L3 = Factors::REACTIVE_POWER * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L3})); - VAR.total = Factors::REACTIVE_POWER * - static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_SYS})); - powermeter.VAR = VAR; - - // Frequency (INT16, weight: Hz*10) - register 300052 (0033h) - // Note: Frequency is also available at 300273 and 301341 as INT32 with different factors, - // but we use 300052 (INT16) to keep the bulk read compact (300001-300055) - types::units::Frequency frequency_Hz; - frequency_Hz.L1 = - Factors::FREQUENCY * - static_cast(modbus_utils::to_int16(data, modbus_utils::ByteOffset{Offsets::FREQUENCY})); - powermeter.frequency_Hz = frequency_Hz; - - // Phase sequence (INT16) - register 300051 (0032h) - // Value -1 = L1-L3-L2 sequence, value 1 = L1-L2-L3 sequence - int16_t phase_sequence = modbus_utils::to_int16(data, modbus_utils::ByteOffset{Offsets::PHASE_SEQUENCE}); - if (phase_sequence == -1) { - powermeter.phase_seq_error = true; // L1-L3-L2 is considered an error (counter-clockwise) - } else if (phase_sequence == 1) { - powermeter.phase_seq_error = false; // L1-L2-L3 is correct (clockwise) - } - // If phase_sequence is neither -1 nor 1, leave phase_seq_error unset - - // Read energy values (INT32, weight: kWh*10) - need separate reads as they're outside 300001-300055 range - // Energy export: register 300053 (kWh (-) TOT) - 2 words - transport::DataVector energy_export_data = p_modbus_transport->fetch(MODBUS_ENERGY_EXPORT_ADDRESS, 2); - types::units::Energy energy_Wh_export; - energy_Wh_export.total = - Factors::ENERGY_KWH_TO_WH * - static_cast(modbus_utils::to_int32(energy_export_data, modbus_utils::ByteOffset{0})); - powermeter.energy_Wh_export = energy_Wh_export; - - // Energy import: register 300079 (kWh (+) TOT) - 2 words - // Note: energy_Wh_import is a required field, not optional - transport::DataVector energy_import_data = p_modbus_transport->fetch(MODBUS_ENERGY_IMPORT_ADDRESS, 2); - powermeter.energy_Wh_import.total = - Factors::ENERGY_KWH_TO_WH * - static_cast(modbus_utils::to_int32(energy_import_data, modbus_utils::ByteOffset{0})); - - // Read internal temperature (INT16, weight: Temperature*10) - register 300776 (0307h) - 1 word - transport::DataVector temperature_data = p_modbus_transport->fetch(MODBUS_TEMPERATURE_ADDRESS, 1); - types::temperature::Temperature temperature; - temperature.temperature = - Factors::TEMPERATURE * - static_cast(modbus_utils::to_int16(temperature_data, modbus_utils::ByteOffset{0})); - temperature.location = "Internal"; - std::vector temperatures; - temperatures.push_back(temperature); - powermeter.temperatures = temperatures; - - this->publish_powermeter(powermeter); - } catch (const std::exception& e) { - // we catch std::exception& here since there may be other exceptions than std::runtime_error - EVLOG_warning << __PRETTY_FUNCTION__ << " Exception caught: " << e.what() << "\n"; - // Don't exit - continue trying in the next iteration + // Read registers 300001-300082 (82 words = 0x50+2) + // This single read includes: + // - 300001-300052: Real-time values (52 words) + // - 300053-300054: kWh (+) TOT (energy import) - INT32, 2 words + // - Gap: 300055-300078 (Modbus will read but we ignore these bytes) + // - 300079-300080: kWh (-) TOT (energy export) - INT32, 2 words + // This optimization reduces Modbus requests from 3 to 2 (removing the separate totals read) + transport::DataVector data = + p_modbus_transport->fetch(MODBUS_REAL_TIME_VALUES_ADDRESS, MODBUS_REAL_TIME_VALUES_COUNT); + + types::powermeter::Powermeter powermeter{}; + powermeter.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); + powermeter.meter_id = std::move(std::string(this->mod->info.id)); + + // Voltage values (INT32, weight: Volt*10) + // 300001 (0000h): V L1-N + // 300003 (0002h): V L2-N + // 300005 (0004h): V L3-N + types::units::Voltage voltage_V; + voltage_V.L1 = + Factors::VOLTAGE * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L1_N})); + voltage_V.L2 = + Factors::VOLTAGE * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L2_N})); + voltage_V.L3 = + Factors::VOLTAGE * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L3_N})); + powermeter.voltage_V = voltage_V; + + // Current values (INT32, weight: Ampere*1000) + // Values are already signed: positive = import, negative = export + // 300013 (000Ch): A L1 + // 300015 (000Eh): A L2 + // 300017 (0010h): A L3 + types::units::Current current_A; + current_A.L1 = + Factors::CURRENT * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L1})); + current_A.L2 = + Factors::CURRENT * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L2})); + current_A.L3 = + Factors::CURRENT * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L3})); + powermeter.current_A = current_A; + + // Power values (INT32, weight: Watt*10) + // Values are already signed: positive = import, negative = export + // 300019 (0012h): W L1 + // 300021 (0014h): W L2 + // 300023 (0016h): W L3 + // 300041 (0028h): W sys + types::units::Power power_W; + power_W.L1 = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L1})); + power_W.L2 = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L2})); + power_W.L3 = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L3})); + power_W.total = + Factors::POWER * static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_SYS})); + powermeter.power_W = power_W; + + // Reactive power values (INT32, weight: var*10) + // Values are already signed: positive = import, negative = export + // 300031 (001Eh): var L1 + // 300033 (0020h): var L2 + // 300035 (0022h): var L3 + // 300045 (002Ch): var sys + types::units::ReactivePower VAR; + VAR.L1 = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L1})); + VAR.L2 = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L2})); + VAR.L3 = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L3})); + VAR.total = Factors::REACTIVE_POWER * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_SYS})); + powermeter.VAR = VAR; + + // Frequency (INT16, weight: Hz*10) - register 300052 (0033h) + // Note: Frequency is also available at 300273 and 301341 as INT32 with different factors, + // but we use 300052 (INT16) to keep the bulk read compact (300001-300055) + types::units::Frequency frequency_Hz; + frequency_Hz.L1 = Factors::FREQUENCY * + static_cast(modbus_utils::to_int16(data, modbus_utils::ByteOffset{Offsets::FREQUENCY})); + powermeter.frequency_Hz = frequency_Hz; + + // Phase sequence (INT16) - register 300051 (0032h) + // Value -1 = L1-L3-L2 sequence, value 1 = L1-L2-L3 sequence + int16_t phase_sequence = modbus_utils::to_int16(data, modbus_utils::ByteOffset{Offsets::PHASE_SEQUENCE}); + if (phase_sequence == -1) { + powermeter.phase_seq_error = true; // L1-L3-L2 is considered an error (counter-clockwise) + } else if (phase_sequence == 1) { + powermeter.phase_seq_error = false; // L1-L2-L3 is correct (clockwise) } + + // Energy import: register 300053 (kWh (+) TOT) - INT32, 2 words + // Byte offset in data: 104 (52*2, since 300053 is at offset 52 from 300001) + // Note: energy_Wh_import is a required field, not optional + powermeter.energy_Wh_import.total = + Factors::ENERGY_KWH_TO_WH * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::ENERGY_IMPORT})); + + // Energy export: register 300079 (kWh (-) TOT) - INT32, 2 words + // Byte offset in data: 156 (78*2, since 300079 is at offset 78 from 300001) + types::units::Energy energy_Wh_export; + energy_Wh_export.total = + Factors::ENERGY_KWH_TO_WH * + static_cast(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::ENERGY_EXPORT})); + powermeter.energy_Wh_export = energy_Wh_export; + + // Read internal temperature (INT16, weight: Temperature*10) - register 300776 (0307h) - 1 word + // transport::DataVector temperature_data = p_modbus_transport->fetch(MODBUS_TEMPERATURE_ADDRESS, 1); + // types::temperature::Temperature temperature; + // temperature.temperature = Factors::TEMPERATURE * + // static_cast(modbus_utils::to_int16(temperature_data, + // modbus_utils::ByteOffset{0})); + // temperature.location = "Internal"; + // std::vector temperatures; + // temperatures.push_back(temperature); + // powermeter.temperatures = temperatures; + + this->publish_powermeter(powermeter); } -void powermeterImpl::dump_device_state(void) { +void powermeterImpl::dump_device_state() { try { // 1. OCMF state transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1); @@ -750,10 +837,10 @@ void powermeterImpl::dump_device_state(void) { uint16_t charging_status = modbus_utils::to_uint16(charging_status_data, modbus_utils::ByteOffset{0}); // 3. Last transaction id (register 328723 / 7059h, CHAR[]) - transport::DataVector last_tx_data = p_modbus_transport->fetch(MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS, - MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT); - auto null_pos = std::find(last_tx_data.begin(), last_tx_data.end(), 0); - std::string last_tx_id(last_tx_data.begin(), null_pos); + // transport::DataVector last_tx_data = p_modbus_transport->fetch(MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS, + // MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT); + // auto null_pos = std::find(last_tx_data.begin(), last_tx_data.end(), 0); + // std::string last_tx_id(last_tx_data.begin(), null_pos); // 4. Time synchronization status (register 328769 / 7060h) transport::DataVector time_sync_status_data = @@ -772,7 +859,7 @@ void powermeterImpl::dump_device_state(void) { EVLOG_info << "EM580 device state dump:"; EVLOG_info << " OCMF state: " << state; EVLOG_info << " Charging status (device, raw): " << charging_status; - EVLOG_info << " Last transaction id (device): " << last_tx_id; + // EVLOG_info << " Last transaction id (device): " << last_tx_id; EVLOG_info << " Time synchronization status (device, raw): " << time_sync_status; EVLOG_info << " Last OCMF command (raw): 0x" << std::hex << raw_cmd << " ('" << cmd_char << "')"; EVLOG_info << " Transaction ID definition (OCMF): 0x" << std::hex << tx_def; @@ -793,13 +880,15 @@ void powermeterImpl::synchronize_time() { auto time_since_epoch = sys_now.time_since_epoch(); int64_t seconds_since_epoch = std::chrono::duration_cast(time_since_epoch).count(); - // Convert to UINT64 and split into 4 words (big-endian) + // Convert to UINT64 and split into 4 words + // According to EM580 Modbus spec: for INT64, word order is LSW->MSW (little-endian word order) + // So we write: [LSW, LSW+1, MSW-1, MSW] = [bits 0-15, bits 16-31, bits 32-47, bits 48-63] uint64_t timestamp = static_cast(seconds_since_epoch); std::vector data; - data.push_back(static_cast((timestamp >> 48) & 0xFFFF)); - data.push_back(static_cast((timestamp >> 32) & 0xFFFF)); - data.push_back(static_cast((timestamp >> 16) & 0xFFFF)); - data.push_back(static_cast(timestamp & 0xFFFF)); + data.push_back(static_cast(timestamp & 0xFFFF)); // LSW: bits 0-15 + data.push_back(static_cast((timestamp >> 16) & 0xFFFF)); // bits 16-31 + data.push_back(static_cast((timestamp >> 32) & 0xFFFF)); // bits 32-47 + data.push_back(static_cast((timestamp >> 48) & 0xFFFF)); // MSW: bits 48-63 // Write UTC timestamp to register 328723 (4 words for INT64) p_modbus_transport->write_multiple_registers(MODBUS_UTC_TIMESTAMP_ADDRESS, data); @@ -849,4 +938,76 @@ void powermeterImpl::time_sync_thread() { } } +void powermeterImpl::read_device_state() { + // Read device state register (Table 4.30, Section 4.3.6) + // Register 320499 (5012h): Device state (UINT16 bitfield) + transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_DEVICE_STATE_ADDRESS, 1); + uint16_t device_state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); + + // Check for error bits and raise VendorError if any are set + std::vector error_messages; + + // Voltage over maximum range errors + if ((device_state & (1U << 0U)) != 0U) { + error_messages.emplace_back("V1N over maximum range"); + } + if ((device_state & (1U << 1U)) != 0U) { + error_messages.emplace_back("V2N over maximum range"); + } + if ((device_state & (1U << 2U)) != 0U) { + error_messages.emplace_back("V3N over maximum range"); + } + if ((device_state & (1U << 3U)) != 0U) { + error_messages.emplace_back("V12 over maximum range"); + } + if ((device_state & (1U << 4U)) != 0U) { + error_messages.emplace_back("V23 over maximum range"); + } + if ((device_state & (1U << 5U)) != 0U) { + error_messages.emplace_back("V31 over maximum range"); + } + + // Current over maximum range errors + if ((device_state & (1U << 6U)) != 0U) { + error_messages.emplace_back("I1 over maximum range"); + } + if ((device_state & (1U << 7U)) != 0U) { + error_messages.emplace_back("I2 over maximum range"); + } + if ((device_state & (1U << 8U)) != 0U) { + error_messages.emplace_back("I3 over maximum range"); + } + + // Frequency outside validity range + if ((device_state & (1U << 9U)) != 0U) { + error_messages.emplace_back("Frequency outside validity range"); + } + + // Module internal fault errors + if ((device_state & (1U << 12U)) != 0U) { + error_messages.emplace_back("EVCS module internal fault"); + } + if ((device_state & (1U << 13U)) != 0U) { + error_messages.emplace_back("Measure module internal fault"); + } + + // If any error bits are set, raise VendorError + if (!error_messages.empty()) { + std::string error_description = "Device state errors detected: "; + for (size_t i = 0; i < error_messages.size(); ++i) { + if (i > 0) { + error_description += ", "; + } + error_description += error_messages[i]; + } + error_description += " (device state: 0x" + fmt::format("{:04X}", device_state) + ")"; + + EVLOG_error << "Device state error: " << error_description; + auto error = this->error_factory->create_error("powermeter/VendorError", "DeviceStateError", error_description); + raise_error(error); + } else { + EVLOG_debug << "Device state OK (0x" << fmt::format("{:04X}", device_state) << ")"; + } +} + } // namespace module::main diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp index 312b117ba4..33983f27ae 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp @@ -82,6 +82,8 @@ class powermeterImpl : public powermeterImplBase { void time_sync_thread(); [[nodiscard]] bool is_transaction_active() const; void write_transaction_registers(const types::powermeter::TransactionReq& transaction_req); + void read_device_state(); + std::vector string_to_modbus_char_array(const std::string& str, size_t word_count); // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp index d88d8d1146..48efb88e7f 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp @@ -2,6 +2,7 @@ // Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest #include "transport.hpp" +#include const int MAX_REGISTER_PER_MESSAGE = 125; @@ -23,6 +24,16 @@ transport::DataVector SerialCommHubTransport::fetch(int address, int register_co types::serial_comm_hub_requests::Result serial_com_hub_result = m_serial_hub.call_modbus_read_input_registers(m_device_id, read_address, register_to_read); + // Check for communication errors + if (serial_com_hub_result.status_code == types::serial_comm_hub_requests::StatusCodeEnum::Timeout) { + throw transport::ModbusTimeoutException("Modbus read timeout: Packet receive timeout"); + } else if (serial_com_hub_result.status_code != types::serial_comm_hub_requests::StatusCodeEnum::Success) { + std::string error_msg = "Modbus read failed with status: " + + types::serial_comm_hub_requests::status_code_enum_to_string( + serial_com_hub_result.status_code); + throw std::runtime_error(error_msg); + } + if (not serial_com_hub_result.value.has_value()) throw std::runtime_error("no result from serial com hub!"); @@ -71,8 +82,12 @@ void SerialCommHubTransport::write_multiple_registers(int address, const std::ve types::serial_comm_hub_requests::StatusCodeEnum status = m_serial_hub.call_modbus_write_multiple_registers(m_device_id, write_address, data_raw); - if (status != types::serial_comm_hub_requests::StatusCodeEnum::Success) { - throw std::runtime_error("Failed to write Modbus registers: " + std::to_string(static_cast(status))); + if (status == types::serial_comm_hub_requests::StatusCodeEnum::Timeout) { + throw transport::ModbusTimeoutException("Modbus write timeout: Packet receive timeout"); + } else if (status != types::serial_comm_hub_requests::StatusCodeEnum::Success) { + std::string error_msg = "Failed to write Modbus registers: " + + types::serial_comm_hub_requests::status_code_enum_to_string(status); + throw std::runtime_error(error_msg); } }); } diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp index 314671429e..e1b4811d8d 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp @@ -15,13 +15,27 @@ #include #include #include +#include #include +#include +#include #include namespace transport { using DataVector = std::vector; +// Custom exception to distinguish timeout errors from other Modbus errors +class ModbusTimeoutException : public std::runtime_error { +public: + explicit ModbusTimeoutException(const std::string& message) : std::runtime_error(message) {} +}; + +// Error handler callback type: void(error_message) +using ErrorHandler = std::function; +// Clear error callback type: void() +using ClearErrorHandler = std::function; + class AbstractModbusTransport { public: @@ -49,6 +63,10 @@ class SerialCommHubTransport : public AbstractModbusTransport { // State tracking std::atomic_bool m_initial_connection_mode{true}; + // Error handling callbacks (optional) + ErrorHandler m_error_handler; + ClearErrorHandler m_clear_error_handler; + // Internal retry helper for functions that return a value template auto retry_with_config(Func&& func) -> decltype(std::forward(func)()) { bool is_initial = m_initial_connection_mode.load(); @@ -61,11 +79,31 @@ class SerialCommHubTransport : public AbstractModbusTransport { try { auto result = std::forward(func)(); // First successful call - switch to normal mode - if (m_initial_connection_mode.exchange(false)) { - // Switched from initial to normal mode + bool was_initial = m_initial_connection_mode.exchange(false); + // Clear CommunicationFault error if communication is restored + // Only clear if we're not in initial connection mode (i.e., we've had at least one successful operation) + if (m_clear_error_handler && !was_initial) { + m_clear_error_handler(); } return result; + } catch (const ModbusTimeoutException& e) { + // Timeout errors should raise CommunicationFault + bool should_retry = is_initial && m_initial_retry_count == 0 ? true : attempt < max_retries; + if (should_retry) { + EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries + << "): " << e.what() << ". Retrying in " << delay_ms << "ms..."; + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + } else { + EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what(); + // Raise CommunicationFault error for timeout errors + if (m_error_handler) { + m_error_handler("Modbus communication error: " + std::string(e.what())); + } + rethrow_exception(std::current_exception()); + } + attempt++; } catch (const std::exception& e) { + // Other errors (non-timeout) should not raise CommunicationFault bool should_retry = is_initial && m_initial_retry_count == 0 ? true : attempt < max_retries; if (should_retry) { EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries @@ -73,11 +111,14 @@ class SerialCommHubTransport : public AbstractModbusTransport { std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); } else { EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what(); + // Don't raise CommunicationFault for non-timeout errors rethrow_exception(std::current_exception()); } attempt++; } } + // This should never be reached, but needed to satisfy compiler + throw std::runtime_error("Retry loop exited unexpectedly"); } // Internal retry helper for void functions @@ -92,11 +133,31 @@ class SerialCommHubTransport : public AbstractModbusTransport { try { std::forward(func)(); // First successful call - switch to normal mode - if (m_initial_connection_mode.exchange(false)) { - // Switched from initial to normal mode + bool was_initial = m_initial_connection_mode.exchange(false); + // Clear CommunicationFault error if communication is restored + // Only clear if we're not in initial connection mode (i.e., we've had at least one successful operation) + if (m_clear_error_handler && !was_initial) { + m_clear_error_handler(); } return; + } catch (const ModbusTimeoutException& e) { + // Timeout errors should raise CommunicationFault + bool should_retry = is_initial && m_initial_retry_count == 0 ? true : attempt < max_retries; + if (should_retry) { + EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries + << "): " << e.what() << ". Retrying in " << delay_ms << "ms..."; + std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); + } else { + EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what(); + // Raise CommunicationFault error for timeout errors + if (m_error_handler) { + m_error_handler("Modbus communication error: " + std::string(e.what())); + } + rethrow_exception(std::current_exception()); + } + attempt++; } catch (const std::exception& e) { + // Other errors (non-timeout) should not raise CommunicationFault bool should_retry = is_initial && m_initial_retry_count == 0 ? true : attempt < max_retries; if (should_retry) { EVLOG_warning << "Modbus operation failed (attempt " << attempt << "/" << max_retries @@ -104,6 +165,7 @@ class SerialCommHubTransport : public AbstractModbusTransport { std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms)); } else { EVLOG_error << "Modbus operation failed after " << attempt << " attempts: " << e.what(); + // Don't raise CommunicationFault for non-timeout errors rethrow_exception(std::current_exception()); } attempt++; @@ -114,14 +176,17 @@ class SerialCommHubTransport : public AbstractModbusTransport { public: SerialCommHubTransport(serial_communication_hubIntf& serial_hub, int device_id, int base_address, int initial_retry_count, int initial_retry_delay_ms, int normal_retry_count, - int normal_retry_delay_ms) : + int normal_retry_delay_ms, ErrorHandler error_handler = nullptr, + ClearErrorHandler clear_error_handler = nullptr) : m_serial_hub(serial_hub), m_device_id(device_id), m_base_address(base_address), m_initial_retry_count(initial_retry_count), m_initial_retry_delay_ms(initial_retry_delay_ms), m_normal_retry_count(normal_retry_count), - m_normal_retry_delay_ms(normal_retry_delay_ms) { + m_normal_retry_delay_ms(normal_retry_delay_ms), + m_error_handler(error_handler), + m_clear_error_handler(clear_error_handler) { } virtual transport::DataVector fetch(int address, int register_count) override; diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/README.md b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/README.md new file mode 100644 index 0000000000..2bbfba2928 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/README.md @@ -0,0 +1,179 @@ +# OCMF Signature Validation + +This directory contains tools for validating OCMF (Open Charge Metering Format) signatures from the Carlo Gavazzi EM580 powermeter. + +## Overview + +The EM580 device signs OCMF transaction data using **ECDSA-brainpoolP384r1-SHA256**. This validation tool verifies the authenticity of OCMF data by checking the digital signature against the device's public key. + +## Prerequisites + +### Python Dependencies + +Install the required Python library: + +```bash +pip install cryptography +``` + +Or if using Nix: + +```bash +nix-shell -p "python3.withPackages (ps: with ps; [ cryptography ])" +``` + +## Files + +- **`validate_ocmf_signature.py`** - Main validation script +- **`test_validation.sh`** - Convenience script for quick testing +- **`README.md`** - This file + +## Usage + +### Method 1: Using the convenience script + +Edit `test_validation.sh` to set your public key and OCMF data, then run: + +```bash +./test_validation.sh +``` + +### Method 2: Using the validation script directly + +#### Validate OCMF pipe-separated string format + +The EM580 device outputs OCMF data in the format: `OCMF||` + +```bash +python3 validate_ocmf_signature.py \ + --public-key "04521C09090AB6A2826A613D36483A71F789F6C0D900F9A9106415EA8BE3F6AFEB5926B39E264CB3727647DA49B153370221F18048B343AC0318203F7043F840CD8BB5C9C6734C0DB46B19711AD94A0DB8F1FA854E2D60D25B33D7DDE145F61E6C" \ + --ocmf-string 'OCMF|{"FV":"1.2",...}|{"SD":"signature_hex","SA":"ECDSA-brainpoolP384r1-SHA256"}' +``` + +Or read from a file: + +```bash +python3 validate_ocmf_signature.py \ + --public-key "04<194_hex_chars>" \ + --ocmf-string "$(cat ocmf_data.txt)" +``` + +#### Validate with separate components + +```bash +python3 validate_ocmf_signature.py \ + --public-key "04<194_hex_chars>" \ + --text "data-to-be-signed" \ + --signature "" +``` + +#### Validate from file + +```bash +python3 validate_ocmf_signature.py \ + --public-key "04<194_hex_chars>" \ + --file data.json \ + --signature "" +``` + +## Public Key Format + +The public key must be in **uncompressed format**: +- Starts with `0x04` +- Followed by X coordinate (48 bytes = 96 hex chars) +- Followed by Y coordinate (48 bytes = 96 hex chars) +- **Total: 97 bytes = 194 hex characters** for P384 + +The public key can be read from the EM580 device at Modbus register **309473** (address 2500h). For a 384-bit key, read 49 words (98 bytes), but the last byte is unused, so use only the first 97 bytes. + +## Signature Format + +The signature can be in two formats: +1. **DER format** (ASN.1 encoded) - most common, typically 102-110 bytes +2. **Raw format**: r || s (each 48 bytes for P384, total 96 bytes = 192 hex chars) + +The script automatically detects the format. + +## OCMF Data Format + +The EM580 device outputs OCMF data in a pipe-separated format: + +``` +OCMF|| +``` + +Where: +- `` - JSON object containing all meter data (FV, GI, GS, RD, etc.) +- `` - JSON object with: + - `SD`: The signature in hex format + - `SA`: The signature algorithm (e.g., "ECDSA-brainpoolP384r1-SHA256") + +## JSON Normalization + +**Important**: OCMF requires signatures to be computed over **compact JSON** (no spaces). The validation script automatically normalizes JSON to compact format before verification. + +Example: +- Original: `{"LI": 99,"LR": 0}` +- Compact: `{"LI":99,"LR":0}` + +The script handles this normalization automatically. + +## Example Output + +``` +Loading public key... +✓ Public key loaded (brainpoolP384r1) + +✓ Parsed OCMF string format + Data length: 828 characters + Signature length: 204 hex characters + +⚠ JSON normalization: Original had 828 chars, compact has 825 chars + Using compact JSON format for signature verification (OCMF requirement) + Original hash: acafca116bd433ed0a8ad1200de600adf977d9bdef966bdecb3ec1c3cda2fdcc + Compact hash: fa3020425aaf1d03f8e2bce13f76e60cb098b3bff1664d1d45503b0d9c6b351b + +Verifying signature... + Algorithm: ECDSA-brainpoolP384r1-SHA256 + Message length: 825 characters (825 bytes) + Message hash (SHA256): fa3020425aaf1d03f8e2bce13f76e60cb098b3bff1664d1d45503b0d9c6b351b + Message preview (first 100 chars): {"FV":"1.2","GI":"Carlo Gavazzi Controls-EM580DINAV23XS3DET","GS":"KZ1660104001D"... + +✓ SIGNATURE VALID - The message is authentic! +``` + +## Troubleshooting + +### Signature verification fails + +If signature verification fails, check: + +1. **Public key**: Ensure it matches the device's current public key (read from register 309473) +2. **Signature**: Ensure it's from the same transaction as the data +3. **Data format**: The script automatically normalizes JSON, but verify the data hasn't been modified +4. **Key/Signature pair**: The public key and signature must be from the same device and transaction + +### Common errors + +- **"Expected 97 bytes for uncompressed P384 public key"**: The public key format is incorrect. Ensure it's 194 hex characters (97 bytes) starting with `04`. +- **"Invalid hex string"**: Check that the public key and signature contain only valid hexadecimal characters (0-9, A-F). +- **"Signature format not recognized"**: The signature should be either DER format (starts with 0x30) or raw format (96 bytes). + +## Technical Details + +### Algorithm +- **Curve**: brainpoolP384r1 (Brainpool P-384) +- **Hash**: SHA-256 +- **Signature**: ECDSA + +### Data-to-be-signed +The device signs the **compact JSON representation** of the OCMF data (the `` part, without the "OCMF|" prefix or signature JSON). + +### Byte Order +- Public key: Uncompressed format (0x04 || X || Y), big-endian +- Signature: DER format (ASN.1) or raw (r || s), big-endian + +## References + +- [OCMF Specification](https://github.com/SAFE-eV/OCMF-Open-Charge-Metering-Format) +- EM580 Modbus Communication Protocol document (Table 4.19, 4.21) diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/test_validation.sh b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/test_validation.sh new file mode 100755 index 0000000000..dd8aacfdae --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/test_validation.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Quick test script for OCMF validation +# +# Usage: +# 1. Edit this script to set your PUBLIC_KEY and OCMF_DATA_FILE +# 2. Run: ./test_validation.sh +# +# Or set environment variables: +# PUBLIC_KEY="04..." OCMF_DATA_FILE="path/to/ocmf.txt" ./test_validation.sh + +# Default values - edit these or set as environment variables +PUBLIC_KEY="${PUBLIC_KEY:-04521C09090AB6A2826A613D36483A71F789F6C0D900F9A9106415EA8BE3F6AFEB5926B39E264CB3727647DA49B153370221F18048B343AC0318203F7043F840CD8BB5C9C6734C0DB46B19711AD94A0DB8F1FA854E2D60D25B33D7DDE145F61E6C}" +OCMF_DATA_FILE="${OCMF_DATA_FILE:-./text.txt}" + +# Check if OCMF data file exists +if [ ! -f "$OCMF_DATA_FILE" ]; then + echo "Error: OCMF data file not found: $OCMF_DATA_FILE" + echo "Please set OCMF_DATA_FILE environment variable or edit this script." + exit 1 +fi + +OCMF_DATA=$(cat "$OCMF_DATA_FILE") + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +python3 "$SCRIPT_DIR/validate_ocmf_signature.py" \ + --public-key "$PUBLIC_KEY" \ + --ocmf-string "$OCMF_DATA" diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/text.txt b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/text.txt new file mode 100644 index 0000000000..f6b3deab7e --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/text.txt @@ -0,0 +1 @@ +OCMF|{"FV":"1.2","GI":"Carlo Gavazzi Controls-EM580DINAV23XS3DET","GS":"KZ1660104001D","GV":"M_1.6.3-C_1.6.3","PG":"T23","MV":"Carlo Gavazzi Controls","MM":"EM580DINAV23XS3DET","MS":"KZ1660104001D","MF":"M_1.6.3-C_1.6.3","IS":true,"IL":"NONE","IF":[],"IT":"ISO14443","ID":"A1z */-+.()[]{}$%^&*_+-=[];',","TT":"This-is-just-a-long-string-to-test-the-tariff-text-functionality.No-spaces-are-allowed.The-kWh-price-is-0.30-EUR/kWh-just-joking-it-is-2.30-EUR/kWh<=>12345678-1234-5678-1234-567812345678","CT":"EVSEID","CI":"DE*ENBW*BER001*EVSE01","LC":{"LN":"CABLE_LOSS","LI": 99,"LR": 0,"LU": "mOhm"},"RD":[{"TM":"2025-12-17T12:09:16,000+0100 S","TX":"B","RV":1.637,"RI":"1-b:1.8.0","RU":"kWh","RT":"AC","RM":"","ST":"G"},{"TM":"2025-12-17T12:30:30,000+0100 S","TX":"E","RV":1.643,"RI":"1-b:1.8.0","RU":"kWh","RT":"AC","RM":"","ST":"G"}]}|{"SD":"306402306ECEF6E68BF22926278DF470DEA50E12DACA2DCBC54F6EED7B73276EC22795F9D48795608D03EE4639EE11EC7013BC980230633380379E601677F1C1DC0958FE421722ABA8361E30019B34463B9A038229E5063EB54DBDBC9EA63E3F069384FDB72C","SA":"ECDSA-brainpoolP384r1-SHA256"} \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/validate_ocmf_signature.py b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/validate_ocmf_signature.py new file mode 100755 index 0000000000..6e97260559 --- /dev/null +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/ocmf_validation/validate_ocmf_signature.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +""" +OCMF Signature Validation Script + +Validates ECDSA signatures using brainpoolP384r1 curve and SHA256 hash algorithm. +This script can be used to verify the authenticity of OCMF (Open Charge Metering Format) data. + +Usage: + python3 validate_ocmf_signature.py --public-key --text --signature + python3 validate_ocmf_signature.py --public-key --file --signature + python3 validate_ocmf_signature.py --public-key --ocmf-json + +The signature should be in DER format (hex encoded). +The public key should be in uncompressed format (hex encoded, 97 bytes = 194 hex chars for P384). +""" + +import argparse +import json +import sys +from hashlib import sha256 + +try: + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature, decode_dss_signature +except ImportError: + print("Error: cryptography library is required. Install it with: pip install cryptography") + sys.exit(1) + + +def parse_ocmf_json(ocmf_json_str): + """ + Parse OCMF JSON string and extract the data-to-be-signed and signature. + + OCMF format structure: + { + "SD": "data-to-be-signed", + "SA": "signature-algorithm", + "SI": "signature" + } + """ + try: + ocmf_data = json.loads(ocmf_json_str) + if "SD" not in ocmf_data or "SI" not in ocmf_data: + raise ValueError("OCMF JSON must contain 'SD' (data) and 'SI' (signature) fields") + return ocmf_data["SD"], ocmf_data["SI"] + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON format: {e}") + + +def parse_ocmf_string(ocmf_str): + """ + Parse OCMF pipe-separated string format: OCMF|| + + The signature_json should contain "SD" field with the signature hex. + """ + parts = ocmf_str.split("|", 2) + if len(parts) != 3 or parts[0] != "OCMF": + raise ValueError("Invalid OCMF string format. Expected: OCMF||") + + data_json = parts[1] + signature_json_str = parts[2] + + # Parse signature JSON to get SD field + try: + signature_json = json.loads(signature_json_str) + signature_hex = signature_json.get("SD", "") + if not signature_hex: + raise ValueError("Signature JSON must contain 'SD' field") + return data_json, signature_hex + except json.JSONDecodeError as e: + raise ValueError(f"Invalid signature JSON format: {e}") + + +def hex_to_bytes(hex_str): + """Convert hex string to bytes, handling both with and without 0x prefix.""" + hex_str = hex_str.strip() + if hex_str.startswith("0x") or hex_str.startswith("0X"): + hex_str = hex_str[2:] + # Remove any whitespace or separators + hex_str = hex_str.replace(" ", "").replace(":", "").replace("-", "") + try: + return bytes.fromhex(hex_str) + except ValueError as e: + raise ValueError(f"Invalid hex string: {e}") + + +def load_public_key_from_hex(public_key_hex): + """ + Load ECDSA public key from hex string (uncompressed format). + + For brainpoolP384r1: + - Uncompressed format: 0x04 || X || Y (97 bytes = 194 hex chars) + - X and Y are each 48 bytes (96 hex chars) + """ + public_key_bytes = hex_to_bytes(public_key_hex) + + # For P384, uncompressed key should be 97 bytes (0x04 + 48 bytes X + 48 bytes Y) + if len(public_key_bytes) != 97: + raise ValueError(f"Expected 97 bytes for uncompressed P384 public key, got {len(public_key_bytes)} bytes") + + if public_key_bytes[0] != 0x04: + raise ValueError("Uncompressed public key must start with 0x04") + + # Extract X and Y coordinates (each 48 bytes) + x = public_key_bytes[1:49] + y = public_key_bytes[49:97] + + # Create public key using brainpoolP384r1 curve + public_numbers = ec.EllipticCurvePublicNumbers( + int.from_bytes(x, byteorder='big'), + int.from_bytes(y, byteorder='big'), + ec.BrainpoolP384R1() + ) + + return public_numbers.public_key(default_backend()) + + +def decode_signature(signature_hex): + """ + Decode signature from hex string. + + The signature can be in two formats: + 1. DER encoded (ASN.1 format) - standard for ECDSA + 2. Raw format: r || s (each 48 bytes for P384) + """ + signature_bytes = hex_to_bytes(signature_hex) + + # Try DER format first (most common) + try: + # For P384, DER signature is typically around 104-110 bytes + # Try to decode as DER + from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + r, s = decode_dss_signature(signature_bytes) + return r, s + except Exception: + # If DER fails, try raw format: r || s (each 48 bytes = 96 bytes total for P384) + if len(signature_bytes) == 96: + r = int.from_bytes(signature_bytes[:48], byteorder='big') + s = int.from_bytes(signature_bytes[48:], byteorder='big') + return r, s + else: + raise ValueError(f"Signature format not recognized. Expected DER or 96-byte raw format, got {len(signature_bytes)} bytes") + + +def normalize_json_for_ocmf(json_str): + """ + Normalize JSON string to compact format (no spaces) as required by OCMF spec. + OCMF signatures are computed over the compact JSON representation. + """ + try: + parsed = json.loads(json_str) + # Re-serialize with no spaces (compact format) + return json.dumps(parsed, separators=(',', ':'), ensure_ascii=False) + except json.JSONDecodeError: + # If it's not valid JSON, return as-is (might be plain text) + return json_str + + +def verify_signature(public_key, message, signature_hex, normalize_json=True): + """ + Verify ECDSA signature using brainpoolP384r1 and SHA256. + + Args: + public_key: ECDSA public key object + message: The message/text to verify (string or bytes) + signature_hex: Signature in hex format (DER or raw) + normalize_json: If True, normalize JSON to compact format (OCMF requirement) + + Returns: + bool: True if signature is valid, False otherwise + """ + # Convert message to bytes if it's a string + if isinstance(message, str): + # Normalize JSON if it looks like JSON (starts with { or [) + if normalize_json and (message.strip().startswith('{') or message.strip().startswith('[')): + message = normalize_json_for_ocmf(message) + message_bytes = message.encode('utf-8') + else: + message_bytes = message + + # Hash the message with SHA256 + message_hash = sha256(message_bytes).digest() + + # Decode signature + r, s = decode_signature(signature_hex) + + # Verify signature + try: + public_key.verify( + encode_dss_signature(r, s), + message_hash, + ec.ECDSA(hashes.SHA256()) + ) + return True + except Exception as e: + print(f"Signature verification failed: {e}") + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Validate ECDSA-brainpoolP384r1-SHA256 signatures for OCMF data", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Validate with separate components + python3 validate_ocmf_signature.py \\ + --public-key "04<194_hex_chars>" \\ + --text "data-to-be-signed" \\ + --signature "" + + # Validate from OCMF pipe-separated string + python3 validate_ocmf_signature.py \\ + --public-key "04<194_hex_chars>" \\ + --ocmf-string 'OCMF|{"data":"..."}|{"SD":"signature","SA":"ECDSA-brainpoolP384r1-SHA256"}' + + # Validate from OCMF JSON string + python3 validate_ocmf_signature.py \\ + --public-key "04<194_hex_chars>" \\ + --ocmf-json '{"SD":"data","SA":"ECDSA-brainpoolP384r1-SHA256","SI":"signature"}' + + # Validate from file + python3 validate_ocmf_signature.py \\ + --public-key "04<194_hex_chars>" \\ + --file ocmf_data.json \\ + --signature "" + """ + ) + + parser.add_argument( + '--public-key', + required=True, + help='Public key in hex format (uncompressed, 194 hex chars for P384)' + ) + + parser.add_argument( + '--text', + help='The text/message to verify (data-to-be-signed)' + ) + + parser.add_argument( + '--signature', + help='Signature in hex format (DER or raw r||s format)' + ) + + parser.add_argument( + '--file', + help='Read text from file (UTF-8)' + ) + + parser.add_argument( + '--ocmf-json', + help='OCMF JSON string containing SD (data) and SI (signature) fields' + ) + + parser.add_argument( + '--ocmf-string', + help='OCMF pipe-separated string format: OCMF||' + ) + + args = parser.parse_args() + + # Load public key + try: + print("Loading public key...") + public_key = load_public_key_from_hex(args.public_key) + print(f"✓ Public key loaded (brainpoolP384r1)") + except Exception as e: + print(f"✗ Error loading public key: {e}") + sys.exit(1) + + # Determine message and signature + message = None + signature = None + + if args.ocmf_string: + # Parse OCMF pipe-separated string + try: + message, signature = parse_ocmf_string(args.ocmf_string) + print(f"✓ Parsed OCMF string format") + print(f" Data length: {len(message)} characters") + print(f" Signature length: {len(signature)} hex characters") + except Exception as e: + print(f"✗ Error parsing OCMF string: {e}") + sys.exit(1) + elif args.ocmf_json: + # Parse OCMF JSON + try: + message, signature = parse_ocmf_json(args.ocmf_json) + print(f"✓ Parsed OCMF JSON") + print(f" Data length: {len(message)} characters") + print(f" Signature length: {len(signature)} hex characters") + except Exception as e: + print(f"✗ Error parsing OCMF JSON: {e}") + sys.exit(1) + elif args.file: + # Read from file + if not args.signature: + print("✗ Error: --signature is required when using --file") + sys.exit(1) + try: + with open(args.file, 'r', encoding='utf-8') as f: + message = f.read() + signature = args.signature + print(f"✓ Read message from file: {args.file}") + print(f" Message length: {len(message)} characters") + except Exception as e: + print(f"✗ Error reading file: {e}") + sys.exit(1) + elif args.text and args.signature: + # Direct text and signature + message = args.text + signature = args.signature + print(f"✓ Using provided text and signature") + print(f" Message length: {len(message)} characters") + else: + print("✗ Error: Must provide either --ocmf-string, --ocmf-json, or (--text and --signature), or (--file and --signature)") + sys.exit(1) + + # Normalize JSON for OCMF (compact format, no spaces) + # OCMF spec requires signatures to be computed over compact JSON + if isinstance(message, str) and (message.strip().startswith('{') or message.strip().startswith('[')): + try: + normalized_message = normalize_json_for_ocmf(message) + if normalized_message != message: + print(f"\n⚠ JSON normalization: Original had {len(message)} chars, compact has {len(normalized_message)} chars") + print(" Using compact JSON format for signature verification (OCMF requirement)") + message = normalized_message + # Debug: show the hash of normalized message + normalized_hash = sha256(normalized_message.encode('utf-8')).hexdigest() + print(f" Normalized message hash: {normalized_hash}") + except json.JSONDecodeError: + print(" Warning: Could not parse as JSON, using as-is") + + # Verify signature + print("\nVerifying signature...") + print(f" Algorithm: ECDSA-brainpoolP384r1-SHA256") + + # Double-check what we're about to hash + message_bytes = message.encode('utf-8') if isinstance(message, str) else message + final_hash = sha256(message_bytes).hexdigest() + print(f" Message length: {len(message)} characters ({len(message_bytes)} bytes)") + print(f" Message hash (SHA256): {final_hash}") + print(f" Message preview (first 100 chars): {message[:100]}...") + + # Also show what verify_signature will hash (should be the same) + try: + is_valid = verify_signature(public_key, message, signature, normalize_json=False) # Already normalized above + + if is_valid: + print("\n✓ SIGNATURE VALID - The message is authentic!") + sys.exit(0) + else: + print("\n✗ SIGNATURE INVALID - The message may have been tampered with!") + sys.exit(1) + except Exception as e: + print(f"\n✗ Error during verification: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() + diff --git a/modules/Misc/SerialCommHub/main/serial_communication_hubImpl.cpp b/modules/Misc/SerialCommHub/main/serial_communication_hubImpl.cpp index f035fd9591..3c4c85665f 100644 --- a/modules/Misc/SerialCommHub/main/serial_communication_hubImpl.cpp +++ b/modules/Misc/SerialCommHub/main/serial_communication_hubImpl.cpp @@ -101,6 +101,7 @@ serial_communication_hubImpl::perform_modbus_request(uint8_t device_address, tin types::serial_comm_hub_requests::Result result; std::vector response; auto retry_counter = config.retries + 1; + bool last_error_was_timeout = false; while (retry_counter > 0) { auto current_trial = config.retries + 1 - retry_counter + 1; @@ -109,9 +110,21 @@ serial_communication_hubImpl::perform_modbus_request(uint8_t device_address, tin config.retries + 1, tiny_modbus::FunctionCode_to_string_with_hex(function), device_address, first_register_address, first_register_address, register_quantity); + last_error_was_timeout = false; try { response = modbus.txrx(device_address, function, first_register_address, register_quantity, config.max_packet_size, wait_for_reply, request); + } catch (const tiny_modbus::TimeoutException& e) { + // TimeoutException is a specific type of communication error + last_error_was_timeout = true; + auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}", + tiny_modbus::FunctionCode_to_string_with_hex(function), device_address, + first_register_address, first_register_address, e.what()); + + if (retry_counter != 1) + EVLOG_debug << logmsg; + else + EVLOG_warning << logmsg; } catch (const tiny_modbus::TinyModbusException& e) { auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}", tiny_modbus::FunctionCode_to_string_with_hex(function), device_address, @@ -144,7 +157,12 @@ serial_communication_hubImpl::perform_modbus_request(uint8_t device_address, tin result.value = vector_to_int(response); system_error_logged = false; // reset after success } else { - result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error; + // If the last error was a timeout, return Timeout status, otherwise Error + if (last_error_was_timeout) { + result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Timeout; + } else { + result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error; + } } return result; } diff --git a/types/serial_comm_hub_requests.yaml b/types/serial_comm_hub_requests.yaml index 71d5028909..70145d8057 100644 --- a/types/serial_comm_hub_requests.yaml +++ b/types/serial_comm_hub_requests.yaml @@ -6,6 +6,7 @@ types: enum: - Success - Error + - Timeout Result: description: Return type for IO transfer functions with 16 bit return values type: object From 41b3268fad7f07c2f8b2963155ae933c97ace02a Mon Sep 17 00:00:00 2001 From: Florin Mihut Date: Thu, 18 Dec 2025 08:57:28 +0100 Subject: [PATCH 3/5] Fixes Signed-off-by: Florin Mihut --- .../CarloGavazzi_EM580/main/base64.hpp | 539 ------------------ .../main/powermeterImpl.cpp | 13 +- .../main/powermeterImpl.hpp | 2 +- .../CarloGavazzi_EM580/main/transport.cpp | 11 +- .../CarloGavazzi_EM580/main/transport.hpp | 9 +- .../CarloGavazzi_EM580/main/utils.hpp | 34 +- .../PowerMeters/RsIskraMeter/src/main.rs | 3 + 7 files changed, 36 insertions(+), 575 deletions(-) delete mode 100644 modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp deleted file mode 100644 index fafa67d327..0000000000 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/base64.hpp +++ /dev/null @@ -1,539 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest -#ifndef BASE64_HPP_ -#define BASE64_HPP_ - -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(__cpp_lib_bit_cast) -#include // For std::bit_cast. -#endif - -namespace base64 { - -namespace detail { - -#if defined(__cpp_lib_bit_cast) -using std::bit_cast; -#else -template -std::enable_if_t && std::is_trivially_copyable_v, - To> -bit_cast(const From& src) noexcept { - static_assert(std::is_trivially_constructible_v, "This implementation additionally requires " - "destination type to be trivially constructible"); - - To dst; - std::memcpy(&dst, &src, sizeof(To)); - return dst; -} -#endif - -inline constexpr char padding_char{'='}; -inline constexpr uint32_t bad_char{0x01FFFFFF}; - -#if !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) -#if (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || \ - (defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN) || (defined(_BYTE_ORDER) && _BYTE_ORDER == _BIG_ENDIAN) || \ - (defined(BYTE_ORDER) && BYTE_ORDER == BIG_ENDIAN) || \ - (defined(__sun) && defined(__SVR4) && defined(_BIG_ENDIAN)) || defined(__ARMEB__) || defined(__THUMBEB__) || \ - defined(__AARCH64EB__) || defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__) || defined(_M_PPC) -#define __BIG_ENDIAN__ -#elif (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || /* gcc */ \ - (defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN) /* linux header */ \ - || (defined(_BYTE_ORDER) && _BYTE_ORDER == _LITTLE_ENDIAN) || \ - (defined(BYTE_ORDER) && BYTE_ORDER == LITTLE_ENDIAN) /* mingw header */ || \ - (defined(__sun) && defined(__SVR4) && defined(_LITTLE_ENDIAN)) || /* solaris */ \ - defined(__ARMEL__) || defined(__THUMBEL__) || defined(__AARCH64EL__) || defined(_MIPSEL) || defined(__MIPSEL) || \ - defined(__MIPSEL__) || defined(_M_IX86) || defined(_M_X64) || defined(_M_IA64) || /* msvc for intel processors */ \ - defined(_M_ARM) /* msvc code on arm executes in little endian mode */ -#define __LITTLE_ENDIAN__ -#endif -#endif - -#if !defined(__LITTLE_ENDIAN__) & !defined(__BIG_ENDIAN__) -#error "UNKNOWN Platform / endianness. Configure endianness explicitly." -#endif - -#if defined(__LITTLE_ENDIAN__) -std::array constexpr decode_table_0 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x000000f8, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x000000fc, 0x000000d0, 0x000000d4, 0x000000d8, 0x000000dc, 0x000000e0, 0x000000e4, - 0x000000e8, 0x000000ec, 0x000000f0, 0x000000f4, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00000004, 0x00000008, 0x0000000c, 0x00000010, 0x00000014, 0x00000018, - 0x0000001c, 0x00000020, 0x00000024, 0x00000028, 0x0000002c, 0x00000030, 0x00000034, 0x00000038, 0x0000003c, - 0x00000040, 0x00000044, 0x00000048, 0x0000004c, 0x00000050, 0x00000054, 0x00000058, 0x0000005c, 0x00000060, - 0x00000064, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000068, 0x0000006c, - 0x00000070, 0x00000074, 0x00000078, 0x0000007c, 0x00000080, 0x00000084, 0x00000088, 0x0000008c, 0x00000090, - 0x00000094, 0x00000098, 0x0000009c, 0x000000a0, 0x000000a4, 0x000000a8, 0x000000ac, 0x000000b0, 0x000000b4, - 0x000000b8, 0x000000bc, 0x000000c0, 0x000000c4, 0x000000c8, 0x000000cc, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -std::array constexpr decode_table_1 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000e003, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x0000f003, 0x00004003, 0x00005003, 0x00006003, 0x00007003, 0x00008003, 0x00009003, - 0x0000a003, 0x0000b003, 0x0000c003, 0x0000d003, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, - 0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000, 0x0000d000, 0x0000e000, 0x0000f000, - 0x00000001, 0x00001001, 0x00002001, 0x00003001, 0x00004001, 0x00005001, 0x00006001, 0x00007001, 0x00008001, - 0x00009001, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000a001, 0x0000b001, - 0x0000c001, 0x0000d001, 0x0000e001, 0x0000f001, 0x00000002, 0x00001002, 0x00002002, 0x00003002, 0x00004002, - 0x00005002, 0x00006002, 0x00007002, 0x00008002, 0x00009002, 0x0000a002, 0x0000b002, 0x0000c002, 0x0000d002, - 0x0000e002, 0x0000f002, 0x00000003, 0x00001003, 0x00002003, 0x00003003, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -std::array constexpr decode_table_2 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00800f00, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00c00f00, 0x00000d00, 0x00400d00, 0x00800d00, 0x00c00d00, 0x00000e00, 0x00400e00, - 0x00800e00, 0x00c00e00, 0x00000f00, 0x00400f00, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00400000, 0x00800000, 0x00c00000, 0x00000100, 0x00400100, 0x00800100, - 0x00c00100, 0x00000200, 0x00400200, 0x00800200, 0x00c00200, 0x00000300, 0x00400300, 0x00800300, 0x00c00300, - 0x00000400, 0x00400400, 0x00800400, 0x00c00400, 0x00000500, 0x00400500, 0x00800500, 0x00c00500, 0x00000600, - 0x00400600, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00800600, 0x00c00600, - 0x00000700, 0x00400700, 0x00800700, 0x00c00700, 0x00000800, 0x00400800, 0x00800800, 0x00c00800, 0x00000900, - 0x00400900, 0x00800900, 0x00c00900, 0x00000a00, 0x00400a00, 0x00800a00, 0x00c00a00, 0x00000b00, 0x00400b00, - 0x00800b00, 0x00c00b00, 0x00000c00, 0x00400c00, 0x00800c00, 0x00c00c00, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -std::array constexpr decode_table_3 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x003e0000, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x003f0000, 0x00340000, 0x00350000, 0x00360000, 0x00370000, 0x00380000, 0x00390000, - 0x003a0000, 0x003b0000, 0x003c0000, 0x003d0000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00010000, 0x00020000, 0x00030000, 0x00040000, 0x00050000, 0x00060000, - 0x00070000, 0x00080000, 0x00090000, 0x000a0000, 0x000b0000, 0x000c0000, 0x000d0000, 0x000e0000, 0x000f0000, - 0x00100000, 0x00110000, 0x00120000, 0x00130000, 0x00140000, 0x00150000, 0x00160000, 0x00170000, 0x00180000, - 0x00190000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x001a0000, 0x001b0000, - 0x001c0000, 0x001d0000, 0x001e0000, 0x001f0000, 0x00200000, 0x00210000, 0x00220000, 0x00230000, 0x00240000, - 0x00250000, 0x00260000, 0x00270000, 0x00280000, 0x00290000, 0x002a0000, 0x002b0000, 0x002c0000, 0x002d0000, - 0x002e0000, 0x002f0000, 0x00300000, 0x00310000, 0x00320000, 0x00330000, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -// TODO fix decoding tables to avoid the need for different indices in big -// endian? -inline constexpr size_t decidx0{0}; -inline constexpr size_t decidx1{1}; -inline constexpr size_t decidx2{2}; - -#elif defined(__BIG_ENDIAN__) - -std::array constexpr decode_table_0 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00f80000, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00fc0000, 0x00d00000, 0x00d40000, 0x00d80000, 0x00dc0000, 0x00e00000, 0x00e40000, - 0x00e80000, 0x00ec0000, 0x00f00000, 0x00f40000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00040000, 0x00080000, 0x000c0000, 0x00100000, 0x00140000, 0x00180000, - 0x001c0000, 0x00200000, 0x00240000, 0x00280000, 0x002c0000, 0x00300000, 0x00340000, 0x00380000, 0x003c0000, - 0x00400000, 0x00440000, 0x00480000, 0x004c0000, 0x00500000, 0x00540000, 0x00580000, 0x005c0000, 0x00600000, - 0x00640000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00680000, 0x006c0000, - 0x00700000, 0x00740000, 0x00780000, 0x007c0000, 0x00800000, 0x00840000, 0x00880000, 0x008c0000, 0x00900000, - 0x00940000, 0x00980000, 0x009c0000, 0x00a00000, 0x00a40000, 0x00a80000, 0x00ac0000, 0x00b00000, 0x00b40000, - 0x00b80000, 0x00bc0000, 0x00c00000, 0x00c40000, 0x00c80000, 0x00cc0000, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -std::array constexpr decode_table_1 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0003e000, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x0003f000, 0x00034000, 0x00035000, 0x00036000, 0x00037000, 0x00038000, 0x00039000, - 0x0003a000, 0x0003b000, 0x0003c000, 0x0003d000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, - 0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000, 0x0000d000, 0x0000e000, 0x0000f000, - 0x00010000, 0x00011000, 0x00012000, 0x00013000, 0x00014000, 0x00015000, 0x00016000, 0x00017000, 0x00018000, - 0x00019000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0001a000, 0x0001b000, - 0x0001c000, 0x0001d000, 0x0001e000, 0x0001f000, 0x00020000, 0x00021000, 0x00022000, 0x00023000, 0x00024000, - 0x00025000, 0x00026000, 0x00027000, 0x00028000, 0x00029000, 0x0002a000, 0x0002b000, 0x0002c000, 0x0002d000, - 0x0002e000, 0x0002f000, 0x00030000, 0x00031000, 0x00032000, 0x00033000, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -std::array constexpr decode_table_2 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000f80, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000fc0, 0x00000d00, 0x00000d40, 0x00000d80, 0x00000dc0, 0x00000e00, 0x00000e40, - 0x00000e80, 0x00000ec0, 0x00000f00, 0x00000f40, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00000040, 0x00000080, 0x000000c0, 0x00000100, 0x00000140, 0x00000180, - 0x000001c0, 0x00000200, 0x00000240, 0x00000280, 0x000002c0, 0x00000300, 0x00000340, 0x00000380, 0x000003c0, - 0x00000400, 0x00000440, 0x00000480, 0x000004c0, 0x00000500, 0x00000540, 0x00000580, 0x000005c0, 0x00000600, - 0x00000640, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000680, 0x000006c0, - 0x00000700, 0x00000740, 0x00000780, 0x000007c0, 0x00000800, 0x00000840, 0x00000880, 0x000008c0, 0x00000900, - 0x00000940, 0x00000980, 0x000009c0, 0x00000a00, 0x00000a40, 0x00000a80, 0x00000ac0, 0x00000b00, 0x00000b40, - 0x00000b80, 0x00000bc0, 0x00000c00, 0x00000c40, 0x00000c80, 0x00000cc0, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -std::array constexpr decode_table_3 = { - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000003e, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x0000003f, 0x00000034, 0x00000035, 0x00000036, 0x00000037, 0x00000038, 0x00000039, - 0x0000003a, 0x0000003b, 0x0000003c, 0x0000003d, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x00000000, 0x00000001, 0x00000002, 0x00000003, 0x00000004, 0x00000005, 0x00000006, - 0x00000007, 0x00000008, 0x00000009, 0x0000000a, 0x0000000b, 0x0000000c, 0x0000000d, 0x0000000e, 0x0000000f, - 0x00000010, 0x00000011, 0x00000012, 0x00000013, 0x00000014, 0x00000015, 0x00000016, 0x00000017, 0x00000018, - 0x00000019, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000001a, 0x0000001b, - 0x0000001c, 0x0000001d, 0x0000001e, 0x0000001f, 0x00000020, 0x00000021, 0x00000022, 0x00000023, 0x00000024, - 0x00000025, 0x00000026, 0x00000027, 0x00000028, 0x00000029, 0x0000002a, 0x0000002b, 0x0000002c, 0x0000002d, - 0x0000002e, 0x0000002f, 0x00000030, 0x00000031, 0x00000032, 0x00000033, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, - 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff}; - -// TODO fix decoding tables to avoid the need for different indices in big -// endian? -inline constexpr size_t decidx0{1}; -inline constexpr size_t decidx1{2}; -inline constexpr size_t decidx2{3}; - -#endif - -std::array constexpr encode_table_0 = { - 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'C', 'C', 'C', 'C', 'D', 'D', 'D', 'D', 'E', 'E', 'E', 'E', 'F', 'F', - 'F', 'F', 'G', 'G', 'G', 'G', 'H', 'H', 'H', 'H', 'I', 'I', 'I', 'I', 'J', 'J', 'J', 'J', 'K', 'K', 'K', 'K', - 'L', 'L', 'L', 'L', 'M', 'M', 'M', 'M', 'N', 'N', 'N', 'N', 'O', 'O', 'O', 'O', 'P', 'P', 'P', 'P', 'Q', 'Q', - 'Q', 'Q', 'R', 'R', 'R', 'R', 'S', 'S', 'S', 'S', 'T', 'T', 'T', 'T', 'U', 'U', 'U', 'U', 'V', 'V', 'V', 'V', - 'W', 'W', 'W', 'W', 'X', 'X', 'X', 'X', 'Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z', 'a', 'a', 'a', 'a', 'b', 'b', - 'b', 'b', 'c', 'c', 'c', 'c', 'd', 'd', 'd', 'd', 'e', 'e', 'e', 'e', 'f', 'f', 'f', 'f', 'g', 'g', 'g', 'g', - 'h', 'h', 'h', 'h', 'i', 'i', 'i', 'i', 'j', 'j', 'j', 'j', 'k', 'k', 'k', 'k', 'l', 'l', 'l', 'l', 'm', 'm', - 'm', 'm', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'o', 'p', 'p', 'p', 'p', 'q', 'q', 'q', 'q', 'r', 'r', 'r', 'r', - 's', 's', 's', 's', 't', 't', 't', 't', 'u', 'u', 'u', 'u', 'v', 'v', 'v', 'v', 'w', 'w', 'w', 'w', 'x', 'x', - 'x', 'x', 'y', 'y', 'y', 'y', 'z', 'z', 'z', 'z', '0', '0', '0', '0', '1', '1', '1', '1', '2', '2', '2', '2', - '3', '3', '3', '3', '4', '4', '4', '4', '5', '5', '5', '5', '6', '6', '6', '6', '7', '7', '7', '7', '8', '8', - '8', '8', '9', '9', '9', '9', '+', '+', '+', '+', '/', '/', '/', '/'}; - -std::array constexpr encode_table_1 = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', - 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', - 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', - 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', - 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', - 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', - 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', - 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; - -} // namespace detail - -template -inline OutputBuffer encode_into(InputIterator begin, InputIterator end) { - typedef std::decay_t input_value_type; - static_assert(std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v); - typedef typename OutputBuffer::value_type output_value_type; - static_assert(std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v); - const size_t binarytextsize = end - begin; - const size_t encodedsize = (binarytextsize / 3 + (binarytextsize % 3 > 0)) << 2; - OutputBuffer encoded(encodedsize, detail::padding_char); - - const uint8_t* bytes = reinterpret_cast(&*begin); - char* currEncoding = reinterpret_cast(&encoded[0]); - - for (size_t i = binarytextsize / 3; i; --i) { - const uint8_t t1 = *bytes++; - const uint8_t t2 = *bytes++; - const uint8_t t3 = *bytes++; - *currEncoding++ = detail::encode_table_0[t1]; - *currEncoding++ = detail::encode_table_1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)]; - *currEncoding++ = detail::encode_table_1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)]; - *currEncoding++ = detail::encode_table_1[t3]; - } - - switch (binarytextsize % 3) { - case 0: { - break; - } - case 1: { - const uint8_t t1 = bytes[0]; - *currEncoding++ = detail::encode_table_0[t1]; - *currEncoding++ = detail::encode_table_1[(t1 & 0x03) << 4]; - // *currEncoding++ = detail::padding_char; - // *currEncoding++ = detail::padding_char; - break; - } - case 2: { - const uint8_t t1 = bytes[0]; - const uint8_t t2 = bytes[1]; - *currEncoding++ = detail::encode_table_0[t1]; - *currEncoding++ = detail::encode_table_1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)]; - *currEncoding++ = detail::encode_table_1[(t2 & 0x0F) << 2]; - // *currEncoding++ = detail::padding_char; - break; - } - default: { - throw std::runtime_error{"Invalid base64 encoded data"}; - } - } - - return encoded; -} - -template inline OutputBuffer encode_into(std::string_view data) { - return encode_into(std::begin(data), std::end(data)); -} - -inline std::string to_base64(std::string_view data) { - return encode_into(std::begin(data), std::end(data)); -} - -template inline OutputBuffer decode_into(std::string_view base64Text) { - typedef typename OutputBuffer::value_type output_value_type; - static_assert(std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v); - if (base64Text.empty()) { - return OutputBuffer(); - } - - if ((base64Text.size() & 3) != 0) { - throw std::runtime_error{"Invalid base64 encoded data - Size not divisible by 4"}; - } - - const size_t numPadding = std::count(base64Text.rbegin(), base64Text.rbegin() + 4, '='); - if (numPadding > 2) { - throw std::runtime_error{"Invalid base64 encoded data - Found more than 2 padding signs"}; - } - - const size_t decodedsize = (base64Text.size() * 3 >> 2) - numPadding; - OutputBuffer decoded(decodedsize, '.'); - - const uint8_t* bytes = reinterpret_cast(&base64Text[0]); - char* currDecoding = reinterpret_cast(&decoded[0]); - - for (size_t i = (base64Text.size() >> 2) - (numPadding != 0); i; --i) { - const uint8_t t1 = *bytes++; - const uint8_t t2 = *bytes++; - const uint8_t t3 = *bytes++; - const uint8_t t4 = *bytes++; - - const uint32_t d1 = detail::decode_table_0[t1]; - const uint32_t d2 = detail::decode_table_1[t2]; - const uint32_t d3 = detail::decode_table_2[t3]; - const uint32_t d4 = detail::decode_table_3[t4]; - - const uint32_t temp = d1 | d2 | d3 | d4; - - if (temp >= detail::bad_char) { - throw std::runtime_error{"Invalid base64 encoded data - Invalid character"}; - } - - // Use bit_cast instead of union and type punning to avoid - // undefined behaviour risk: - // https://en.wikipedia.org/wiki/Type_punning#Use_of_union - const std::array tempBytes = detail::bit_cast, uint32_t>(temp); - - *currDecoding++ = tempBytes[detail::decidx0]; - *currDecoding++ = tempBytes[detail::decidx1]; - *currDecoding++ = tempBytes[detail::decidx2]; - } - - switch (numPadding) { - case 0: { - break; - } - case 1: { - const uint8_t t1 = *bytes++; - const uint8_t t2 = *bytes++; - const uint8_t t3 = *bytes++; - - const uint32_t d1 = detail::decode_table_0[t1]; - const uint32_t d2 = detail::decode_table_1[t2]; - const uint32_t d3 = detail::decode_table_2[t3]; - - const uint32_t temp = d1 | d2 | d3; - - if (temp >= detail::bad_char) { - throw std::runtime_error{"Invalid base64 encoded data - Invalid character"}; - } - - // Use bit_cast instead of union and type punning to avoid - // undefined behaviour risk: - // https://en.wikipedia.org/wiki/Type_punning#Use_of_union - const std::array tempBytes = detail::bit_cast, uint32_t>(temp); - *currDecoding++ = tempBytes[detail::decidx0]; - *currDecoding++ = tempBytes[detail::decidx1]; - break; - } - case 2: { - const uint8_t t1 = *bytes++; - const uint8_t t2 = *bytes++; - - const uint32_t d1 = detail::decode_table_0[t1]; - const uint32_t d2 = detail::decode_table_1[t2]; - - const uint32_t temp = d1 | d2; - - if (temp >= detail::bad_char) { - throw std::runtime_error{"Invalid base64 encoded data - Invalid character"}; - } - - const std::array tempBytes = detail::bit_cast, uint32_t>(temp); - *currDecoding++ = tempBytes[detail::decidx0]; - break; - } - default: { - throw std::runtime_error{"Invalid base64 encoded data - Invalid padding number"}; - } - } - - return decoded; -} - -template -inline OutputBuffer decode_into(InputIterator begin, InputIterator end) { - typedef std::decay_t input_value_type; - static_assert(std::is_same_v || std::is_same_v || - std::is_same_v || std::is_same_v); - std::string_view data(reinterpret_cast(&*begin), end - begin); - return decode_into(data); -} - -inline std::string from_base64(std::string_view data) { - return decode_into(data); -} - -} // namespace base64 - -#endif // BASE64_HPP_ diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp index bbceeafc0e..a4ad7b0744 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp @@ -150,7 +150,7 @@ void powermeterImpl::init() { if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault", "CommunicationError")) { EVLOG_error << "Raising CommunicationFault: " << error_message; auto error = this->error_factory->create_error("powermeter/CommunicationFault", "CommunicationError", - error_message, Everest::error::Severity::High); + error_message, Everest::error::Severity::High); raise_error(error); } }; @@ -166,8 +166,8 @@ void powermeterImpl::init() { p_modbus_transport = move(std::make_unique( *(mod->r_modbus.get()), config.powermeter_device_id, MODBUS_BASE_ADDRESS, config.initial_connection_retry_count, - config.initial_connection_retry_delay_ms, config.communication_retry_count, - config.communication_retry_delay_ms, error_handler, clear_error_handler)); + config.initial_connection_retry_delay_ms, config.communication_retry_count, config.communication_retry_delay_ms, + error_handler, clear_error_handler)); } void powermeterImpl::read_signature_config() { @@ -590,10 +590,13 @@ powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& treq std::string tariff_text = treq.tariff_text.value_or("") + "<=>" + treq.transaction_id; if (!validate_string_for_em580(tariff_text)) { EVLOG_error << "String contains invalid characters for EM580 device: '" << tariff_text << "'"; - return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "Invalid tariff text (device supports only an subset of ASCII characters)"}; + return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, + {}, + {}, + "Invalid tariff text (device supports only an subset of ASCII characters)"}; } else { std::vector tariff_text_data = - string_to_modbus_char_array(tariff_text, MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT); + string_to_modbus_char_array(tariff_text, MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT); p_modbus_transport->write_multiple_registers(MODBUS_OCMF_TARIFF_TEXT_ADDRESS, tariff_text_data); } diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp index 33983f27ae..e1f40029cc 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp @@ -33,7 +33,7 @@ class powermeterImpl : public powermeterImplBase { public: powermeterImpl() = delete; powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - powermeterImplBase(ev, "main"), mod(mod), config(config){}; + powermeterImplBase(ev, "main"), mod(mod), config(config) {}; // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp index 48efb88e7f..0698e9288b 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp @@ -28,9 +28,9 @@ transport::DataVector SerialCommHubTransport::fetch(int address, int register_co if (serial_com_hub_result.status_code == types::serial_comm_hub_requests::StatusCodeEnum::Timeout) { throw transport::ModbusTimeoutException("Modbus read timeout: Packet receive timeout"); } else if (serial_com_hub_result.status_code != types::serial_comm_hub_requests::StatusCodeEnum::Success) { - std::string error_msg = "Modbus read failed with status: " + - types::serial_comm_hub_requests::status_code_enum_to_string( - serial_com_hub_result.status_code); + std::string error_msg = + "Modbus read failed with status: " + + types::serial_comm_hub_requests::status_code_enum_to_string(serial_com_hub_result.status_code); throw std::runtime_error(error_msg); } @@ -38,8 +38,9 @@ transport::DataVector SerialCommHubTransport::fetch(int address, int register_co throw std::runtime_error("no result from serial com hub!"); // make sure that returned vector is a int32 vector - static_assert(std::is_same_v); + static_assert( + std::is_same_v); union { int32_t val_32; diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp index e1b4811d8d..f693744c41 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.hpp @@ -28,7 +28,8 @@ using DataVector = std::vector; // Custom exception to distinguish timeout errors from other Modbus errors class ModbusTimeoutException : public std::runtime_error { public: - explicit ModbusTimeoutException(const std::string& message) : std::runtime_error(message) {} + explicit ModbusTimeoutException(const std::string& message) : std::runtime_error(message) { + } }; // Error handler callback type: void(error_message) @@ -81,7 +82,8 @@ class SerialCommHubTransport : public AbstractModbusTransport { // First successful call - switch to normal mode bool was_initial = m_initial_connection_mode.exchange(false); // Clear CommunicationFault error if communication is restored - // Only clear if we're not in initial connection mode (i.e., we've had at least one successful operation) + // Only clear if we're not in initial connection mode (i.e., we've had at least one successful + // operation) if (m_clear_error_handler && !was_initial) { m_clear_error_handler(); } @@ -135,7 +137,8 @@ class SerialCommHubTransport : public AbstractModbusTransport { // First successful call - switch to normal mode bool was_initial = m_initial_connection_mode.exchange(false); // Clear CommunicationFault error if communication is restored - // Only clear if we're not in initial connection mode (i.e., we've had at least one successful operation) + // Only clear if we're not in initial connection mode (i.e., we've had at least one successful + // operation) if (m_clear_error_handler && !was_initial) { m_clear_error_handler(); } diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp index 12f70e340c..7bbf158e59 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp @@ -10,12 +10,12 @@ #include #include -#include "base64.hpp" +// #include "base64.hpp" #include "transport.hpp" /** * @brief Utility functions for converting Modbus data to various types - * + * * These functions handle byte order conversion from Modbus (big-endian) format * to native integer types. Modbus transmits data in big-endian format where * the most significant byte comes first. @@ -25,16 +25,22 @@ namespace modbus_utils { // Strong type wrappers to prevent parameter swapping struct ByteOffset { - explicit ByteOffset(transport::DataVector::size_type v) : value(v) {} - operator transport::DataVector::size_type() const { return value; } + explicit ByteOffset(transport::DataVector::size_type v) : value(v) { + } + operator transport::DataVector::size_type() const { + return value; + } private: transport::DataVector::size_type value; }; struct ByteLength { - explicit ByteLength(transport::DataVector::size_type v) : value(v) {} - operator transport::DataVector::size_type() const { return value; } + explicit ByteLength(transport::DataVector::size_type v) : value(v) { + } + operator transport::DataVector::size_type() const { + return value; + } private: transport::DataVector::size_type value; @@ -96,22 +102,6 @@ inline std::string to_hex_string(const transport::DataVector& data, ByteOffset o return ss.str(); } -/** - * @brief Convert a range of bytes to a Base64-encoded string - * @param data The Modbus data vector - * @param offset Byte offset into the data vector - * @param length Number of bytes to convert - * @return Base64-encoded string - */ -inline std::string to_base64_string(const transport::DataVector& data, ByteOffset offset, ByteLength length) { - const auto off = static_cast(offset); - const auto len = static_cast(length); - auto begin = std::begin(data) + off; - auto end = begin + len; - return base64::encode_into(begin, end); -} - } // namespace modbus_utils #endif // POWERMETER_UTILS_HPP - diff --git a/modules/HardwareDrivers/PowerMeters/RsIskraMeter/src/main.rs b/modules/HardwareDrivers/PowerMeters/RsIskraMeter/src/main.rs index 7b45b4094b..958e5c53c6 100644 --- a/modules/HardwareDrivers/PowerMeters/RsIskraMeter/src/main.rs +++ b/modules/HardwareDrivers/PowerMeters/RsIskraMeter/src/main.rs @@ -160,6 +160,7 @@ impl From for Result<()> { match value { StatusCodeEnum::Success => Ok(()), StatusCodeEnum::Error => anyhow::bail!("StatusCodeEnum::Error"), + StatusCodeEnum::Timeout => anyhow::bail!("StatusCodeEnum::Timeout"), } } } @@ -180,6 +181,7 @@ impl generated::types::serial_comm_hub_requests::Result { } }, StatusCodeEnum::Error => anyhow::bail!("StatusCodeEnum::Error"), + StatusCodeEnum::Timeout => anyhow::bail!("StatusCodeEnum::Timeout"), } } } @@ -205,6 +207,7 @@ impl From fo } }, StatusCodeEnum::Error => anyhow::bail!("StatusCodeEnum::Error"), + StatusCodeEnum::Timeout => anyhow::bail!("StatusCodeEnum::Timeout"), } } } From 498761473a88676c6378741b1a7a6be9af70dea2 Mon Sep 17 00:00:00 2001 From: Florin Mihut Date: Thu, 18 Dec 2025 17:27:17 +0100 Subject: [PATCH 4/5] Commit Signed-off-by: Florin Mihut --- config/bringup/config-bringup-CGEM580.yaml | 4 ++++ .../CarloGavazzi_EM580/main/powermeterImpl.cpp | 8 +++++--- .../CarloGavazzi_EM580/main/powermeterImpl.hpp | 2 ++ .../CarloGavazzi_EM580/main/transport.cpp | 2 +- .../PowerMeters/CarloGavazzi_EM580/manifest.yaml | 12 ++++++++++++ 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/config/bringup/config-bringup-CGEM580.yaml b/config/bringup/config-bringup-CGEM580.yaml index 382e3a61c8..20ce912099 100644 --- a/config/bringup/config-bringup-CGEM580.yaml +++ b/config/bringup/config-bringup-CGEM580.yaml @@ -1,5 +1,6 @@ settings: telemetry_enabled: false + mqtt_broker_socket_path: /run/mosquitto/mosquitto.sock active_modules: cgem580: module: CarloGavazzi_EM580 @@ -7,6 +8,8 @@ active_modules: main: powermeter_device_id: 1 timezone_offset_minutes: 60 + live_measurement_interval_ms: 1000 # once per second + communication_error_pause_delay_s: 10 # pause 10 seconds on communication error before retry connections: modbus: # required interface: serial_communication_hub - module_id: comm_hub @@ -19,6 +22,7 @@ active_modules: serial_port: /dev/ttyUSB0 # adjust to your device path baudrate: 115200 parity: 0 # 0=None,1=Odd,2=Even (match your device) + within_message_timeout_ms: 10 cli: config_module: diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp index a4ad7b0744..1f90df943e 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp @@ -301,6 +301,7 @@ void powermeterImpl::ready() { std::thread live_measure_publisher_thread([this] { std::atomic_bool device_not_configured = true; while (true) { + const auto measurement_interval = std::chrono::milliseconds{config.live_measurement_interval_ms}; try { if (device_not_configured.load()) { configure_device(); @@ -309,11 +310,12 @@ void powermeterImpl::ready() { read_powermeter_values(); read_device_state(); } catch (const std::exception& e) { - EVLOG_error << "Failed to communicate with the device, try again in 10 seconds: " << e.what(); + EVLOG_error << "Failed to communicate with the device, try again in " + << config.communication_error_pause_delay_s << " seconds: " << e.what(); device_not_configured = true; - std::this_thread::sleep_for(std::chrono::seconds(10)); + std::this_thread::sleep_for(std::chrono::seconds{config.communication_error_pause_delay_s}); } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(measurement_interval); } }); live_measure_publisher_thread.detach(); diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp index e1f40029cc..a23a5a35a2 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.hpp @@ -27,6 +27,8 @@ struct Conf { int initial_connection_retry_count; int initial_connection_retry_delay_ms; int timezone_offset_minutes; + int live_measurement_interval_ms; + int communication_error_pause_delay_s; }; class powermeterImpl : public powermeterImplBase { diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp index 0698e9288b..5a6f9d93cf 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/transport.cpp @@ -11,7 +11,7 @@ namespace transport { transport::DataVector SerialCommHubTransport::fetch(int address, int register_count) { return retry_with_config([this, address, register_count]() { transport::DataVector response; - response.reserve(register_count * 2); // this is a uint 8 vector + response.reserve(register_count * 2); // this is a uint8_t vector int remaining_register_to_read{register_count}; int read_address{address - m_base_address}; diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml index 3e21c6315f..2e668e4003 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/manifest.yaml @@ -22,6 +22,12 @@ provides: minimum: 10 maximum: 10000 default: 500 + communication_error_pause_delay_s: + description: Delay in seconds before retrying communication in the live measurement thread after a failure. Default 10 seconds. Applies to initial communication too. + type: integer + minimum: 1 + maximum: 600 + default: 10 initial_connection_retry_count: description: Number of retries for initial connection/signature config read during module initialization. 0 means no infinite retries. type: integer @@ -40,6 +46,12 @@ provides: minimum: -1440 maximum: 1440 default: 0 + live_measurement_interval_ms: + description: Interval in milliseconds between live powermeter reads and publishes. Default 1000 ms (once per second). Allowed range 500-60000 ms (twice per second to once per minute). + type: integer + minimum: 500 + maximum: 60000 + default: 1000 requires: modbus: interface: serial_communication_hub From 3d0099eaf45094276afc8c64b83b4d6aec9cb140 Mon Sep 17 00:00:00 2001 From: Florin Mihut Date: Fri, 19 Dec 2025 10:03:20 +0100 Subject: [PATCH 5/5] Beautify Signed-off-by: Florin Mihut --- .../PowerMeters/CarloGavazzi_EM580/main/utils.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp index 7bbf158e59..ffc8083a0c 100644 --- a/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp +++ b/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/utils.hpp @@ -97,7 +97,7 @@ inline std::string to_hex_string(const transport::DataVector& data, ByteOffset o const auto len = static_cast(length); std::stringstream ss; for (std::size_t index = 0; index < len; ++index) { - ss << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << static_cast(data[index + off]); + ss << std::uppercase << std::hex << std::setfill('0') << std::setw(2) << static_cast(data[off + index]); } return ss.str(); }