// SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest #ifndef CARLO_GAVAZZI_EM580_HELPER_HPP #define CARLO_GAVAZZI_EM580_HELPER_HPP #include #include #include #include #include #include #include #include #include #include #include #include #include "transport.hpp" #include namespace em580 { namespace registers { constexpr std::int32_t MODBUS_BASE_ADDRESS = 300001; // the following identification register is only accessible/visible when a direct single access is used constexpr std::int32_t MODBUS_IDENTIFICATION_CODE_ADDRESS = 300012; // 000Bh: Carlo Gavazzi Controls identification code constexpr std::int32_t MODBUS_SIGNATURE_TYPE_ADDRESS = 309472; // 24FFh: Signature type (UINT16) constexpr std::int32_t MODBUS_PUBLIC_KEY_ADDRESS = 309473; // 2500h: Public key (UINT16[130]) // DER formatted public key (Table 4.20/4.21), mandatory to read whole block // from 2600h. constexpr std::int32_t MODBUS_PUBLIC_KEY_DER_ADDRESS = 309729; // 2600h: Public key DER (read-only) constexpr std::uint16_t MODBUS_PUBLIC_KEY_DER_WORD_COUNT_256 = 46; // 2600h..262Dh (92 bytes, DER length 0x5A + 2) constexpr std::uint16_t MODBUS_PUBLIC_KEY_DER_WORD_COUNT_384 = 62; // 2600h..263Dh (124 bytes, DER length 0x7A + 2) constexpr std::int32_t MODBUS_SIGNED_MAP_ADDRESS = 302049; constexpr std::uint16_t MODBUS_SIGNED_MAP_WORD_COUNT_256 = 93; // 61 words signed Data + 32 words signature constexpr std::uint16_t MODBUS_SIGNED_MAP_WORD_COUNT_384 = 109; // 61 words signed Data + 48 words signature constexpr std::int32_t MODBUS_REAL_TIME_VALUES_ADDRESS = 300001; // We only need instantaneous values up to 300052 (frequency) for the live polling loop. // Energy totals are read from 301281+ (INT64, Wh) and signed values from 302049+. constexpr std::uint16_t MODBUS_REAL_TIME_VALUES_COUNT = 52; // Registers 300001-300052 (52 words) constexpr std::int32_t MODBUS_REAL_TIME_ENERGY_ADDRESS_EM300_SERIES = 301025; constexpr std::uint16_t MODBUS_REAL_TIME_ENERGY_COUNT_EM300_SERIES = 12; // Table 2.5-1: 301025-301036 (12 words) constexpr std::int32_t MODBUS_REAL_TIME_ENERGY_ADDRESS = 301281; constexpr std::uint16_t MODBUS_REAL_TIME_ENERGY_COUNT = 32; // Registers 301281-301312 (32 words) constexpr std::int32_t MODBUS_TEMPERATURE_ADDRESS = 300776; // Internal Temperature constexpr std::int32_t MODBUS_FIRMWARE_MEASURE_MODULE_ADDRESS = 300771; // Measure module firmware version/revision constexpr std::int32_t MODBUS_FIRMWARE_COMMUNICATION_MODULE_ADDRESS = 300772; // Communication module firmware version/revision constexpr std::int32_t MODBUS_SERIAL_NUMBER_START_ADDRESS = 320481; // Serial number (7 registers: 320481-320487) constexpr std::uint16_t MODBUS_SERIAL_NUMBER_REGISTER_COUNT = 7; // 7 UINT16 registers = 14 bytes constexpr std::int32_t MODBUS_PRODUCTION_YEAR_ADDRESS = 320488; // Production year (1 UINT16 register) // same register for older series, but with following note in datasheet: // This register is available only in EM330 and EM340 manufactured from // October 1st 2018 (from serial number YR2018 274xxxS and following) constexpr std::int32_t MODBUS_PRODUCTION_YEAR_ADDRESS_EM300_SERIES = 320497; // Device state register (Table 4.30, Section 4.3.6) constexpr std::int32_t MODBUS_DEVICE_STATE_ADDRESS = 320499; // 5012h: Device state (UINT16 bitfield) // Time synchronization registers constexpr std::int32_t MODBUS_UTC_TIMESTAMP_ADDRESS = 328723; // UTC Timestamp for synchronization (INT64, 4 words) constexpr std::int32_t MODBUS_TIMEZONE_OFFSET_ADDRESS = 328722; // Local time delta in minutes (INT16, 1 word) // OCMF Transaction registers (Table 4.34) constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS = 328673; // 7000h: OCMF Ident. Status (UINT16) constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS = 328674; // 7001h: OCMF Ident. Level (UINT16) constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS = 328675; // 7002h: OCMF Ident. Flags 1-4 (4 UINT16) constexpr std::uint16_t MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT = 4; // 4 flags constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS = 328679; // 7006h: OCMF Ident. Type (UINT16) constexpr std::int32_t MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS = 328680; // 7007h: OCMF Ident. Data (CHAR[40] = 20 words) constexpr std::uint16_t MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT = 20; // 40 bytes = 20 words constexpr std::int32_t MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS = 328700; // 701Bh: OCMF Charging point identifier type (UINT16) constexpr std::int32_t MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS = 328701; // 701Ch: OCMF CPI (CHAR[40] = 20 words) constexpr std::uint16_t MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT = 20; // 40 bytes = 20 words constexpr std::int32_t MODBUS_OCMF_SESSION_MODALITY_ADDRESS = 328727; // 7036h: OCMF Session Modality (UINT16) constexpr std::uint16_t MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE = 0; // Charging vehicle constexpr std::uint16_t MODBUS_OCMF_SESSION_MODALITY_VEHICLE_TO_GRID = 1; // Vehicle to grid constexpr std::uint16_t MODBUS_OCMF_SESSION_MODALITY_BIDIRECTIONAL = 2; // Bidirectional // Tariff text register (Table 4.32) // 326881 (6900h): Tariff text (CHAR[252] = 126 words) constexpr std::int32_t MODBUS_OCMF_TARIFF_TEXT_ADDRESS = 326881; // 6900h: Tariff text (CHAR[252] = 126 words) constexpr std::uint16_t MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT = 126; // 252 bytes = 126 words (CHAR[252]) constexpr std::int32_t MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS = 328417; // 6F00h: OCMF Transaction ID Generation (UINT16) // Tariff update register (Table 4.33) constexpr std::int32_t MODBUS_OCMF_TARIFF_UPDATE_ADDRESS = 327085; // 69CCh: Tariff update (UINT16) // OCMF Command register (Table 4.35) // The register is UINT16 containing the ASCII code (e.g. 'B', 'E', 'A'). constexpr std::int32_t MODBUS_OCMF_COMMAND_ADDRESS = 328737; // 7040h: OCMF Command Data (UINT16) constexpr std::uint16_t MODBUS_OCMF_COMMAND_START = 0x42; // Start transaction ('B') constexpr std::uint16_t MODBUS_OCMF_COMMAND_END = 0x45; // End transaction ('E') constexpr std::uint16_t MODBUS_OCMF_COMMAND_ABORT = 0x41; // Abort transaction ('A') // OCMF State / status registers (Table 4.39 and related) constexpr std::int32_t MODBUS_OCMF_STATE_ADDRESS = 328929; // 7100h: OCMF State (UINT16) constexpr std::int32_t MODBUS_OCMF_TRANSACTION_ID_ADDRESS = 328931; // 7102h: OCMF Transaction ID (UINT32) constexpr std::uint16_t MODBUS_OCMF_STATE_NOT_READY = 0; // Not ready constexpr std::uint16_t MODBUS_OCMF_STATE_RUNNING = 1; // Running constexpr std::uint16_t MODBUS_OCMF_STATE_READY = 2; // Ready constexpr std::uint16_t MODBUS_OCMF_STATE_CORRUPTED = 3; // Corrupted constexpr std::int32_t MODBUS_OCMF_STATE_SIZE_ADDRESS = 328930; // 7101h: OCMF Size (UINT16) constexpr std::int32_t MODBUS_OCMF_STATE_FILE_ADDRESS = 328945; // 7110h: OCMF File (max theoretically 2031 words) constexpr std::uint16_t MODBUS_OCMF_STATE_FILE_WORD_COUNT = 2031; // 2031 words = 4062 bytes constexpr std::int32_t MODBUS_OCMF_CHARGING_STATUS_ADDRESS = 328742; // 7045h: Charging status (UINT16) constexpr std::int32_t MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS = 328762; // 7059h: Last transaction id (CHAR[]) constexpr std::uint16_t MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT = 7; // 14 bytes = 7 words constexpr std::int32_t MODBUS_OCMF_TIME_SYNC_STATUS_ADDRESS = 328769; // 7060h: Time synchronization status (UINT16) } // namespace registers } // namespace em580 namespace modbus_utils { inline void check_bounds_or_throw(const transport::DataVector& data, transport::DataVector::size_type offset, transport::DataVector::size_type needed_bytes, const char* what) { if (offset > data.size() || needed_bytes > (data.size() - offset)) { throw std::out_of_range(std::string(what) + ": offset/length out of range (offset=" + std::to_string(offset) + ", needed=" + std::to_string(needed_bytes) + ", size=" + std::to_string(data.size()) + ")"); } } // Strong type wrappers to prevent parameter swapping struct ByteOffset { explicit ByteOffset(transport::DataVector::size_type val) : value(val) { } explicit operator transport::DataVector::size_type() const { return value; } private: transport::DataVector::size_type value; }; struct ByteLength { explicit ByteLength(transport::DataVector::size_type val) : value(val) { } explicit operator transport::DataVector::size_type() const { return value; } private: transport::DataVector::size_type value; }; inline std::uint32_t to_uint32(const transport::DataVector& data, ByteOffset offset) { const auto off = static_cast(offset); check_bounds_or_throw(data, off, 4, "to_uint32"); return static_cast(data[off] << 24 | data[off + 1] << 16 | data[off + 2] << 8 | data[off + 3]); } inline std::int32_t to_int32(const transport::DataVector& data, ByteOffset offset) { const auto off = static_cast(offset); check_bounds_or_throw(data, off, 4, "to_int32"); return static_cast(data[off + 2] << 24 | data[off + 3] << 16 | data[off] << 8 | data[off + 1]); } inline std::int64_t to_int64(const transport::DataVector& data, ByteOffset offset) { const auto off = static_cast(offset); check_bounds_or_throw(data, off, 8, "to_int64"); // EM580 Modbus spec: // - Byte order inside a word is MSB -> LSB. // - Word order for INT64/UINT64 is LSW -> MSW. const std::uint64_t w0 = (static_cast(data[off]) << 8) | static_cast(data[off + 1]); const std::uint64_t w1 = (static_cast(data[off + 2]) << 8) | static_cast(data[off + 3]); const std::uint64_t w2 = (static_cast(data[off + 4]) << 8) | static_cast(data[off + 5]); const std::uint64_t w3 = (static_cast(data[off + 6]) << 8) | static_cast(data[off + 7]); const std::uint64_t u = (w0) | (w1 << 16) | (w2 << 32) | (w3 << 48); return static_cast(u); } inline std::uint16_t to_uint16(const transport::DataVector& data, ByteOffset offset) { const auto off = static_cast(offset); check_bounds_or_throw(data, off, 2, "to_uint16"); return static_cast(data[off] << 8 | data[off + 1]); } inline std::int16_t to_int16(const transport::DataVector& data, ByteOffset offset) { std::uint16_t raw = to_uint16(data, offset); return static_cast(raw); } 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); check_bounds_or_throw(data, off, len, "to_hex_string"); 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[off + index]); } return ss.str(); } inline std::size_t max_payload_bytes_for_words(std::size_t max_words) { const std::size_t capacity_bytes = max_words * 2; return capacity_bytes > 0 ? capacity_bytes - 1 : 0; // reserve NUL } inline void log_truncation_warning_if_needed(const char* field_name, const std::string& value, std::size_t max_words) { const std::size_t capacity_bytes = max_words * 2; const std::size_t max_payload_bytes = max_payload_bytes_for_words(max_words); if (value.size() > max_payload_bytes) { EVLOG_warning << field_name << " too long (" << value.size() << " bytes). Max is " << max_payload_bytes << " bytes (" << max_words << " words / " << capacity_bytes << " bytes incl. NUL). " << "It will be truncated."; } } /// Converts a string to a big-endian Modbus CHAR array (vector of UINT16 words) /// that is **0-terminated** and contains only the **used** part (i.e. no full /// fixed-length padding). /// /// - Max capacity is `max_words * 2` bytes. /// - Ensures a terminating `\\0` byte is present within the returned data. /// - If `str` is too long, it is truncated to fit `max_words * 2 - 1` bytes (+ /// 1 byte terminator). inline std::vector string_to_modbus_char_array(const std::string& str, std::size_t max_words) { const std::size_t max_bytes = max_words * 2; if (max_bytes == 0) { return {}; } const std::size_t used_len = std::min(str.size(), max_bytes - 1); // leave space for terminator const std::size_t bytes_to_write = used_len + 1; // include terminator byte const std::size_t words_to_write = (bytes_to_write + 1) / 2; // ceil(bytes/2) std::vector data(words_to_write, 0); for (std::size_t i = 0; i < used_len; ++i) { const std::size_t word_idx = i / 2; if ((i % 2) == 0) { data[word_idx] = static_cast(str[i]) << 8; } else { data[word_idx] |= static_cast(str[i]); } } return data; } } // namespace modbus_utils namespace ocmf { /// Confirm OCMF file read by writing NOT_READY (0) into the OCMF state /// register. inline void confirm_file_read(transport::AbstractModbusTransport& modbus_transport) { std::vector ocmf_confirmation_data = {em580::registers::MODBUS_OCMF_STATE_NOT_READY}; modbus_transport.write_multiple_registers(em580::registers::MODBUS_OCMF_STATE_ADDRESS, ocmf_confirmation_data); } /// Wait until OCMF state becomes READY (2). /// @return true if READY, false on CORRUPTED or timeout. inline bool wait_for_ready(transport::AbstractModbusTransport& modbus_transport, std::chrono::milliseconds poll_interval = std::chrono::milliseconds{100}, int max_retries = 10) { std::uint16_t state = em580::registers::MODBUS_OCMF_STATE_NOT_READY; transport::DataVector state_data; int retries = 0; while (state != em580::registers::MODBUS_OCMF_STATE_READY) { state_data = modbus_transport.fetch(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1); state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0}); if (state == em580::registers::MODBUS_OCMF_STATE_CORRUPTED) { return false; } if (state != em580::registers::MODBUS_OCMF_STATE_READY) { EVLOG_info << "OCMF state: " << state; std::this_thread::sleep_for(poll_interval); retries++; if (retries > max_retries) { return false; } } } return true; } inline bool is_uuid36(const std::string& s) { if (s.size() != 36) { return false; } for (std::size_t i = 0; i < s.size(); ++i) { const char c = s[i]; if (i == 8 || i == 13 || i == 18 || i == 23) { if (c != '-') { return false; } continue; } if (!std::isxdigit(static_cast(c))) { return false; } } return true; } inline std::optional extract_transaction_id_from_ocmf_record(const std::string& ocmf_record) { const std::string key = "\"TT\""; std::size_t key_pos = ocmf_record.find(key); if (key_pos == std::string::npos) { return std::nullopt; } std::size_t colon_pos = ocmf_record.find(':', key_pos + key.size()); if (colon_pos == std::string::npos) { return std::nullopt; } std::size_t value_start = ocmf_record.find('"', colon_pos + 1); if (value_start == std::string::npos) { return std::nullopt; } ++value_start; std::string tt_value; tt_value.reserve(128); bool escaped = false; for (std::size_t i = value_start; i < ocmf_record.size(); ++i) { const char c = ocmf_record[i]; if (escaped) { tt_value.push_back(c); escaped = false; continue; } if (c == '\\') { escaped = true; continue; } if (c == '"') { break; } tt_value.push_back(c); } const std::string marker = "<=>"; const std::size_t marker_pos = tt_value.rfind(marker); if (marker_pos == std::string::npos) { return std::nullopt; } std::string tail = tt_value.substr(marker_pos + marker.size()); while (!tail.empty() && std::isspace(static_cast(tail.front()))) { tail.erase(tail.begin()); } while (!tail.empty() && std::isspace(static_cast(tail.back()))) { tail.pop_back(); } std::optional last_uuid; if (tail.size() >= 36) { for (std::size_t i = 0; i + 36 <= tail.size(); ++i) { const std::string candidate = tail.substr(i, 36); if (is_uuid36(candidate)) { last_uuid = candidate; } } } return last_uuid; } /// Extract transaction id (UUID) from a tariff text string. /// /// Driver convention: tariff text is written as "<=>". /// Returns the last UUID found after the "<=>" marker. inline std::optional extract_transaction_id_from_tariff_text(const std::string& tariff_text, std::string_view marker) { const std::size_t marker_pos = tariff_text.rfind(marker); if (marker_pos == std::string::npos) { return std::nullopt; } std::string tail = tariff_text.substr(marker_pos + marker.size()); while (!tail.empty() && std::isspace(static_cast(tail.front()))) { tail.erase(tail.begin()); } while (!tail.empty() && std::isspace(static_cast(tail.back()))) { tail.pop_back(); } // The transaction id is appended at the end, so search from the back. if (tail.size() < 36) { return std::nullopt; } for (std::size_t i = tail.size() - 36 + 1; i-- > 0;) { const std::string candidate = tail.substr(i, 36); if (is_uuid36(candidate)) { return candidate; } } return std::nullopt; } inline std::uint16_t flag_to_value(types::powermeter::OCMFIdentificationFlags flag) { 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; } inline std::uint16_t level_to_value(types::powermeter::OCMFIdentificationLevel level) { switch (level) { case types::powermeter::OCMFIdentificationLevel::NONE: return 0; case types::powermeter::OCMFIdentificationLevel::HEARSAY: return 1; case types::powermeter::OCMFIdentificationLevel::TRUSTED: return 2; case types::powermeter::OCMFIdentificationLevel::VERIFIED: return 3; case types::powermeter::OCMFIdentificationLevel::CERTIFIED: return 4; case types::powermeter::OCMFIdentificationLevel::SECURE: return 5; case types::powermeter::OCMFIdentificationLevel::MISMATCH: return 6; case types::powermeter::OCMFIdentificationLevel::INVALID: return 7; case types::powermeter::OCMFIdentificationLevel::OUTDATED: return 8; case types::powermeter::OCMFIdentificationLevel::UNKNOWN: return 9; } return 0; } inline std::uint16_t type_to_value(types::powermeter::OCMFIdentificationType type) { switch (type) { case types::powermeter::OCMFIdentificationType::NONE: return 0; case types::powermeter::OCMFIdentificationType::DENIED: return 1; case types::powermeter::OCMFIdentificationType::UNDEFINED: return 2; case types::powermeter::OCMFIdentificationType::ISO14443: return 10; case types::powermeter::OCMFIdentificationType::ISO15693: return 11; case types::powermeter::OCMFIdentificationType::EMAID: return 20; case types::powermeter::OCMFIdentificationType::EVCCID: return 21; case types::powermeter::OCMFIdentificationType::EVCOID: return 30; case types::powermeter::OCMFIdentificationType::ISO7812: return 40; case types::powermeter::OCMFIdentificationType::CARD_TXN_NR: return 50; case types::powermeter::OCMFIdentificationType::CENTRAL: return 60; case types::powermeter::OCMFIdentificationType::CENTRAL_1: return 61; case types::powermeter::OCMFIdentificationType::CENTRAL_2: return 62; case types::powermeter::OCMFIdentificationType::LOCAL: return 70; case types::powermeter::OCMFIdentificationType::LOCAL_1: return 71; case types::powermeter::OCMFIdentificationType::LOCAL_2: return 72; case types::powermeter::OCMFIdentificationType::PHONE_NUMBER: return 80; case types::powermeter::OCMFIdentificationType::KEY_CODE: return 90; } return 0; } } // namespace ocmf namespace device_state_utils { inline std::vector decode_device_state_errors(std::uint16_t device_state) { struct BitError { const char* message; std::uint16_t bit; }; static constexpr std::array errors = {{ {"V1N over maximum range", 0U}, {"V2N over maximum range", 1U}, {"V3N over maximum range", 2U}, {"V12 over maximum range", 3U}, {"V23 over maximum range", 4U}, {"V31 over maximum range", 5U}, {"I1 over maximum range", 6U}, {"I2 over maximum range", 7U}, {"I3 over maximum range", 8U}, {"Frequency outside validity range", 9U}, {"EVCS module internal fault", 12U}, {"Measure module internal fault", 13U}, }}; std::vector out; for (const auto& err : errors) { if ((device_state & static_cast(1U << err.bit)) != 0U) { out.emplace_back(err.message); } } return out; } } // namespace device_state_utils #endif // CARLO_GAVAZZI_EM580_HELPER_HPP