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:
@@ -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})
|
||||
|
||||
|
||||
@@ -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}"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user