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
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,38 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_carlo_gavazzi_em580_helper_tests)
set(MODULE_DIR "${PROJECT_SOURCE_DIR}/modules/HardwareDrivers/PowerMeters/CarloGavazzi_EM580")
add_executable(${TEST_TARGET_NAME}
test_em580_helper.cpp
test_em580_powermeter_impl.cpp
${MODULE_DIR}/main/powermeterImpl.cpp
${MODULE_DIR}/main/transport.cpp
)
add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME})
set(INCLUDE_DIR
"main"
"tests"
"${MODULE_DIR}/main"
"${MODULE_DIR}/tests"
)
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
target_include_directories(${TEST_TARGET_NAME} PRIVATE
tests
${INCLUDE_DIR}
${GENERATED_INCLUDE_DIR}
${CMAKE_BINARY_DIR}/generated/modules/CarloGavazzi_EM580
)
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
GTest::gtest_main
everest::framework
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
EVEREST_DIR="$(cd -- "${SCRIPT_DIR}/../../../../.." && pwd)"
DIST_DIR="${EVEREST_DIR}/build/dist"
DIST_ETC_DIR="${DIST_DIR}/etc/everest"
MANAGER_BIN="${DIST_DIR}/bin/manager"
BUPOWERMETER_BIN="${DIST_DIR}/libexec/everest/modules/BUPowermeter/BUPowermeter"
if [[ ! -x "${MANAGER_BIN}" ]]; then
echo "ERROR: manager binary not found/executable at: ${MANAGER_BIN}" >&2
echo "Did you build EVerest and generate the dist/ folder?" >&2
exit 1
fi
if [[ ! -x "${BUPOWERMETER_BIN}" ]]; then
echo "ERROR: BUPowermeter binary not found/executable at: ${BUPOWERMETER_BIN}" >&2
echo "Did you build EVerest and generate the dist/ folder?" >&2
exit 1
fi
if [[ ! -d "${DIST_ETC_DIR}" ]]; then
echo "ERROR: dist etc dir not found at: ${DIST_ETC_DIR}" >&2
exit 1
fi
other_pids=()
cleanup() {
for pid in "${other_pids[@]:-}"; do
kill "${pid}" 2>/dev/null || true
done
}
trap cleanup EXIT INT TERM
read -r -p "Start CGEM580 bringup with 1, 6, 7, 12 or 13 devices? [1/6/7/12/13]: " device_count
case "${device_count}" in
1)
config_file="config-bringup-CGEM580.yaml"
;;
6)
config_file="config-bringup-CGEM580-6x.yaml"
;;
7)
config_file="config-bringup-CGEM580-7x.yaml"
;;
12)
config_file="config-bringup-CGEM580-12x.yaml"
;;
13)
config_file="config-bringup-CGEM580-13x.yaml"
;;
*)
echo "Invalid choice: '${device_count}'. Please enter 1, 6, 7, 12 or 13."
exit 2
;;
esac
if [[ ! -f "${DIST_ETC_DIR}/${config_file}" ]]; then
echo "ERROR: config file not found at: ${DIST_ETC_DIR}/${config_file}" >&2
exit 1
fi
# Start manager first, then powermeters. When the manager window closes, all others will be closed as well.
# Important: run with CWD in ${DIST_ETC_DIR} (matches previous scripts and avoids any CWD-sensitive behavior).
xterm -bg black -fg white -geometry 400x150 -e bash -lc "cd \"${DIST_ETC_DIR}\" && \"${MANAGER_BIN}\" --prefix \"${DIST_DIR}\" --conf \"${config_file}\"" &
manager_pid=$!
for i in $(seq 1 "${device_count}"); do
xterm -bg black -fg white -geometry 200x55 -e bash -lc "cd \"${DIST_ETC_DIR}\" && sleep 1 && \"${BUPOWERMETER_BIN}\" --module cli_${i}" &
other_pids+=($!)
done
wait "${manager_pid}"

View File

@@ -0,0 +1,249 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <gtest/gtest.h>
#include "helper.hpp"
#include <chrono>
#include <deque>
#include <map>
#include <tuple>
namespace {
constexpr std::size_t kFetchCallAlignmentBytes = 8;
constexpr std::size_t kWriteCallAlignmentBytes = 32;
struct alignas(kFetchCallAlignmentBytes) FetchCall {
std::int32_t address;
std::uint16_t register_count;
};
struct alignas(kWriteCallAlignmentBytes) WriteCall {
std::int32_t address;
std::vector<std::uint16_t> data;
};
class FakeModbusTransport : public transport::AbstractModbusTransport {
public:
// Script one fetch response for (address, register_count).
void push_fetch_response(std::int32_t address, std::uint16_t register_count, transport::DataVector response) {
scripted_fetch_[Key{address, register_count}].push_back(std::move(response));
}
const std::vector<FetchCall>& fetch_calls() const {
return fetch_calls_;
}
const std::vector<WriteCall>& write_calls() const {
return write_calls_;
}
transport::DataVector fetch(std::int32_t address, std::uint16_t register_count) override {
fetch_calls_.push_back(FetchCall{address, register_count});
const Key key{address, register_count};
auto iter = scripted_fetch_.find(key);
if (iter == scripted_fetch_.end() || iter->second.empty()) {
throw std::runtime_error("FakeModbusTransport: no scripted fetch response for address/count");
}
transport::DataVector out = std::move(iter->second.front());
iter->second.pop_front();
return out;
}
void write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) override {
write_calls_.push_back(WriteCall{address, data});
}
private:
using Key = std::tuple<std::int32_t, std::uint16_t>;
std::map<Key, std::deque<transport::DataVector>> scripted_fetch_;
std::vector<FetchCall> fetch_calls_;
std::vector<WriteCall> write_calls_;
};
transport::DataVector u16_be(std::uint16_t value) {
constexpr std::uint32_t kByteBits = 8U;
constexpr std::uint32_t kByteMask = 0xFFU;
const auto high = static_cast<std::uint8_t>((static_cast<std::uint32_t>(value) >> kByteBits) & kByteMask);
const auto low = static_cast<std::uint8_t>(static_cast<std::uint32_t>(value) & kByteMask);
return transport::DataVector{high, low};
}
} // namespace
TEST(EM580Helper, ExtractTransactionIdFromTTHappyPath) {
const std::string uuid = "12345678-1234-5678-1234-567812345678";
const std::string ocmf = R"(OCMF|{"TT":"price-2.30-EUR/kWh<=>)" + uuid + R"(","RD":[]}|
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
ASSERT_TRUE(tid.has_value());
EXPECT_EQ(*tid, uuid);
}
TEST(EM580Helper, ExtractTransactionIdFromTTMissingMarker) {
const std::string ocmf = R"(OCMF|{"TT":"no-marker-here","RD":[]}|
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
EXPECT_FALSE(tid.has_value());
}
TEST(EM580Helper, MaxPayloadBytesForWords) {
EXPECT_EQ(modbus_utils::max_payload_bytes_for_words(0), 0);
EXPECT_EQ(modbus_utils::max_payload_bytes_for_words(1),
1); // 2 bytes total, reserve NUL => 1
EXPECT_EQ(modbus_utils::max_payload_bytes_for_words(126),
251); // 252 bytes total, reserve NUL => 251
}
TEST(EM580Helper, StringToModbusCharArrayZeroTerminatedAndUsedOnly) {
const auto words = modbus_utils::string_to_modbus_char_array("AB", 126);
ASSERT_EQ(words.size(), 2U); // 'A''B''\0' => 3 bytes => 2 words
EXPECT_EQ(words[0], 0x4142);
EXPECT_EQ(words[1], 0x0000);
}
TEST(EM580Helper, StringToModbusCharArrayTruncatesToFitWithNul) {
// 1 word => 2 bytes total => only 1 byte payload + NUL
const auto words = modbus_utils::string_to_modbus_char_array("HELLO", 1);
ASSERT_EQ(words.size(), 1U);
EXPECT_EQ(words[0], 0x4800); // 'H' + '\0'
}
TEST(EM580Helper, OcmfConfirmFileReadWritesNotReadyToStateRegister) {
FakeModbusTransport transport;
ocmf::confirm_file_read(transport);
ASSERT_EQ(transport.write_calls().size(), 1U);
EXPECT_EQ(transport.write_calls()[0].address, em580::registers::MODBUS_OCMF_STATE_ADDRESS);
ASSERT_EQ(transport.write_calls()[0].data.size(), 1U);
EXPECT_EQ(transport.write_calls()[0].data[0], em580::registers::MODBUS_OCMF_STATE_NOT_READY);
}
TEST(EM580Helper, OcmfWaitForReadyReachesReady) {
FakeModbusTransport transport;
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_RUNNING));
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_READY));
const bool success = ocmf::wait_for_ready(transport, std::chrono::milliseconds{0}, 10);
EXPECT_TRUE(success);
EXPECT_EQ(transport.fetch_calls().size(), 3U);
}
TEST(EM580Helper, OcmfWaitForReadyFailsOnCorrupted) {
FakeModbusTransport transport;
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_CORRUPTED));
const bool success = ocmf::wait_for_ready(transport, std::chrono::milliseconds{0}, 10);
EXPECT_FALSE(success);
EXPECT_EQ(transport.fetch_calls().size(), 1U);
}
TEST(EM580Helper, IsUuid36ValidAndInvalid) {
EXPECT_TRUE(ocmf::is_uuid36("12345678-1234-5678-1234-567812345678"));
EXPECT_TRUE(ocmf::is_uuid36("ABCDEFAB-CDEF-ABCD-EFAB-CDEFABCDEFAB"));
EXPECT_FALSE(ocmf::is_uuid36("12345678-1234-5678-1234-56781234567")); // too short
EXPECT_FALSE(ocmf::is_uuid36("123456781234-5678-1234-567812345678")); // missing '-'
EXPECT_FALSE(ocmf::is_uuid36("12345678-1234-5678-1234-56781234567Z")); // non-hex
}
TEST(EM580Helper, ExtractTransactionIdFromRecordMissingTTField) {
const std::string ocmf = R"(OCMF|{"FV":"1.2","RD":[]}|
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
EXPECT_FALSE(tid.has_value());
}
TEST(EM580Helper, ExtractTransactionIdFromRecordInvalidUuidAfterMarker) {
const std::string ocmf = R"(OCMF|{"TT":"foo<=>not-a-uuid","RD":[]}|
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
const auto tid = ocmf::extract_transaction_id_from_ocmf_record(ocmf);
EXPECT_FALSE(tid.has_value());
}
TEST(EM580Helper, DecodeDeviceStateErrorsReturnsMatchingMessages) {
// Bits 0 and 13 correspond to V1N over-range and Measure module internal
// fault.
const auto state = static_cast<std::uint16_t>((1U << 0U) | (1U << 13U));
const auto errors = device_state_utils::decode_device_state_errors(state);
ASSERT_EQ(errors.size(), 2U);
EXPECT_EQ(errors[0], "V1N over maximum range");
EXPECT_EQ(errors[1], "Measure module internal fault");
}
TEST(EM580Helper, ModbusToUint16AndToUint32ByteOrder) {
const transport::DataVector data = {
0x12, 0x34, // u16 @0 => 0x1234
0xAA, 0xBB, // u16 @2 => 0xAABB
0xDE, 0xAD, 0xBE, 0xEF // u32 @4 => 0xDEADBEEF
};
EXPECT_EQ(modbus_utils::to_uint16(data, modbus_utils::ByteOffset{0}), 0x1234);
EXPECT_EQ(modbus_utils::to_uint16(data, modbus_utils::ByteOffset{2}), 0xAABB);
EXPECT_EQ(modbus_utils::to_uint32(data, modbus_utils::ByteOffset{4}), 0xDEADBEEF);
}
TEST(EM580Helper, StringToModbusCharArrayEmptyStringIsJustTerminator) {
const auto words = modbus_utils::string_to_modbus_char_array("", 126);
ASSERT_EQ(words.size(), 1U);
EXPECT_EQ(words[0], 0x0000);
}
TEST(EM580Helper, StringToModbusCharArrayPacksOddLengthAndAddsNul) {
// "ABC\0" => 4 bytes => 2 words: 0x4142 0x4300
const auto words = modbus_utils::string_to_modbus_char_array("ABC", 126);
ASSERT_EQ(words.size(), 2U);
EXPECT_EQ(words[0], 0x4142);
EXPECT_EQ(words[1], 0x4300);
}
TEST(EM580Helper, StringToModbusCharArrayTruncatesAndStillTerminates) {
// max_words=2 => 4 bytes total => 3 payload bytes + NUL
const auto words = modbus_utils::string_to_modbus_char_array("HELLO", 2);
ASSERT_EQ(words.size(), 2U);
EXPECT_EQ(words[0], 0x4845); // 'H''E'
EXPECT_EQ(words[1], 0x4C00); // 'L''\0' (truncated)
}
TEST(EM580Helper, OcmfWaitForReadyTimesOutAfterMaxRetries) {
FakeModbusTransport transport;
static constexpr int kNonReadyReads = 11;
// kNonReadyReads non-ready reads => retries becomes kNonReadyReads and
// (retries > max_retries(10)) => false
#pragma unroll
for (int i = 0; i < kNonReadyReads; ++i) {
transport.push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
}
const bool success = ocmf::wait_for_ready(transport, std::chrono::milliseconds{0}, 10);
EXPECT_FALSE(success);
EXPECT_EQ(transport.fetch_calls().size(), static_cast<std::size_t>(kNonReadyReads));
}
TEST(EM580Helper, DecodeDeviceStateErrorsEmptyIfNoBitsSet) {
const auto errors = device_state_utils::decode_device_state_errors(0U);
EXPECT_TRUE(errors.empty());
}
TEST(EM580Helper, ModbusToHexStringUppercaseNoSeparators) {
const transport::DataVector data = {0x00, 0x2a, 0xAB, 0xCD, 0xEF};
EXPECT_EQ(modbus_utils::to_hex_string(data, modbus_utils::ByteOffset{0}, modbus_utils::ByteLength{data.size()}),
"002AABCDEF");
}
TEST(EM580Helper, ModbusToInt16Sign) {
const transport::DataVector data = {0xFF, 0xFE}; // 0xFFFE => -2
EXPECT_EQ(modbus_utils::to_int16(data, modbus_utils::ByteOffset{0}), -2);
}

View File

@@ -0,0 +1,243 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <gtest/gtest.h>
#include "helper.hpp"
#include "powermeterImpl.hpp"
#include <deque>
#include <map>
#include <string_view>
#include <tuple>
namespace {
constexpr std::size_t kFetchCallAlignmentBytes = 8;
constexpr std::size_t kWriteCallAlignmentBytes = 32;
constexpr std::size_t kBytesPerRegister = 2;
struct alignas(kFetchCallAlignmentBytes) FetchCall {
std::int32_t address;
std::uint16_t register_count;
};
struct alignas(kWriteCallAlignmentBytes) WriteCall {
std::int32_t address;
std::vector<std::uint16_t> data;
};
class FakeModbusTransport : public transport::AbstractModbusTransport {
public:
void push_fetch_response(std::int32_t address, std::uint16_t register_count, transport::DataVector response) {
scripted_fetch_[Key{address, register_count}].push_back(std::move(response));
}
const std::vector<FetchCall>& fetch_calls() const {
return fetch_calls_;
}
const std::vector<WriteCall>& write_calls() const {
return write_calls_;
}
transport::DataVector fetch(std::int32_t address, std::uint16_t register_count) override {
fetch_calls_.push_back(FetchCall{address, register_count});
const Key key{address, register_count};
auto iter = scripted_fetch_.find(key);
if (iter == scripted_fetch_.end() || iter->second.empty()) {
throw std::runtime_error("FakeModbusTransport: no scripted fetch response for address/count");
}
transport::DataVector out = std::move(iter->second.front());
iter->second.pop_front();
return out;
}
void write_multiple_registers(std::int32_t address, const std::vector<std::uint16_t>& data) override {
write_calls_.push_back(WriteCall{address, data});
}
private:
using Key = std::tuple<std::int32_t, std::uint16_t>;
std::map<Key, std::deque<transport::DataVector>> scripted_fetch_;
std::vector<FetchCall> fetch_calls_;
std::vector<WriteCall> write_calls_;
};
transport::DataVector u16_be(std::uint16_t value) {
constexpr std::uint32_t kByteBits = 8U;
constexpr std::uint32_t kByteMask = 0xFFU;
const auto high = static_cast<std::uint8_t>((static_cast<std::uint32_t>(value) >> kByteBits) & kByteMask);
const auto low = static_cast<std::uint8_t>(static_cast<std::uint32_t>(value) & kByteMask);
return transport::DataVector{high, low};
}
transport::DataVector bytes(std::string_view str) {
return transport::DataVector{str.begin(), str.end()};
}
transport::DataVector zero_bytes_for_words(std::uint16_t words) {
return transport::DataVector(words * kBytesPerRegister, 0U);
}
module::main::Conf make_test_conf() {
constexpr int kDefaultIntervalMs = 1000;
module::main::Conf conf{};
conf.powermeter_device_id = 1;
conf.communication_retry_count = 0;
conf.communication_retry_delay_ms = 0;
conf.initial_connection_retry_count = 0;
conf.initial_connection_retry_delay_ms = 0;
conf.timezone_offset_minutes = 0;
conf.live_measurement_interval_ms = kDefaultIntervalMs;
conf.device_state_read_interval_ms = kDefaultIntervalMs;
conf.communication_error_pause_delay_s = 0;
return conf;
}
} // namespace
TEST(EM580PowermeterImpl, StartTransactionHappyPathCountsWrites) {
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
auto conf = make_test_conf();
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
auto transport = std::make_unique<FakeModbusTransport>();
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
transport->push_fetch_response(em580::registers::MODBUS_SIGNED_MAP_ADDRESS,
em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256,
zero_bytes_for_words(em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256));
auto* transport_ptr = transport.get();
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
module::main::powermeterImpl::TestAccess::set_signed_map_word_count(
impl, em580::registers::MODBUS_SIGNED_MAP_WORD_COUNT_256);
types::powermeter::TransactionReq req{};
req.evse_id = "DE*TEST*EVSE01";
req.transaction_id = "12345678-1234-5678-1234-567812345678";
req.identification_status = types::powermeter::OCMFUserIdentificationStatus::ASSIGNED;
req.identification_flags = {};
req.identification_type = types::powermeter::OCMFIdentificationType::ISO14443;
req.identification_level = types::powermeter::OCMFIdentificationLevel::NONE;
req.identification_data.emplace("ABC");
req.tariff_text.emplace("TARIFF");
const auto resp = module::main::powermeterImpl::TestAccess::start_transaction(impl, req);
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::OK);
// Happy-path expectation: 8 writes from write_transaction_registers + 2
// additional writes (session modality + 'B')
EXPECT_EQ(transport_ptr->write_calls().size(), 10U);
}
TEST(EM580PowermeterImpl, StopTransactionPendingClosedTransactionMismatchReturnsError) {
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
auto conf = make_test_conf();
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
auto transport = std::make_unique<FakeModbusTransport>();
// read_ocmf_file(): size then file bytes
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_SIZE_ADDRESS, 1, u16_be(1));
const std::string other_uuid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
const std::string ocmf_file = R"(OCMF|{"TT":"x<=>)" + other_uuid + R"("}|
{"SA":"ECDSA-brainpoolP384r1-SHA256","SD":"00"})";
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_FILE_ADDRESS, 1, bytes(ocmf_file));
auto* transport_ptr = transport.get();
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
module::main::powermeterImpl::TestAccess::set_pending_closed_transaction(impl, true);
std::string requested_uuid = "12345678-1234-5678-1234-567812345678";
const auto resp = module::main::powermeterImpl::TestAccess::stop_transaction(impl, requested_uuid);
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR);
ASSERT_TRUE(resp.error.has_value());
EXPECT_EQ(*resp.error, "Transaction id mismatch");
EXPECT_EQ(transport_ptr->write_calls().size(), 0U);
}
TEST(EM580PowermeterImpl, StopTransactionEmptyIdWithoutPendingClosedTransactionCleansUpAndReturnsOk) {
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
auto conf = make_test_conf();
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
auto transport = std::make_unique<FakeModbusTransport>();
// clear_transaction_states() fetches OCMF state once; use NOT_READY to avoid
// extra cleanup behavior.
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_NOT_READY));
auto* transport_ptr = transport.get();
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
module::main::powermeterImpl::TestAccess::set_transaction_id(impl, "12345678-1234-5678-1234-567812345678");
module::main::powermeterImpl::TestAccess::set_pending_closed_transaction(impl, false);
std::string empty_id;
const auto resp = module::main::powermeterImpl::TestAccess::stop_transaction(impl, empty_id);
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::OK);
// Expect one write ('E' end command) when no pending closed transaction
// exists.
EXPECT_EQ(transport_ptr->write_calls().size(), 1U);
}
TEST(EM580PowermeterImpl, StartTransactionSpuriousReadyStateDoesCleanupAndNoTransactionWrites) {
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
auto conf = make_test_conf();
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
auto transport = std::make_unique<FakeModbusTransport>();
// handle_start_transaction() first checks OCMF state.
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_READY));
// clear_transaction_states(): reads state again, sees READY, reads file
// (size+file) and confirms NOT_READY.
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_ADDRESS, 1,
u16_be(em580::registers::MODBUS_OCMF_STATE_READY));
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_SIZE_ADDRESS, 1, u16_be(1));
transport->push_fetch_response(em580::registers::MODBUS_OCMF_STATE_FILE_ADDRESS, 1,
bytes("OCMF|{\"TT\":\"x<=>12345678-1234-5678-1234-567812345678\"}|{}"));
auto* transport_ptr = transport.get();
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
types::powermeter::TransactionReq req{};
req.evse_id = "DE*TEST*EVSE01";
req.transaction_id = "12345678-1234-5678-1234-567812345678";
req.identification_status = types::powermeter::OCMFUserIdentificationStatus::ASSIGNED;
req.identification_flags = {};
req.identification_type = types::powermeter::OCMFIdentificationType::ISO14443;
req.identification_level = types::powermeter::OCMFIdentificationLevel::NONE;
req.identification_data.emplace("ABC");
req.tariff_text.emplace("TARIFF");
const auto resp = module::main::powermeterImpl::TestAccess::start_transaction(impl, req);
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::OK);
// Only the cleanup confirm write should happen here (no transaction register
// writes).
EXPECT_EQ(transport_ptr->write_calls().size(), 1U);
EXPECT_EQ(transport_ptr->write_calls()[0].address, em580::registers::MODBUS_OCMF_STATE_ADDRESS);
}
TEST(EM580PowermeterImpl, StopTransactionUnknownIdReturnsErrorAndNoWrites) {
static Everest::PtrContainer<module::CarloGavazzi_EM580> dummy_mod;
auto conf = make_test_conf();
module::main::powermeterImpl impl(nullptr, dummy_mod, conf);
auto transport = std::make_unique<FakeModbusTransport>();
auto* transport_ptr = transport.get();
module::main::powermeterImpl::TestAccess::set_modbus_transport(impl, std::move(transport));
module::main::powermeterImpl::TestAccess::set_pending_closed_transaction(impl, false);
module::main::powermeterImpl::TestAccess::set_transaction_id(impl, "11111111-1111-1111-1111-111111111111");
std::string requested_uuid = "22222222-2222-2222-2222-222222222222";
const auto resp = module::main::powermeterImpl::TestAccess::stop_transaction(impl, requested_uuid);
EXPECT_EQ(resp.status, types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR);
ASSERT_TRUE(resp.error.has_value());
EXPECT_EQ(*resp.error, "No open transaction or unknown transaction id");
EXPECT_TRUE(transport_ptr->write_calls().empty());
}