Files
cariflex/tools/EVerest-main/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580/main/powermeterImpl.cpp
Eric F d398a6ced2 Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
2026-06-08 00:38:27 -04:00

1085 lines
55 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <set>
#include <stdexcept>
#include <thread>
#include <utility>
#include <date/date.h>
#include <fmt/core.h>
#include <utils/date.hpp>
#include "everest/logging.hpp"
#include "helper.hpp"
#include "powermeterImpl.hpp"
namespace {
using em580::registers::MODBUS_BASE_ADDRESS;
using em580::registers::MODBUS_DEVICE_STATE_ADDRESS;
using em580::registers::MODBUS_FIRMWARE_COMMUNICATION_MODULE_ADDRESS;
using em580::registers::MODBUS_FIRMWARE_MEASURE_MODULE_ADDRESS;
using em580::registers::MODBUS_IDENTIFICATION_CODE_ADDRESS;
using em580::registers::MODBUS_OCMF_CHARGING_POINT_ID_START_ADDRESS;
using em580::registers::MODBUS_OCMF_CHARGING_POINT_ID_TYPE_ADDRESS;
using em580::registers::MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT;
using em580::registers::MODBUS_OCMF_CHARGING_STATUS_ADDRESS;
using em580::registers::MODBUS_OCMF_COMMAND_ADDRESS;
using em580::registers::MODBUS_OCMF_COMMAND_END;
using em580::registers::MODBUS_OCMF_COMMAND_START;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_DATA_START_ADDRESS;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_FLAGS_COUNT;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_FLAGS_START_ADDRESS;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_LEVEL_ADDRESS;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_STATUS_ADDRESS;
using em580::registers::MODBUS_OCMF_IDENTIFICATION_TYPE_ADDRESS;
using em580::registers::MODBUS_OCMF_LAST_TRANSACTION_ID_ADDRESS;
using em580::registers::MODBUS_OCMF_LAST_TRANSACTION_ID_WORD_COUNT;
using em580::registers::MODBUS_OCMF_SESSION_MODALITY_ADDRESS;
using em580::registers::MODBUS_OCMF_SESSION_MODALITY_CHARGING_VEHICLE;
using em580::registers::MODBUS_OCMF_STATE_ADDRESS;
using em580::registers::MODBUS_OCMF_STATE_FILE_ADDRESS;
using em580::registers::MODBUS_OCMF_STATE_NOT_READY;
using em580::registers::MODBUS_OCMF_STATE_READY;
using em580::registers::MODBUS_OCMF_STATE_RUNNING;
using em580::registers::MODBUS_OCMF_STATE_SIZE_ADDRESS;
using em580::registers::MODBUS_OCMF_TARIFF_TEXT_ADDRESS;
using em580::registers::MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT;
using em580::registers::MODBUS_OCMF_TIME_SYNC_STATUS_ADDRESS;
using em580::registers::MODBUS_OCMF_TRANSACTION_ID_GENERATION_ADDRESS;
using em580::registers::MODBUS_PRODUCTION_YEAR_ADDRESS;
using em580::registers::MODBUS_PRODUCTION_YEAR_ADDRESS_EM300_SERIES;
using em580::registers::MODBUS_PUBLIC_KEY_ADDRESS;
using em580::registers::MODBUS_PUBLIC_KEY_DER_ADDRESS;
using em580::registers::MODBUS_PUBLIC_KEY_DER_WORD_COUNT_256;
using em580::registers::MODBUS_PUBLIC_KEY_DER_WORD_COUNT_384;
using em580::registers::MODBUS_REAL_TIME_ENERGY_ADDRESS;
using em580::registers::MODBUS_REAL_TIME_ENERGY_ADDRESS_EM300_SERIES;
using em580::registers::MODBUS_REAL_TIME_ENERGY_COUNT;
using em580::registers::MODBUS_REAL_TIME_ENERGY_COUNT_EM300_SERIES;
using em580::registers::MODBUS_REAL_TIME_VALUES_ADDRESS;
using em580::registers::MODBUS_REAL_TIME_VALUES_COUNT;
using em580::registers::MODBUS_SERIAL_NUMBER_REGISTER_COUNT;
using em580::registers::MODBUS_SERIAL_NUMBER_START_ADDRESS;
using em580::registers::MODBUS_SIGNATURE_TYPE_ADDRESS;
using em580::registers::MODBUS_TEMPERATURE_ADDRESS;
using em580::registers::MODBUS_TIMEZONE_OFFSET_ADDRESS;
using em580::registers::MODBUS_UTC_TIMESTAMP_ADDRESS;
using em580::registers::MODBUS_SIGNED_MAP_ADDRESS;
using em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256;
using em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_384;
const char* ocmf_state_to_string(std::uint16_t ocmf_state) {
switch (ocmf_state) {
case MODBUS_OCMF_STATE_READY:
return "READY";
case MODBUS_OCMF_STATE_RUNNING:
return "RUNNING";
case MODBUS_OCMF_STATE_NOT_READY:
return "NOT_READY";
default:
return "UNKNOWN";
}
}
} // namespace
// 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 std::size_t V_L1_N = 0; // 300001 (0000h)
constexpr std::size_t V_L2_N = 4; // 300003 (0002h)
constexpr std::size_t V_L3_N = 8; // 300005 (0004h)
// Current registers (INT32, 4 bytes each)
constexpr std::size_t A_L1 = 24; // 300013 (000Ch)
constexpr std::size_t A_L2 = 28; // 300015 (000Eh)
constexpr std::size_t A_L3 = 32; // 300017 (0010h)
// Power registers (INT32, 4 bytes each)
constexpr std::size_t W_L1 = 36; // 300019 (0012h)
constexpr std::size_t W_L2 = 40; // 300021 (0014h)
constexpr std::size_t W_L3 = 44; // 300023 (0016h)
constexpr std::size_t W_SYS = 80; // 300041 (0028h)
// Reactive power registers (INT32, 4 bytes each)
constexpr std::size_t VAR_L1 = 60; // 300031 (001Eh)
constexpr std::size_t VAR_L2 = 64; // 300033 (0020h)
constexpr std::size_t VAR_L3 = 68; // 300035 (0022h)
constexpr std::size_t VAR_SYS = 88; // 300045 (002Ch)
// Phase sequence register (INT16, 2 bytes)
constexpr std::size_t PHASE_SEQUENCE = 100; // 300051 (0032h)
// Frequency register (INT16, 2 bytes)
constexpr std::size_t FREQUENCY = 102; // 300052 (0033h)
// Energy registers (INT64, 8 bytes each) - within extended read range
constexpr std::size_t ENERGY_IMPORT = 0; // 301281 (0500h) - kWh (+) TOT, byte offset 0 (52*2)
constexpr std::size_t ENERGY_EXPORT = 56; // 301309 (051Ch) - kWh (-) TOT, byte offset 28 (28*2)
// Energy registers (INT32, 4 bytes each) — Table 2.5-1 EM/ET300 Modbus (rev 2.17)
constexpr std::size_t ENERGY_IMPORT_INT = 0; // 301025 (0400h) - kWh (+) TOT INT, byte offset 0 (0*2)
constexpr std::size_t ENERGY_IMPORT_DEC = 4; // 301027 (0402h) - kWh (+) TOT DEC, byte offset 4 (2*2)
constexpr std::size_t ENERGY_EXPORT_INT = 16; // 301033 (0408h) - kWh (-) TOT INT, byte offset 16 (8*2)
constexpr std::size_t ENERGY_EXPORT_DEC = 20; // 301035 (040Ah) - kWh (-) TOT DEC, byte offset 20 (10*2)
} // 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 TEMPERATURE = 0.1F; // Value weight: Temperature*10
constexpr float ENERGY_INT = 1000.0F; // Value weight: kWh*1
constexpr float ENERGY_DEC = 1.0F; // Value weight: kWh*1000
} // namespace Factors
namespace module::main {
namespace {
/// Build a stop-transaction reply with explicit fields (avoids brace-init field-order mistakes vs
/// types/powermeter.yaml).
[[nodiscard]] types::powermeter::TransactionStopResponse
make_transaction_stop_response(types::powermeter::TransactionRequestStatus status) {
types::powermeter::TransactionStopResponse response;
response.status = status;
return response;
}
[[nodiscard]] types::powermeter::TransactionStopResponse
make_transaction_stop_response(types::powermeter::TransactionRequestStatus status, std::string error) {
types::powermeter::TransactionStopResponse response;
response.status = status;
response.error = std::move(error);
return response;
}
/// Build a start-transaction reply with explicit fields (types/powermeter.yaml: status, error, min/max stop time).
[[nodiscard]] types::powermeter::TransactionStartResponse
make_transaction_start_response(types::powermeter::TransactionRequestStatus status) {
types::powermeter::TransactionStartResponse response;
response.status = status;
return response;
}
[[nodiscard]] types::powermeter::TransactionStartResponse
make_transaction_start_response(types::powermeter::TransactionRequestStatus status, std::string error) {
types::powermeter::TransactionStartResponse response;
response.status = status;
response.error = std::move(error);
return response;
}
} // namespace
powermeterImpl::~powermeterImpl() {
stop_requested_.store(true);
stop_cv_.notify_all();
if (live_measure_thread_.joinable()) {
live_measure_thread_.join();
}
if (time_sync_thread_.joinable()) {
time_sync_thread_.join();
}
}
void powermeterImpl::init() {
m_pending_closed_transaction = false;
// 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 (!error_state_monitor->is_error_active("powermeter/CommunicationFault", "CommunicationError")) {
EVLOG_error << "Raising CommunicationFault: " << error_message;
auto error = 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 (error_state_monitor->is_error_active("powermeter/CommunicationFault", "CommunicationError")) {
EVLOG_info << "Clearing CommunicationFault: Communication restored";
clear_error("powermeter/CommunicationFault", "CommunicationError");
}
};
const transport::SerialCommHubTransport::RetryConfig retry_config{
config.initial_connection_retry_count,
config.initial_connection_retry_delay_ms,
config.communication_retry_count,
config.communication_retry_delay_ms,
};
const transport::SerialCommHubTransport::TransportConfig transport_config{
config.powermeter_device_id,
MODBUS_BASE_ADDRESS,
retry_config,
};
p_modbus_transport = std::make_unique<transport::SerialCommHubTransport>(*mod->r_modbus, transport_config,
error_handler, clear_error_handler);
}
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<SignatureType>(modbus_utils::to_uint16(data, modbus_utils::ByteOffset{0}));
};
auto read_public_key_in_hex = [this](int lengthInBits) {
const transport::DataVector data =
p_modbus_transport->fetch(MODBUS_PUBLIC_KEY_ADDRESS, static_cast<std::uint16_t>((lengthInBits >> 3) + 1));
// Table 4.18/4.19: last byte is unused and always 0x00; keep the 0x04
// prefix, drop the unused byte.
return modbus_utils::to_hex_string(data, modbus_utils::ByteOffset{0},
modbus_utils::ByteLength{data.size() - 1});
};
auto read_public_key_der_in_hex = [this](std::uint16_t der_word_count) {
// Table 4.20/4.21: mandatory to read whole block starting at 2600h.
const transport::DataVector der_data = p_modbus_transport->fetch(MODBUS_PUBLIC_KEY_DER_ADDRESS, der_word_count);
std::size_t der_len = der_data.size();
if (der_data.size() >= 2 && der_data[0] == 0x30) {
// DER header is: 0x30 <len> ...
der_len = std::min<std::size_t>(der_data.size(), static_cast<std::size_t>(2 + der_data[1]));
}
return modbus_utils::to_hex_string(der_data, modbus_utils::ByteOffset{0}, modbus_utils::ByteLength{der_len});
};
const SignatureType signature_type = read_signature_type();
std::string signature_type_string;
std::uint16_t der_word_count = 0;
switch (signature_type) {
case SIGNATURE_256_BIT:
m_public_key_length_in_bits = 256;
signature_type_string = "256-bit";
m_signature_method_string = "ECDSA-brainpoolP256r1-SHA256";
der_word_count = MODBUS_PUBLIC_KEY_DER_WORD_COUNT_256;
// Spec Table 4.5: 256-bit signature is 32 words (= CHAR[64] = 64 bytes)
m_signed_map_word_count = MODBUS_SIGNED_MAP_WORD_COUNT_256;
break;
case SIGNATURE_384_BIT:
m_public_key_length_in_bits = 384;
signature_type_string = "384-bit";
m_signature_method_string = "ECDSA-brainpoolP384r1-SHA256";
der_word_count = MODBUS_PUBLIC_KEY_DER_WORD_COUNT_384;
// Spec Table 4.6: 384-bit signature is 48 words (= CHAR[96] = 96 bytes)
m_signed_map_word_count = MODBUS_SIGNED_MAP_WORD_COUNT_384;
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;
if (config.public_key_format == "binary") {
m_public_key_hex = read_public_key_in_hex(m_public_key_length_in_bits);
EVLOG_info << "Public key (raw, hex): " << m_public_key_hex;
} else if (config.public_key_format == "der") {
m_public_key_hex = read_public_key_der_in_hex(der_word_count);
EVLOG_info << "Public key (DER, hex): " << m_public_key_hex;
} else {
throw std::invalid_argument("invalid public key format: " + config.public_key_format);
}
publish_public_key_ocmf(m_public_key_hex);
}
void powermeterImpl::read_identification() {
static const std::set<std::uint16_t> em300_series_ids = {331, 332, 335, 336, 340, 341, 345, 346, 355};
// Read the identification code to detect meter model
transport::DataVector cgc_id_data = p_modbus_transport->fetch(MODBUS_IDENTIFICATION_CODE_ADDRESS, 1);
std::uint16_t cgc_id = modbus_utils::to_uint16(cgc_id_data, modbus_utils::ByteOffset{0});
EVLOG_info << "Carlo Gavazzi Controls identification code: " << (int)cgc_id;
// check for EM300/ET300 series
if (em300_series_ids.count(cgc_id)) {
m_transaction_support = false;
}
}
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);
std::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
std::uint8_t major = (measure_fw_value >> 8) & 0xF0;
major = major >> 4; // Shift right to get actual major version (0-15)
std::uint8_t minor = (measure_fw_value >> 8) & 0x0F;
std::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);
std::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);
std::string serial_str;
if (m_transaction_support) {
// 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
serial_str.reserve(14);
for (const auto& byte : serial_data) {
char byte_char = static_cast<char>(byte);
// Stop at null terminator if present
if (byte_char == '\0') {
break;
}
serial_str += byte_char;
}
} else {
// on older devices like EM300 series, only the LSB is used
serial_str.reserve(7);
for (auto byte = serial_data.begin() + 1; byte < serial_data.end(); byte += 2) {
char byte_char = static_cast<char>(*byte);
// Stop at null terminator if present
if (byte_char == '\0') {
break;
}
serial_str += byte_char;
}
}
// production year register moved in newer devices:
// register 320488 on newer device like EM580,
// register 320497 on EM300 series
// we coupled it here to the transaction support to keep things simple
transport::DataVector year_data = p_modbus_transport->fetch(
m_transaction_support ? MODBUS_PRODUCTION_YEAR_ADDRESS : MODBUS_PRODUCTION_YEAR_ADDRESS_EM300_SERIES, 1);
std::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::read_transaction_state_and_id() {
transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1);
std::uint16_t ocmf_state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0});
// Read transaction id from the tariff text (6900h, TT) which we write as:
// "<tariff_text><=><transaction_id>".
try {
transport::DataVector tt_data =
p_modbus_transport->fetch(MODBUS_OCMF_TARIFF_TEXT_ADDRESS, MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT);
auto null_pos = std::find(tt_data.begin(), tt_data.end(), 0);
std::string tt_str(tt_data.begin(), null_pos);
const auto tx_id_opt =
ocmf::extract_transaction_id_from_tariff_text(tt_str, powermeterImpl::TARIFF_TEXT_TRANSACTION_ID_MARKER);
if (tx_id_opt.has_value()) {
m_transaction_id = *tx_id_opt;
EVLOG_info << "Recovered transaction id from tariff text (6900h): " << m_transaction_id;
}
} catch (const std::exception& e) {
EVLOG_warning << "Failed to read tariff text (6900h): " << e.what();
}
if (ocmf_state == MODBUS_OCMF_STATE_READY) {
m_pending_closed_transaction = true;
EVLOG_info << "Detected a closed transaction with data pending to be read";
}
if (ocmf_state == MODBUS_OCMF_STATE_RUNNING) {
EVLOG_info << "Detected a running transaction, waiting for a stop transaction command with transaction id: "
<< m_transaction_id << " or an empty transaction id";
m_transaction_active.store(true);
}
}
void powermeterImpl::configure_device() {
EVLOG_info << "Configure the device...";
read_identification();
read_firmware_versions();
read_serial_number();
if (m_transaction_support) {
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);
// see if there is a pending closed transaction that needs to be read
read_transaction_state_and_id();
}
EVLOG_info << "Device configured";
}
void powermeterImpl::ready() {
// Retry logic is now handled by SerialCommHubTransport
live_measure_thread_ = std::thread([this] {
std::atomic_bool device_not_configured = true;
auto last_device_state_read = std::chrono::steady_clock::time_point{};
while (!stop_requested_.load()) {
const auto measurement_interval = std::chrono::milliseconds{config.live_measurement_interval_ms};
const auto device_state_interval = std::chrono::milliseconds{config.device_state_read_interval_ms};
try {
if (device_not_configured.load()) {
configure_device();
device_not_configured = false;
last_device_state_read = std::chrono::steady_clock::time_point{}; // force state read
}
read_powermeter_values();
if (m_transaction_support) {
const auto now = std::chrono::steady_clock::now();
if (last_device_state_read == std::chrono::steady_clock::time_point{} ||
(now - last_device_state_read) >= device_state_interval) {
read_device_state();
last_device_state_read = now;
}
}
} catch (const std::invalid_argument& e) {
EVLOG_error << "Configuration error (will not retry): " << e.what();
break;
} catch (const std::exception& e) {
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::unique_lock<std::mutex> lock(stop_mutex_);
stop_cv_.wait_for(lock, std::chrono::seconds{config.communication_error_pause_delay_s},
[this] { return stop_requested_.load(); });
}
}
{
std::unique_lock<std::mutex> lock(stop_mutex_);
stop_cv_.wait_for(lock, measurement_interval, [this] { return stop_requested_.load(); });
}
}
});
// Start time synchronization thread
time_sync_thread_ = std::thread([this]() { time_sync_thread(); });
}
void powermeterImpl::write_transaction_registers(const types::powermeter::TransactionReq& transaction_req) {
// 1. Write OCMF Identification Status (register 328673, 7000h)
// 0 = NOT_ASSIGNED (False), 1 = ASSIGNED (True)
std::uint16_t identification_status_value =
(transaction_req.identification_status == types::powermeter::OCMFUserIdentificationStatus::ASSIGNED) ? 1 : 0;
std::vector<std::uint16_t> 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
std::uint16_t identification_level_value = 0; // Default: NONE
if (transaction_req.identification_level.has_value()) {
identification_level_value = ocmf::level_to_value(transaction_req.identification_level.value());
}
std::vector<std::uint16_t> 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<std::uint16_t> 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] = ocmf::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)
std::uint16_t identification_type_value = ocmf::type_to_value(transaction_req.identification_type);
std::vector<std::uint16_t> 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.
std::string client_id_str = transaction_req.identification_data.value_or("");
modbus_utils::log_truncation_warning_if_needed("OCMF Identification Data", client_id_str,
MODBUS_OCMF_IDENTIFICATION_DATA_WORD_COUNT);
std::vector<std::uint16_t> id_data =
modbus_utils::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)
std::uint16_t charging_point_id_type = 0; // EVSEID
std::vector<std::uint16_t> 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)
modbus_utils::log_truncation_warning_if_needed("OCMF Charging Point Identifier (EVSE ID)", transaction_req.evse_id,
MODBUS_OCMF_CHARGING_POINT_ID_WORD_COUNT);
std::vector<std::uint16_t> evse_id_data =
modbus_utils::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 device accepts partial writes as long as the string is 0-terminated.
std::string tariff_text;
const std::string& base = transaction_req.tariff_text.value_or("");
tariff_text.reserve(base.size() + TARIFF_TEXT_TRANSACTION_ID_MARKER.size() + transaction_req.transaction_id.size());
tariff_text.append(base);
tariff_text.append(TARIFF_TEXT_TRANSACTION_ID_MARKER);
tariff_text.append(transaction_req.transaction_id);
modbus_utils::log_truncation_warning_if_needed("OCMF Tariff Text (TT)", tariff_text,
MODBUS_OCMF_TARIFF_TEXT_WORD_COUNT);
const std::vector<std::uint16_t> tariff_text_data =
modbus_utils::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);
}
std::string powermeterImpl::read_ocmf_file() {
transport::DataVector size_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_SIZE_ADDRESS, 1);
std::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");
}
transport::DataVector file_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_FILE_ADDRESS, size);
return std::string{file_data.begin(), file_data.end()};
}
void powermeterImpl::clear_transaction_states() {
transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1);
std::uint16_t ocmf_state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0});
if (ocmf_state == MODBUS_OCMF_STATE_READY) {
EVLOG_info << "Current OCMF state: " << ocmf_state_to_string(ocmf_state) << "(" << ocmf_state << ")";
EVLOG_info << "Cleanup necessary ...";
read_ocmf_file();
// write 0 to the OCMF state to confirm the reading of the OCMF file
std::vector<std::uint16_t> ocmf_confirmation_data = {MODBUS_OCMF_STATE_NOT_READY};
p_modbus_transport->write_multiple_registers(MODBUS_OCMF_STATE_ADDRESS, ocmf_confirmation_data);
EVLOG_info << "Cleanup done.";
}
}
types::powermeter::TransactionStartResponse
powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& treq) {
if (not m_transaction_support) {
EVLOG_info << "start transaction rejected: meter model does not support transactions";
return make_transaction_start_response(types::powermeter::TransactionRequestStatus::NOT_SUPPORTED,
"This meter model does not support transactions.");
}
try {
EVLOG_info << "Starting transaction with transaction id: " << treq.transaction_id
<< " evse id: " << treq.evse_id << " identification status: " << treq.identification_status
<< " identification type: "
<< types::powermeter::ocmfidentification_type_to_string(treq.identification_type)
<< " identification level: "
<< types::powermeter::ocmfidentification_level_to_string(
treq.identification_level.value_or(types::powermeter::OCMFIdentificationLevel::NONE))
<< " identification data: " << treq.identification_data.value_or("")
<< " tariff text: " << treq.tariff_text.value_or("none");
// 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);
std::uint16_t ocmf_state = modbus_utils::to_uint16(state_data, modbus_utils::ByteOffset{0});
EVLOG_info << "Current OCMF state: " << ocmf_state_to_string(ocmf_state) << "(" << ocmf_state << ")";
if (ocmf_state != MODBUS_OCMF_STATE_NOT_READY) {
EVLOG_warning << "Spurious transaction detected, clearing transaction states ...";
clear_transaction_states();
m_pending_closed_transaction = false;
return make_transaction_start_response(types::powermeter::TransactionRequestStatus::OK);
}
// Write transaction registers first
EVLOG_info << "Write transaction registers...";
write_transaction_registers(treq);
EVLOG_info << "Write session modality ... to charging vehicle";
std::vector<std::uint16_t> 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<std::uint16_t> 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.store(true);
m_transaction_id = treq.transaction_id;
// Capture signed meter value for transaction start (returned on stop)
m_start_signed_meter_value.emplace(read_signed_meter_value());
return make_transaction_start_response(types::powermeter::TransactionRequestStatus::OK);
} catch (const std::exception& e) {
EVLOG_error << e.what();
return make_transaction_start_response(types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR,
fmt::format("can't start transaction: {}", e.what()));
}
}
types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) {
if (not m_transaction_support) {
EVLOG_info << "stop transaction rejected: meter model does not support transactions";
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::NOT_SUPPORTED,
"This meter model does not support transactions.");
}
EVLOG_info << "Stopping transaction with transaction id: " << (transaction_id.empty() ? "empty" : transaction_id);
// if the transaction id is empty, we need to clean up the transaction states
// we do our best to clean up the transaction states
if (transaction_id.empty()) {
EVLOG_info << "Cleaning up the transaction request.";
try {
if (!m_pending_closed_transaction and m_transaction_active.load()) {
std::vector<std::uint16_t> 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_pending_closed_transaction = false;
clear_transaction_states();
} catch (const std::exception& e) {
EVLOG_error << e.what();
}
m_pending_closed_transaction = false;
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::OK);
}
try {
if (m_pending_closed_transaction) {
// the received transaction id is different from the current transaction
// id since there is a pending closed transaction, I assume a power loss
// occurred we need to check if the transaction id is equal to the
// transaction id from OCMF file
EVLOG_info << "Power loss occurred, checking if the transaction id == "
"transaction id from OCMF file";
std::string ocmf_file = read_ocmf_file();
const auto ocmf_file_transaction_id_opt = ocmf::extract_transaction_id_from_ocmf_record(ocmf_file);
if (!ocmf_file_transaction_id_opt.has_value()) {
EVLOG_error << "Failed to extract transaction id from OCMF file TT field";
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR,
"Failed to extract transaction id from OCMF file");
}
const std::string& ocmf_file_transaction_id = *ocmf_file_transaction_id_opt;
EVLOG_info << "OCMF file transaction id: " << ocmf_file_transaction_id;
if (ocmf_file_transaction_id != transaction_id) {
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR,
"Transaction id mismatch");
}
EVLOG_info << "Transaction id matches, sending successful transaction "
"stop response with OCMF file";
m_pending_closed_transaction = false;
auto signed_meter_value = types::units_signed::SignedMeterValue{ocmf_file, "", "OCMF"};
signed_meter_value.public_key.emplace(m_public_key_hex);
ocmf::confirm_file_read(*p_modbus_transport);
return types::powermeter::TransactionStopResponse{types::powermeter::TransactionRequestStatus::OK,
{}, // Empty start_signed_meter_value
signed_meter_value};
} else if (m_transaction_id == transaction_id) {
EVLOG_info << "Sending the end transaction command to the device";
// Write 'E' command to end transaction (Table 4.35, register 328737)
std::vector<std::uint16_t> 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)
if (!ocmf::wait_for_ready(*p_modbus_transport)) {
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR,
"can't stop transaction: OCMF did not reach ready state");
}
// For Eichrecht, return the OCMF file as the signed meter value report.
const std::string ocmf_data = read_ocmf_file();
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
ocmf::confirm_file_read(*p_modbus_transport);
m_pending_closed_transaction = false;
return types::powermeter::TransactionStopResponse{types::powermeter::TransactionRequestStatus::OK,
m_start_signed_meter_value, signed_meter_value};
} else {
EVLOG_error << "No open transaction or unknown transaction id: " << transaction_id;
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR,
"No open transaction or unknown transaction id");
}
} catch (const std::exception& e) {
EVLOG_error << e.what();
return make_transaction_stop_response(types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR,
fmt::format("can't stop transaction: {}", e.what()));
}
}
void powermeterImpl::read_powermeter_values() {
transport::DataVector data;
if (m_transaction_support) {
// Read a compact range starting at 300001 containing all instantaneous values we use
// up to 300052 (frequency). Energy totals are read separately from 301281+ (INT64, Wh).
data = p_modbus_transport->fetch(MODBUS_REAL_TIME_VALUES_ADDRESS, MODBUS_REAL_TIME_VALUES_COUNT);
} else {
// older models/firmwares are nasty when reading the whole block, so we split the request manually
data = p_modbus_transport->fetch(MODBUS_REAL_TIME_VALUES_ADDRESS, MODBUS_REAL_TIME_VALUES_COUNT - 2);
auto data2 = p_modbus_transport->fetch(MODBUS_REAL_TIME_VALUES_ADDRESS + MODBUS_REAL_TIME_VALUES_COUNT - 2, 2);
data.insert(data.end(), data2.begin(), data2.end());
}
types::powermeter::Powermeter powermeter{};
powermeter.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
powermeter.meter_id = m_serial_number;
// 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<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L1_N}));
voltage_V.L2 =
Factors::VOLTAGE * static_cast<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::V_L2_N}));
voltage_V.L3 =
Factors::VOLTAGE * static_cast<float>(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<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L1}));
current_A.L2 =
Factors::CURRENT * static_cast<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::A_L2}));
current_A.L3 =
Factors::CURRENT * static_cast<float>(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<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L1}));
power_W.L2 =
Factors::POWER * static_cast<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L2}));
power_W.L3 =
Factors::POWER * static_cast<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::W_L3}));
power_W.total =
Factors::POWER * static_cast<float>(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<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L1}));
VAR.L2 = Factors::REACTIVE_POWER *
static_cast<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L2}));
VAR.L3 = Factors::REACTIVE_POWER *
static_cast<float>(modbus_utils::to_int32(data, modbus_utils::ByteOffset{Offsets::VAR_L3}));
VAR.total = Factors::REACTIVE_POWER *
static_cast<float>(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<float>(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
std::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 (m_transaction_support) {
transport::DataVector dataEnergy =
p_modbus_transport->fetch(MODBUS_REAL_TIME_ENERGY_ADDRESS, MODBUS_REAL_TIME_ENERGY_COUNT);
// Energy import: register 301281 (kWh (+) TOT) - INT64, 4 words
// Spec (Table 4.3): value weight is Wh.
// Note: energy_Wh_import is a required field, not optional
powermeter.energy_Wh_import.total =
static_cast<float>(modbus_utils::to_int64(dataEnergy, modbus_utils::ByteOffset{Offsets::ENERGY_IMPORT}));
// Energy export: register 301309 (kWh (-) TOT) - INT64, 4 words
// Spec (Table 4.3): value weight is Wh.
types::units::Energy energy_Wh_export;
energy_Wh_export.total =
static_cast<float>(modbus_utils::to_int64(dataEnergy, modbus_utils::ByteOffset{Offsets::ENERGY_EXPORT}));
powermeter.energy_Wh_export = energy_Wh_export;
} else {
transport::DataVector dataEnergy = p_modbus_transport->fetch(MODBUS_REAL_TIME_ENERGY_ADDRESS_EM300_SERIES,
MODBUS_REAL_TIME_ENERGY_COUNT_EM300_SERIES);
// Energy import: register 301025 (kWh (+) TOT) - INT32, 2 words -> INT part
// Spec (Table 2.5.1): value weight is kWh*1.
// Note: energy_Wh_import is a required field, not optional
powermeter.energy_Wh_import.total =
Factors::ENERGY_INT * static_cast<float>(modbus_utils::to_int32(
dataEnergy, modbus_utils::ByteOffset{Offsets::ENERGY_IMPORT_INT}));
// Energy import: register 301027 (kWh (+) TOT) - INT32, 2 words -> DEC part
// Spec (Table 2.5.1): value weight is kWh*1000.
powermeter.energy_Wh_import.total +=
Factors::ENERGY_DEC * static_cast<float>(modbus_utils::to_int32(
dataEnergy, modbus_utils::ByteOffset{Offsets::ENERGY_IMPORT_DEC}));
// Energy export: register 301033 (kWh (-) TOT) - INT32, 2 words -> INT part
// Spec (Table 2.5.1): value weight is kWh*1.
types::units::Energy energy_Wh_export;
energy_Wh_export.total =
Factors::ENERGY_INT * static_cast<float>(modbus_utils::to_int32(
dataEnergy, modbus_utils::ByteOffset{Offsets::ENERGY_EXPORT_INT}));
// Energy export: register 301035 (kWh (-) TOT) - INT32, 2 words -> DEC part
// Spec (Table 2.5.1): value weight is kWh*1000.
energy_Wh_export.total +=
Factors::ENERGY_DEC * static_cast<float>(modbus_utils::to_int32(
dataEnergy, modbus_utils::ByteOffset{Offsets::ENERGY_EXPORT_DEC}));
powermeter.energy_Wh_export = energy_Wh_export;
}
// Disable for now the temperature reading, since I can't read it in the above
// block read 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<float>(modbus_utils::to_int16(temperature_data,
// modbus_utils::ByteOffset{0}));
// temperature.location = "Internal";
// std::vector<types::temperature::Temperature> temperatures;
// temperatures.push_back(temperature);
// powermeter.temperatures = temperatures;
if (m_transaction_support) {
powermeter.signed_meter_value = read_signed_meter_value();
}
publish_powermeter(powermeter);
}
void powermeterImpl::dump_device_state() {
try {
// 1. OCMF state
transport::DataVector state_data = p_modbus_transport->fetch(MODBUS_OCMF_STATE_ADDRESS, 1);
std::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);
std::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);
std::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);
std::uint16_t raw_cmd = modbus_utils::to_uint16(cmd_data, modbus_utils::ByteOffset{0});
// ASCII code is stored in the low byte
char cmd_char = static_cast<char>(raw_cmd & 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);
std::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();
std::int64_t seconds_since_epoch = std::chrono::duration_cast<std::chrono::seconds>(time_since_epoch).count();
// 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]
std::uint64_t timestamp = static_cast<std::uint64_t>(seconds_since_epoch);
std::vector<std::uint16_t> data;
data.push_back(static_cast<std::uint16_t>(timestamp & 0xFFFF)); // LSW: bits 0-15
data.push_back(static_cast<std::uint16_t>((timestamp >> 16) & 0xFFFF)); // bits 16-31
data.push_back(static_cast<std::uint16_t>((timestamp >> 32) & 0xFFFF)); // bits 32-47
data.push_back(static_cast<std::uint16_t>((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);
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.
std::int16_t offset_int16 = static_cast<std::int16_t>(offset_minutes);
std::vector<std::uint16_t> data;
data.push_back(static_cast<std::uint16_t>(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 (!stop_requested_.load()) {
{
std::unique_lock<std::mutex> lock(stop_mutex_);
stop_cv_.wait_until(lock, next_sync_time, [this] { return stop_requested_.load(); });
}
if (stop_requested_.load()) {
break;
}
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;
}
}
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);
std::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
const auto error_messages = device_state_utils::decode_device_state_errors(device_state);
// 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 = error_factory->create_error("powermeter/VendorError", "DeviceStateError", error_description);
raise_error(error);
} else {
EVLOG_debug << "Device state OK (0x" << fmt::format("{:04X}", device_state) << ")";
}
}
types::units_signed::SignedMeterValue powermeterImpl::read_signed_meter_value() {
transport::DataVector signed_data = p_modbus_transport->fetch(MODBUS_SIGNED_MAP_ADDRESS, m_signed_map_word_count);
// Spec (Table 4.4): signed data spans 61 words (302049..302109) and signature starts at 302110.
static constexpr std::size_t SIGNED_MAP_SIGNED_DATA_WORDS = 61;
static constexpr std::size_t SIGNED_MAP_SIGNED_DATA_BYTES = SIGNED_MAP_SIGNED_DATA_WORDS * 2;
const std::size_t signature_bytes =
(m_signed_map_word_count > SIGNED_MAP_SIGNED_DATA_WORDS)
? (static_cast<std::size_t>(m_signed_map_word_count) - SIGNED_MAP_SIGNED_DATA_WORDS) * 2
: 0;
std::string signed_data_string = modbus_utils::to_hex_string(
signed_data, modbus_utils::ByteOffset{0}, modbus_utils::ByteLength{SIGNED_MAP_SIGNED_DATA_BYTES});
std::string signed_data_signature_string = modbus_utils::to_hex_string(
signed_data, modbus_utils::ByteOffset{SIGNED_MAP_SIGNED_DATA_BYTES}, modbus_utils::ByteLength{signature_bytes});
types::units_signed::SignedMeterValue smv;
smv.signed_meter_data =
R"({"signedData":")" + signed_data_string + R"(","signature":")" + signed_data_signature_string + R"("})";
smv.signing_method = m_signature_method_string;
smv.encoding_method = "plain";
smv.public_key = m_public_key_hex;
smv.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now());
return smv;
}
} // namespace module::main