From fdafb9b97d74c576007173e6e737b3c2c187974d Mon Sep 17 00:00:00 2001 From: Kacper Dalach Date: Tue, 16 Dec 2025 12:17:16 +0100 Subject: [PATCH 1/4] libiso15118: d2: Add ServiceDiscovery req/res Signed-off-by: Kacper Dalach --- .../iso15118/message/d2/msg_data_types.hpp | 8 ++ .../iso15118/message/d2/service_discovery.hpp | 66 +++++++++++++ .../include/iso15118/message/d2/type.hpp | 4 + .../iso15118/src/iso15118/CMakeLists.txt | 1 + .../iso15118/message/d2/service_discovery.cpp | 94 ++++++++++++++++++ .../src/iso15118/message/d2/variant.cpp | 2 + .../iso15118/test/exi/cb/iso2/CMakeLists.txt | 1 + .../test/exi/cb/iso2/service_discovery.cpp | 95 +++++++++++++++++++ 8 files changed, 271 insertions(+) create mode 100644 lib/everest/iso15118/include/iso15118/message/d2/service_discovery.hpp create mode 100644 lib/everest/iso15118/src/iso15118/message/d2/service_discovery.cpp create mode 100644 lib/everest/iso15118/test/exi/cb/iso2/service_discovery.cpp diff --git a/lib/everest/iso15118/include/iso15118/message/d2/msg_data_types.hpp b/lib/everest/iso15118/include/iso15118/message/d2/msg_data_types.hpp index 2b16395772..0cbabb2301 100644 --- a/lib/everest/iso15118/include/iso15118/message/d2/msg_data_types.hpp +++ b/lib/everest/iso15118/include/iso15118/message/d2/msg_data_types.hpp @@ -19,6 +19,9 @@ namespace data_types { constexpr auto SESSION_ID_LENGTH = 8; using SESSION_ID = std::array; // hexBinary, max length 8 +using ServiceID = uint16_t; +using ParameterSetID = int16_t; + constexpr auto GEN_CHALLENGE_LENGTH = 16; using GenChallenge = std::array; // base64 binary using PercentValue = uint8_t; // [0 - 100] @@ -65,6 +68,11 @@ enum class FaultCode { UnknownError, }; +enum class PaymentOption { + Contract, + ExternalPayment +}; + enum class EvseProcessing { Finished, Ongoing, diff --git a/lib/everest/iso15118/include/iso15118/message/d2/service_discovery.hpp b/lib/everest/iso15118/include/iso15118/message/d2/service_discovery.hpp new file mode 100644 index 0000000000..c18a2e410c --- /dev/null +++ b/lib/everest/iso15118/include/iso15118/message/d2/service_discovery.hpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace iso15118::d2::msg { + +namespace data_types { +using ServiceName = std::string; // MaxLength: 32 +using ServiceScope = std::string; // MaxLength: 64 +using ServiceScope = std::string; // MaxLength: 32 +using PaymentOptionList = std::vector; // [1 - 2] + +enum class ServiceCategory { + EvCharging, + Internet, + ContractCertificate, + OtherCustom +}; + +enum class EnergyTransferMode { + AC_single_phase_core, + AC_three_phase_core, + DC_core, + DC_extended, + DC_combo_core, + DC_unique +}; +using SupportedEnergyTransferMode = std::vector; // MaxLength: 6 + +struct Service { + ServiceID service_id; + std::optional service_name{std::nullopt}; + ServiceCategory service_category; + std::optional service_scope{std::nullopt}; + bool FreeService; +}; +using ServiceList = std::vector; // [1 - 8] + +struct ChargeService : Service { + SupportedEnergyTransferMode supported_energy_transfer_mode; +}; +} // namespace data_types + +struct ServiceDiscoveryRequest { + Header header; + std::optional service_scope{std::nullopt}; + std::optional service_category{std::nullopt}; +}; + +struct ServiceDiscoveryResponse { + Header header; + data_types::ResponseCode response_code; + data_types::PaymentOptionList payment_option_list; + data_types::ChargeService charge_service; + std::optional service_list{std::nullopt}; +}; + +} // namespace iso15118::d2::msg diff --git a/lib/everest/iso15118/include/iso15118/message/d2/type.hpp b/lib/everest/iso15118/include/iso15118/message/d2/type.hpp index d87872d6f9..d1895b687f 100644 --- a/lib/everest/iso15118/include/iso15118/message/d2/type.hpp +++ b/lib/everest/iso15118/include/iso15118/message/d2/type.hpp @@ -12,6 +12,8 @@ enum class Type { SupportedAppProtocolRes, SessionSetupReq, SessionSetupRes, + ServiceDiscoveryReq, + ServiceDiscoveryRes, AuthorizationReq, AuthorizationRes, CableCheckReq, @@ -49,6 +51,8 @@ CREATE_TYPE_TRAIT(SupportedAppProtocolRequest, SupportedAppProtocolReq); CREATE_TYPE_TRAIT(SupportedAppProtocolResponse, SupportedAppProtocolRes); CREATE_TYPE_TRAIT(SessionSetupRequest, SessionSetupReq); CREATE_TYPE_TRAIT(SessionSetupResponse, SessionSetupRes); +CREATE_TYPE_TRAIT(ServiceDiscoveryRequest, ServiceDiscoveryReq); +CREATE_TYPE_TRAIT(ServiceDiscoveryResponse, ServiceDiscoveryRes); CREATE_TYPE_TRAIT(AuthorizationRequest, AuthorizationReq); CREATE_TYPE_TRAIT(AuthorizationResponse, AuthorizationRes); CREATE_TYPE_TRAIT(DC_CableCheckRequest, CableCheckReq); diff --git a/lib/everest/iso15118/src/iso15118/CMakeLists.txt b/lib/everest/iso15118/src/iso15118/CMakeLists.txt index e7e35dadc9..710ba9cf81 100644 --- a/lib/everest/iso15118/src/iso15118/CMakeLists.txt +++ b/lib/everest/iso15118/src/iso15118/CMakeLists.txt @@ -75,6 +75,7 @@ target_sources(iso15118 message/d2/dc_welding_detection.cpp message/d2/variant.cpp message/d2/msg_data_types.cpp + message/d2/service_discovery.cpp message/d2/session_setup.cpp tbd_controller.cpp diff --git a/lib/everest/iso15118/src/iso15118/message/d2/service_discovery.cpp b/lib/everest/iso15118/src/iso15118/message/d2/service_discovery.cpp new file mode 100644 index 0000000000..d2ff5da2c9 --- /dev/null +++ b/lib/everest/iso15118/src/iso15118/message/d2/service_discovery.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +#include + +#include +#include + +#include + +namespace iso15118::d2::msg { + +template <> void convert(const data_types::Service& in, struct iso2_ServiceType& out) { + out.ServiceID = in.service_id; + CPP2CB_STRING_IF_USED(in.service_name, out.ServiceName); + cb_convert_enum(in.service_category, out.ServiceCategory); + CPP2CB_STRING_IF_USED(in.service_scope, out.ServiceScope); + out.FreeService = in.FreeService; +} + +template <> void convert(const data_types::ChargeService& in, struct iso2_ChargeServiceType& out) { + out.ServiceID = in.service_id; + CPP2CB_STRING_IF_USED(in.service_name, out.ServiceName); + cb_convert_enum(in.service_category, out.ServiceCategory); + CPP2CB_STRING_IF_USED(in.service_scope, out.ServiceScope); + out.FreeService = in.FreeService; + + int modeIndex = 0; + for (auto const& mode : in.supported_energy_transfer_mode) { + // TODO(kd) guard for accessing iso2_ struct array out of bounds? + cb_convert_enum(mode, out.SupportedEnergyTransferMode.EnergyTransferMode.array[modeIndex++]); + } + out.SupportedEnergyTransferMode.EnergyTransferMode.arrayLen = in.supported_energy_transfer_mode.size(); +} + +template <> void convert(const struct iso2_ServiceDiscoveryReqType& in, ServiceDiscoveryRequest& out) { + if (in.ServiceCategory_isUsed) { + d2::msg::data_types::ServiceCategory category{}; + cb_convert_enum(in.ServiceCategory, category); + out.service_category = category; + } + CB2CPP_STRING_IF_USED(in.ServiceScope, out.service_scope); +} + +template <> +void insert_type(VariantAccess& va, const struct iso2_ServiceDiscoveryReqType& in, + const struct iso2_MessageHeaderType& header) { + va.insert_type(in, header); +} + +template <> void convert(const ServiceDiscoveryResponse& in, struct iso2_ServiceDiscoveryResType& out) { + init_iso2_ServiceDiscoveryResType(&out); + + cb_convert_enum(in.response_code, out.ResponseCode); + + int optionIndex = 0; + for (auto const& option : in.payment_option_list) { + cb_convert_enum(option, out.PaymentOptionList.PaymentOption.array[optionIndex++]); + } + out.PaymentOptionList.PaymentOption.arrayLen = in.payment_option_list.size(); + + convert(in.charge_service, out.ChargeService); + + int serviceIndex = 0; + if (in.service_list) { + for (auto const& service : in.service_list.value()) { + convert(service, out.ServiceList.Service.array[serviceIndex++]); + } + out.ServiceList.Service.arrayLen = in.service_list->size(); + CB_SET_USED(out.ServiceList); + } +} + +template <> int serialize_to_exi(const ServiceDiscoveryResponse& in, exi_bitstream_t& out) { + + iso2_exiDocument doc; + init_iso2_exiDocument(&doc); + init_iso2_BodyType(&doc.V2G_Message.Body); + + convert(in.header, doc.V2G_Message.Header); + + CB_SET_USED(doc.V2G_Message.Body.ServiceDiscoveryRes); + convert(in, doc.V2G_Message.Body.ServiceDiscoveryRes); + + return encode_iso2_exiDocument(&out, &doc); +} + +template <> size_t serialize(const ServiceDiscoveryResponse& in, const io::StreamOutputView& out) { + return serialize_helper(in, out); +} + +} // namespace iso15118::d2::msg diff --git a/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp b/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp index e82d8a5b14..59da4ba68f 100644 --- a/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp +++ b/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp @@ -45,6 +45,8 @@ void handle_v2g(VariantAccess& va) { if (doc.V2G_Message.Body.SessionSetupReq_isUsed) { insert_type(va, doc.V2G_Message.Body.SessionSetupReq, doc.V2G_Message.Header); + } else if (doc.V2G_Message.Body.ServiceDiscoveryReq_isUsed) { + insert_type(va, doc.V2G_Message.Body.ServiceDiscoveryReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.AuthorizationReq_isUsed) { insert_type(va, doc.V2G_Message.Body.AuthorizationReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.CableCheckReq_isUsed) { diff --git a/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt b/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt index 6a9b91885b..d74c120414 100644 --- a/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt +++ b/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt @@ -14,6 +14,7 @@ create_exi_test_target(dc_cable_check) create_exi_test_target(dc_current_demand) create_exi_test_target(dc_pre_charge) create_exi_test_target(dc_welding_detection) +create_exi_test_target(service_discovery) create_exi_test_target(session_setup) create_exi_test_target(authorization) diff --git a/lib/everest/iso15118/test/exi/cb/iso2/service_discovery.cpp b/lib/everest/iso15118/test/exi/cb/iso2/service_discovery.cpp new file mode 100644 index 0000000000..78267d8e8f --- /dev/null +++ b/lib/everest/iso15118/test/exi/cb/iso2/service_discovery.cpp @@ -0,0 +1,95 @@ +#include + +#include +#include + +#include "helper.hpp" + +#include +#include + +using namespace iso15118; +namespace dt = d2::msg::data_types; + +SCENARIO("Ser/Deserialize d2 service discovery messages") { + GIVEN("Deserialize service discovery req") { + uint8_t doc_raw[] = {0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, 0xCE, 0xC2, 0x13, 0x4B, 0x51, 0xB0, + 0x18, 0xA8, 0xCA, 0xE6, 0xE8, 0x40, 0xE6, 0xC6, 0xDE, 0xE0, 0xCA, 0x00, 0x00}; + + const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)}; + + d2::msg::Variant variant(io::v2gtp::PayloadType::SAP, stream_view, false); + + THEN("It should be deserialized successfully") { + REQUIRE(variant.get_type() == d2::msg::Type::ServiceDiscoveryReq); + + const auto& msg = variant.get(); + const auto& header = msg.header; + + REQUIRE(header.session_id == std::array{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}); + + REQUIRE(msg.service_scope.value() == "Test scope"); + REQUIRE(msg.service_category.value() == dt::ServiceCategory::EvCharging); + } + } + GIVEN("Serialize service discovery res - minimal") { + + const auto header = d2::msg::Header{{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}, std::nullopt}; + + auto res = d2::msg::ServiceDiscoveryResponse{}; + res.header = header; + res.response_code = dt::ResponseCode::OK; + res.payment_option_list = { + dt::PaymentOption::ExternalPayment, + }; + auto service = dt::ChargeService{}; + service.service_id = 1; + service.supported_energy_transfer_mode = {dt::EnergyTransferMode::DC_extended}; + service.service_category = dt::ServiceCategory::EvCharging; + service.FreeService = true; + res.charge_service = service; + + std::vector expected = {0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, 0xCE, 0xC2, 0x13, + 0x4B, 0x51, 0xC0, 0x01, 0x20, 0x04, 0x82, 0x83, 0x24}; + + THEN("It should be serialized successfully") { + REQUIRE(serialize_helper(res) == expected); + } + } + GIVEN("Serialize service discovery res - all fields present") { + + const auto header = d2::msg::Header{{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}, std::nullopt}; + + auto res = d2::msg::ServiceDiscoveryResponse{}; + res.header = header; + res.response_code = dt::ResponseCode::OK; + res.payment_option_list = { + dt::PaymentOption::ExternalPayment, + }; + auto chargeService = dt::ChargeService{}; + chargeService.service_id = 1; + chargeService.service_name = "Service Foo"; + chargeService.supported_energy_transfer_mode = {dt::EnergyTransferMode::DC_extended}; + chargeService.service_scope = "Test scope"; + chargeService.service_category = dt::ServiceCategory::EvCharging; + chargeService.FreeService = true; + res.charge_service = chargeService; + auto service = dt::Service{}; + service.service_id = 99; + service.service_name = "Service Bar"; + service.service_scope = "Test scope 2"; + service.service_category = dt::ServiceCategory::Internet; + service.FreeService = true; + res.service_list = {service}; + + std::vector expected = { + 0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, 0xCE, 0xC2, 0x13, 0x4B, 0x51, 0xC0, 0x01, 0x20, 0x04, 0x03, 0x54, + 0xD9, 0x5C, 0x9D, 0x9A, 0x58, 0xD9, 0x48, 0x11, 0x9B, 0xDB, 0xC0, 0x01, 0x8A, 0x8C, 0xAE, 0x6E, 0x84, 0x0E, + 0x6C, 0x6D, 0xEE, 0x0C, 0xA2, 0x0C, 0x80, 0xC6, 0x01, 0xAA, 0x6C, 0xAE, 0x4E, 0xCD, 0x2C, 0x6C, 0xA4, 0x08, + 0x4C, 0x2E, 0x41, 0x00, 0xE5, 0x46, 0x57, 0x37, 0x42, 0x07, 0x36, 0x36, 0xF7, 0x06, 0x52, 0x03, 0x21, 0x10}; + + THEN("It should be serialized successfully") { + REQUIRE(serialize_helper(res) == expected); + } + } +} From 5952df0507b590535c4b4d63495c2582ca5c7e4c Mon Sep 17 00:00:00 2001 From: Kacper Dalach Date: Wed, 14 Jan 2026 10:38:26 +0100 Subject: [PATCH 2/4] libiso15118: d2: Add PaymentServiceSelection req/res Signed-off-by: Kacper Dalach --- .../message/d2/payment_service_selection.hpp | 31 ++++++++++ .../include/iso15118/message/d2/type.hpp | 4 ++ .../iso15118/src/iso15118/CMakeLists.txt | 1 + .../message/d2/payment_service_selection.cpp | 58 +++++++++++++++++++ .../src/iso15118/message/d2/variant.cpp | 2 + .../iso15118/test/exi/cb/iso2/CMakeLists.txt | 1 + .../exi/cb/iso2/payment_service_selection.cpp | 47 +++++++++++++++ 7 files changed, 144 insertions(+) create mode 100644 lib/everest/iso15118/include/iso15118/message/d2/payment_service_selection.hpp create mode 100644 lib/everest/iso15118/src/iso15118/message/d2/payment_service_selection.cpp create mode 100644 lib/everest/iso15118/test/exi/cb/iso2/payment_service_selection.cpp diff --git a/lib/everest/iso15118/include/iso15118/message/d2/payment_service_selection.hpp b/lib/everest/iso15118/include/iso15118/message/d2/payment_service_selection.hpp new file mode 100644 index 0000000000..d73788a314 --- /dev/null +++ b/lib/everest/iso15118/include/iso15118/message/d2/payment_service_selection.hpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#include +#include + +namespace iso15118::d2::msg { + +namespace data_types { +struct SelectedService { + ServiceID service_id; + std::optional parameter_set_id{std::nullopt}; +}; +using SelectedServiceList = std::vector; // [1 - 16] +} // namespace data_types + +struct PaymentServiceSelectionRequest { + Header header; + data_types::PaymentOption selected_payment_option; + data_types::SelectedServiceList selected_service_list; +}; + +struct PaymentServiceSelectionResponse { + Header header; + data_types::ResponseCode response_code; +}; + +} // namespace iso15118::d2::msg diff --git a/lib/everest/iso15118/include/iso15118/message/d2/type.hpp b/lib/everest/iso15118/include/iso15118/message/d2/type.hpp index d1895b687f..6e9853c633 100644 --- a/lib/everest/iso15118/include/iso15118/message/d2/type.hpp +++ b/lib/everest/iso15118/include/iso15118/message/d2/type.hpp @@ -14,6 +14,8 @@ enum class Type { SessionSetupRes, ServiceDiscoveryReq, ServiceDiscoveryRes, + PaymentServiceSelectionReq, + PaymentServiceSelectionRes, AuthorizationReq, AuthorizationRes, CableCheckReq, @@ -53,6 +55,8 @@ CREATE_TYPE_TRAIT(SessionSetupRequest, SessionSetupReq); CREATE_TYPE_TRAIT(SessionSetupResponse, SessionSetupRes); CREATE_TYPE_TRAIT(ServiceDiscoveryRequest, ServiceDiscoveryReq); CREATE_TYPE_TRAIT(ServiceDiscoveryResponse, ServiceDiscoveryRes); +CREATE_TYPE_TRAIT(PaymentServiceSelectionRequest, PaymentServiceSelectionReq); +CREATE_TYPE_TRAIT(PaymentServiceSelectionResponse, PaymentServiceSelectionRes); CREATE_TYPE_TRAIT(AuthorizationRequest, AuthorizationReq); CREATE_TYPE_TRAIT(AuthorizationResponse, AuthorizationRes); CREATE_TYPE_TRAIT(DC_CableCheckRequest, CableCheckReq); diff --git a/lib/everest/iso15118/src/iso15118/CMakeLists.txt b/lib/everest/iso15118/src/iso15118/CMakeLists.txt index 710ba9cf81..6e90ea1c6e 100644 --- a/lib/everest/iso15118/src/iso15118/CMakeLists.txt +++ b/lib/everest/iso15118/src/iso15118/CMakeLists.txt @@ -75,6 +75,7 @@ target_sources(iso15118 message/d2/dc_welding_detection.cpp message/d2/variant.cpp message/d2/msg_data_types.cpp + message/d2/payment_service_selection.cpp message/d2/service_discovery.cpp message/d2/session_setup.cpp diff --git a/lib/everest/iso15118/src/iso15118/message/d2/payment_service_selection.cpp b/lib/everest/iso15118/src/iso15118/message/d2/payment_service_selection.cpp new file mode 100644 index 0000000000..4a0c67b89c --- /dev/null +++ b/lib/everest/iso15118/src/iso15118/message/d2/payment_service_selection.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Pionix GmbH and Contributors to EVerest +#include + +#include + +#include +#include + +#include + +namespace iso15118::d2::msg { + +template <> void convert(const struct iso2_PaymentServiceSelectionReqType& in, PaymentServiceSelectionRequest& out) { + cb_convert_enum(in.SelectedPaymentOption, out.selected_payment_option); + + for (int i = 0; i < in.SelectedServiceList.SelectedService.arrayLen; i++) { + const auto& in_service = in.SelectedServiceList.SelectedService.array[i]; + data_types::SelectedService service; + service.service_id = in_service.ServiceID; + if (in_service.ParameterSetID_isUsed) { + service.parameter_set_id = in_service.ParameterSetID; + } + out.selected_service_list.push_back(service); + } +} + +template <> +void insert_type(VariantAccess& va, const struct iso2_PaymentServiceSelectionReqType& in, + const struct iso2_MessageHeaderType& header) { + va.insert_type(in, header); +} + +template <> void convert(const PaymentServiceSelectionResponse& in, struct iso2_PaymentServiceSelectionResType& out) { + init_iso2_PaymentServiceSelectionResType(&out); + + cb_convert_enum(in.response_code, out.ResponseCode); +} + +template <> int serialize_to_exi(const PaymentServiceSelectionResponse& in, exi_bitstream_t& out) { + + iso2_exiDocument doc; + init_iso2_exiDocument(&doc); + init_iso2_BodyType(&doc.V2G_Message.Body); + + convert(in.header, doc.V2G_Message.Header); + + CB_SET_USED(doc.V2G_Message.Body.PaymentServiceSelectionRes); + convert(in, doc.V2G_Message.Body.PaymentServiceSelectionRes); + + return encode_iso2_exiDocument(&out, &doc); +} + +template <> size_t serialize(const PaymentServiceSelectionResponse& in, const io::StreamOutputView& out) { + return serialize_helper(in, out); +} + +} // namespace iso15118::d2::msg diff --git a/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp b/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp index 59da4ba68f..241970b2d5 100644 --- a/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp +++ b/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp @@ -47,6 +47,8 @@ void handle_v2g(VariantAccess& va) { insert_type(va, doc.V2G_Message.Body.SessionSetupReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.ServiceDiscoveryReq_isUsed) { insert_type(va, doc.V2G_Message.Body.ServiceDiscoveryReq, doc.V2G_Message.Header); + } else if (doc.V2G_Message.Body.PaymentServiceSelectionReq_isUsed) { + insert_type(va, doc.V2G_Message.Body.PaymentServiceSelectionReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.AuthorizationReq_isUsed) { insert_type(va, doc.V2G_Message.Body.AuthorizationReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.CableCheckReq_isUsed) { diff --git a/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt b/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt index d74c120414..978c857c27 100644 --- a/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt +++ b/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt @@ -15,6 +15,7 @@ create_exi_test_target(dc_current_demand) create_exi_test_target(dc_pre_charge) create_exi_test_target(dc_welding_detection) create_exi_test_target(service_discovery) +create_exi_test_target(payment_service_selection) create_exi_test_target(session_setup) create_exi_test_target(authorization) diff --git a/lib/everest/iso15118/test/exi/cb/iso2/payment_service_selection.cpp b/lib/everest/iso15118/test/exi/cb/iso2/payment_service_selection.cpp new file mode 100644 index 0000000000..3203c8a240 --- /dev/null +++ b/lib/everest/iso15118/test/exi/cb/iso2/payment_service_selection.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include + +#include "helper.hpp" + +using namespace iso15118; +namespace dt = d2::msg::data_types; + +SCENARIO("Ser/Deserialize d2 payment service selection messages") { + + GIVEN("Deserialize payment service selection req") { + uint8_t doc_raw[] = {0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, 0xCE, 0xC2, + 0x13, 0x4B, 0x51, 0x32, 0x06, 0x30, 0x01, 0x08}; + + const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)}; + + d2::msg::Variant variant(io::v2gtp::PayloadType::SAP, stream_view, false); + + THEN("It should be deserialized successfully") { + REQUIRE(variant.get_type() == d2::msg::Type::PaymentServiceSelectionReq); + + const auto& msg = variant.get(); + const auto& header = msg.header; + + REQUIRE(header.session_id == std::array{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}); + + REQUIRE(msg.selected_payment_option == dt::PaymentOption::ExternalPayment); + REQUIRE(msg.selected_service_list[0].service_id == 99); + REQUIRE(msg.selected_service_list[0].parameter_set_id == 2); + } + } + GIVEN("Serialize payment service selection res") { + + const auto header = d2::msg::Header{{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}, std::nullopt}; + + const auto res = d2::msg::PaymentServiceSelectionResponse{header, dt::ResponseCode::OK}; + + std::vector expected = {0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, + 0xCE, 0xC2, 0x13, 0x4B, 0x51, 0x40, 0x00}; + + THEN("It should be serialized successfully") { + REQUIRE(serialize_helper(res) == expected); + } + } +} From d9c6474cf9034ffaed4490f1001b490151943fa9 Mon Sep 17 00:00:00 2001 From: Kacper Dalach Date: Thu, 15 Jan 2026 07:08:53 +0100 Subject: [PATCH 3/4] libiso15118: d2: Add ServiceDetail req/res Signed-off-by: Kacper Dalach --- .../iso15118/message/d2/service_detail.hpp | 43 +++++++++ .../include/iso15118/message/d2/type.hpp | 4 + .../iso15118/src/iso15118/CMakeLists.txt | 1 + .../iso15118/message/d2/service_detail.cpp | 87 +++++++++++++++++++ .../src/iso15118/message/d2/variant.cpp | 2 + .../iso15118/test/exi/cb/iso2/CMakeLists.txt | 1 + .../test/exi/cb/iso2/service_detail.cpp | 74 ++++++++++++++++ 7 files changed, 212 insertions(+) create mode 100644 lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp create mode 100644 lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp create mode 100644 lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp diff --git a/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp b/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp new file mode 100644 index 0000000000..a4e548739b --- /dev/null +++ b/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#include +#include +#include + +namespace iso15118::d2::msg { + +namespace data_types { +struct Parameter { + std::string name; + std::optional boolValue{std::nullopt}; + std::optional byteValue{std::nullopt}; + std::optional shortValue{std::nullopt}; + std::optional intValue{std::nullopt}; + std::optional physicalValue{std::nullopt}; + std::optional stringValue{std::nullopt}; +}; + +struct ParameterSet { + ParameterSetID parameter_set_id; + std::vector parameter; // [1 - 16] +}; +using ServiceParameterList = std::vector; // [1 - 255] +} // namespace data_types + +struct ServiceDetailRequest { + Header header; + data_types::ServiceID service_id; +}; + +struct ServiceDetailResponse { + Header header; + data_types::ResponseCode response_code; + data_types::ServiceID service_id; + std::optional service_parameter_list{std::nullopt}; +}; + +} // namespace iso15118::d2::msg diff --git a/lib/everest/iso15118/include/iso15118/message/d2/type.hpp b/lib/everest/iso15118/include/iso15118/message/d2/type.hpp index 6e9853c633..fe289336c9 100644 --- a/lib/everest/iso15118/include/iso15118/message/d2/type.hpp +++ b/lib/everest/iso15118/include/iso15118/message/d2/type.hpp @@ -14,6 +14,8 @@ enum class Type { SessionSetupRes, ServiceDiscoveryReq, ServiceDiscoveryRes, + ServiceDetailReq, + ServiceDetailRes, PaymentServiceSelectionReq, PaymentServiceSelectionRes, AuthorizationReq, @@ -55,6 +57,8 @@ CREATE_TYPE_TRAIT(SessionSetupRequest, SessionSetupReq); CREATE_TYPE_TRAIT(SessionSetupResponse, SessionSetupRes); CREATE_TYPE_TRAIT(ServiceDiscoveryRequest, ServiceDiscoveryReq); CREATE_TYPE_TRAIT(ServiceDiscoveryResponse, ServiceDiscoveryRes); +CREATE_TYPE_TRAIT(ServiceDetailRequest, ServiceDetailReq); +CREATE_TYPE_TRAIT(ServiceDetailResponse, ServiceDetailRes); CREATE_TYPE_TRAIT(PaymentServiceSelectionRequest, PaymentServiceSelectionReq); CREATE_TYPE_TRAIT(PaymentServiceSelectionResponse, PaymentServiceSelectionRes); CREATE_TYPE_TRAIT(AuthorizationRequest, AuthorizationReq); diff --git a/lib/everest/iso15118/src/iso15118/CMakeLists.txt b/lib/everest/iso15118/src/iso15118/CMakeLists.txt index 6e90ea1c6e..ca278c8485 100644 --- a/lib/everest/iso15118/src/iso15118/CMakeLists.txt +++ b/lib/everest/iso15118/src/iso15118/CMakeLists.txt @@ -76,6 +76,7 @@ target_sources(iso15118 message/d2/variant.cpp message/d2/msg_data_types.cpp message/d2/payment_service_selection.cpp + message/d2/service_detail.cpp message/d2/service_discovery.cpp message/d2/session_setup.cpp diff --git a/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp b/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp new file mode 100644 index 0000000000..4e0e58a060 --- /dev/null +++ b/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Pionix GmbH and Contributors to EVerest +#include + +#include + +#include +#include + +#include + +namespace iso15118::d2::msg { + +template <> void convert(const data_types::ServiceParameterList& in, struct iso2_ServiceParameterListType& out) { + int parameter_set_index = 0; + for (auto const& parameter_set : in) { + if (parameter_set_index >= iso2_ParameterSetType_5_ARRAY_SIZE) { + // TODO(kd): Should we raise exception here? I've seen that in -20 there is often no bounds checking at all. + // Example: src/iso15118/message/service_detail.cpp:274 + break; + } + + auto& out_parameter_set = out.ParameterSet.array[parameter_set_index++]; + out_parameter_set.ParameterSetID = parameter_set.parameter_set_id; + + int parameter_idx = 0; + for (auto const& parameter : parameter_set.parameter) { + if (parameter_idx >= iso2_ParameterType_16_ARRAY_SIZE) { + // TODO(kd): Should we raise exception here? I've seen that in -20 there is often no bounds checking at + // all. Example: src/iso15118/message/service_detail.cpp:274 + break; + } + auto& out_param = out_parameter_set.Parameter.array[parameter_idx++]; + CPP2CB_STRING(parameter.name, out_param.Name); + + // TODO(kd): This is a bit naive implementation. Alternatively we could do an if statement using has_value() + // for each field. This way we can prevent errors but also we will introduce a priority between field types. + CPP2CB_ASSIGN_IF_USED(parameter.boolValue, out_param.boolValue); + CPP2CB_ASSIGN_IF_USED(parameter.byteValue, out_param.byteValue); + CPP2CB_ASSIGN_IF_USED(parameter.shortValue, out_param.shortValue); + CPP2CB_ASSIGN_IF_USED(parameter.intValue, out_param.intValue); + CPP2CB_CONVERT_IF_USED(parameter.physicalValue, out_param.physicalValue); + CPP2CB_STRING_IF_USED(parameter.stringValue, out_param.stringValue); + } + out_parameter_set.Parameter.arrayLen = parameter_set.parameter.size(); + } + out.ParameterSet.arrayLen = in.size(); +} + +template <> void convert(const struct iso2_ServiceDetailReqType& in, ServiceDetailRequest& out) { + out.service_id = in.ServiceID; +} + +template <> +void insert_type(VariantAccess& va, const struct iso2_ServiceDetailReqType& in, + const struct iso2_MessageHeaderType& header) { + va.insert_type(in, header); +} + +template <> void convert(const ServiceDetailResponse& in, struct iso2_ServiceDetailResType& out) { + init_iso2_ServiceDetailResType(&out); + + cb_convert_enum(in.response_code, out.ResponseCode); + out.ServiceID = in.service_id; + + CPP2CB_CONVERT_IF_USED(in.service_parameter_list, out.ServiceParameterList); +} + +template <> int serialize_to_exi(const ServiceDetailResponse& in, exi_bitstream_t& out) { + + iso2_exiDocument doc; + init_iso2_exiDocument(&doc); + init_iso2_BodyType(&doc.V2G_Message.Body); + + convert(in.header, doc.V2G_Message.Header); + + CB_SET_USED(doc.V2G_Message.Body.ServiceDetailRes); + convert(in, doc.V2G_Message.Body.ServiceDetailRes); + + return encode_iso2_exiDocument(&out, &doc); +} + +template <> size_t serialize(const ServiceDetailResponse& in, const io::StreamOutputView& out) { + return serialize_helper(in, out); +} + +} // namespace iso15118::d2::msg diff --git a/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp b/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp index 241970b2d5..38163e75ca 100644 --- a/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp +++ b/lib/everest/iso15118/src/iso15118/message/d2/variant.cpp @@ -47,6 +47,8 @@ void handle_v2g(VariantAccess& va) { insert_type(va, doc.V2G_Message.Body.SessionSetupReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.ServiceDiscoveryReq_isUsed) { insert_type(va, doc.V2G_Message.Body.ServiceDiscoveryReq, doc.V2G_Message.Header); + } else if (doc.V2G_Message.Body.ServiceDetailReq_isUsed) { + insert_type(va, doc.V2G_Message.Body.ServiceDetailReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.PaymentServiceSelectionReq_isUsed) { insert_type(va, doc.V2G_Message.Body.PaymentServiceSelectionReq, doc.V2G_Message.Header); } else if (doc.V2G_Message.Body.AuthorizationReq_isUsed) { diff --git a/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt b/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt index 978c857c27..525eb01dfb 100644 --- a/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt +++ b/lib/everest/iso15118/test/exi/cb/iso2/CMakeLists.txt @@ -15,6 +15,7 @@ create_exi_test_target(dc_current_demand) create_exi_test_target(dc_pre_charge) create_exi_test_target(dc_welding_detection) create_exi_test_target(service_discovery) +create_exi_test_target(service_detail) create_exi_test_target(payment_service_selection) create_exi_test_target(session_setup) create_exi_test_target(authorization) diff --git a/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp b/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp new file mode 100644 index 0000000000..4aa3b87f09 --- /dev/null +++ b/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp @@ -0,0 +1,74 @@ +#include + +#include +#include + +#include "helper.hpp" + +using namespace iso15118; +namespace dt = d2::msg::data_types; + +SCENARIO("Ser/Deserialize d2 service detail messages") { + + GIVEN("Deserialize service detail req") { + uint8_t doc_raw[] = {0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, 0xCE, 0xC2, 0x13, 0x4B, 0x51, 0x91, 0x8C, 0x00}; + + const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)}; + + d2::msg::Variant variant(io::v2gtp::PayloadType::SAP, stream_view, false); + + THEN("It should be deserialized successfully") { + REQUIRE(variant.get_type() == d2::msg::Type::ServiceDetailReq); + + const auto& msg = variant.get(); + const auto& header = msg.header; + + REQUIRE(header.session_id == std::array{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}); + + REQUIRE(msg.service_id == 99); + } + } + GIVEN("Serialize service detail res") { + + const auto header = d2::msg::Header{{0x02, 0xDB, 0x22, 0x07, 0x3B, 0x08, 0x4D, 0x2D}, std::nullopt}; + + auto res = d2::msg::ServiceDetailResponse{}; + res.header = header; + res.response_code = dt::ResponseCode::OK; + res.service_id = 99; + auto parameterSet = dt::ParameterSet{}; + parameterSet.parameter_set_id = 50; + + auto boolValue = dt::Parameter{"bool"}; + boolValue.boolValue = true; + auto byteValue = dt::Parameter{"byte"}; + byteValue.byteValue = 8; + auto shortValue = dt::Parameter{"short"}; + shortValue.shortValue = 16; + auto intValue = dt::Parameter{"int"}; + intValue.intValue = 32; + auto physicalValue = dt::Parameter{"physical"}; + physicalValue.physicalValue = dt::PhysicalValue{55, -1, dt::UnitSymbol::A}; + auto stringValue = dt::Parameter{"string"}; + stringValue.stringValue = "Foo Bar"; + + parameterSet.parameter.push_back(boolValue); + parameterSet.parameter.push_back(byteValue); + parameterSet.parameter.push_back(shortValue); + parameterSet.parameter.push_back(intValue); + parameterSet.parameter.push_back(physicalValue); + parameterSet.parameter.push_back(stringValue); + res.service_parameter_list = {parameterSet}; + + std::vector expected = {0x80, 0x98, 0x02, 0x00, 0xB6, 0xC8, 0x81, 0xCE, 0xC2, 0x13, 0x4B, 0x51, 0xA0, + 0x01, 0x8C, 0x01, 0x90, 0x06, 0x62, 0x6F, 0x6F, 0x6C, 0x08, 0x01, 0x98, 0x9E, + 0x5D, 0x19, 0x4A, 0x20, 0x00, 0xEE, 0x6D, 0x0D, 0xEE, 0x4E, 0x88, 0x10, 0x00, + 0x2B, 0x4B, 0x73, 0xA3, 0x08, 0x00, 0x14, 0xE0, 0xD0, 0xF2, 0xE6, 0xD2, 0xC6, + 0xC2, 0xD9, 0x04, 0x18, 0x1B, 0x80, 0x10, 0xE6, 0xE8, 0xE4, 0xD2, 0xDC, 0xCF, + 0x41, 0x28, 0xCD, 0xED, 0xE4, 0x08, 0x4C, 0x2E, 0x42, 0x80}; + + THEN("It should be serialized successfully") { + REQUIRE(serialize_helper(res) == expected); // FIXME: Failing right now + } + } +} From 3970efbdc2aaf250dccf671169a4216172f0ef02 Mon Sep 17 00:00:00 2001 From: Kacper Dalach Date: Thu, 15 Jan 2026 10:27:49 +0100 Subject: [PATCH 4/4] libiso15118: d2: Use std::variant in Parameter std::variant is used instead of a bunch of std::optional. This is to make the usage of Parameter more clear. Signed-off-by: Kacper Dalach --- .../iso15118/message/d2/service_detail.hpp | 8 ++--- .../iso15118/message/d2/service_detail.cpp | 29 ++++++++++++++----- .../test/exi/cb/iso2/service_detail.cpp | 18 ++++-------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp b/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp index a4e548739b..8b7e2d84f1 100644 --- a/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp +++ b/lib/everest/iso15118/include/iso15118/message/d2/service_detail.hpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace iso15118::d2::msg { @@ -13,12 +14,7 @@ namespace iso15118::d2::msg { namespace data_types { struct Parameter { std::string name; - std::optional boolValue{std::nullopt}; - std::optional byteValue{std::nullopt}; - std::optional shortValue{std::nullopt}; - std::optional intValue{std::nullopt}; - std::optional physicalValue{std::nullopt}; - std::optional stringValue{std::nullopt}; + std::variant value; }; struct ParameterSet { diff --git a/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp b/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp index 4e0e58a060..500c0154f1 100644 --- a/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp +++ b/lib/everest/iso15118/src/iso15118/message/d2/service_detail.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Pionix GmbH and Contributors to EVerest +#include #include #include @@ -8,6 +9,7 @@ #include #include +#include namespace iso15118::d2::msg { @@ -33,14 +35,25 @@ template <> void convert(const data_types::ServiceParameterList& in, struct iso2 auto& out_param = out_parameter_set.Parameter.array[parameter_idx++]; CPP2CB_STRING(parameter.name, out_param.Name); - // TODO(kd): This is a bit naive implementation. Alternatively we could do an if statement using has_value() - // for each field. This way we can prevent errors but also we will introduce a priority between field types. - CPP2CB_ASSIGN_IF_USED(parameter.boolValue, out_param.boolValue); - CPP2CB_ASSIGN_IF_USED(parameter.byteValue, out_param.byteValue); - CPP2CB_ASSIGN_IF_USED(parameter.shortValue, out_param.shortValue); - CPP2CB_ASSIGN_IF_USED(parameter.intValue, out_param.intValue); - CPP2CB_CONVERT_IF_USED(parameter.physicalValue, out_param.physicalValue); - CPP2CB_STRING_IF_USED(parameter.stringValue, out_param.stringValue); + if (std::holds_alternative(parameter.value)) { + out_param.boolValue = std::get(parameter.value); + out_param.boolValue_isUsed = true; + } else if (std::holds_alternative(parameter.value)) { + out_param.byteValue = std::get(parameter.value); + out_param.byteValue_isUsed = true; + } else if (std::holds_alternative(parameter.value)) { + out_param.shortValue = std::get(parameter.value); + out_param.shortValue_isUsed = true; + } else if (std::holds_alternative(parameter.value)) { + out_param.intValue = std::get(parameter.value); + out_param.intValue_isUsed = true; + } else if (std::holds_alternative(parameter.value)) { + convert(std::get(parameter.value), out_param.physicalValue); + out_param.physicalValue_isUsed = true; + } else if (std::holds_alternative(parameter.value)) { + CPP2CB_STRING(std::get(parameter.value), out_param.stringValue); + out_param.stringValue_isUsed = true; + } } out_parameter_set.Parameter.arrayLen = parameter_set.parameter.size(); } diff --git a/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp b/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp index 4aa3b87f09..fd87f1a667 100644 --- a/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp +++ b/lib/everest/iso15118/test/exi/cb/iso2/service_detail.cpp @@ -39,18 +39,12 @@ SCENARIO("Ser/Deserialize d2 service detail messages") { auto parameterSet = dt::ParameterSet{}; parameterSet.parameter_set_id = 50; - auto boolValue = dt::Parameter{"bool"}; - boolValue.boolValue = true; - auto byteValue = dt::Parameter{"byte"}; - byteValue.byteValue = 8; - auto shortValue = dt::Parameter{"short"}; - shortValue.shortValue = 16; - auto intValue = dt::Parameter{"int"}; - intValue.intValue = 32; - auto physicalValue = dt::Parameter{"physical"}; - physicalValue.physicalValue = dt::PhysicalValue{55, -1, dt::UnitSymbol::A}; - auto stringValue = dt::Parameter{"string"}; - stringValue.stringValue = "Foo Bar"; + dt::Parameter boolValue{"bool", true}; + dt::Parameter byteValue{"byte", (int8_t)8}; + dt::Parameter shortValue{"short", (int16_t)16}; + dt::Parameter intValue{"int", (int32_t)32}; + dt::Parameter physicalValue{"physical", dt::PhysicalValue{55, -1, dt::UnitSymbol::A}}; + dt::Parameter stringValue{"string", "Foo Bar"}; parameterSet.parameter.push_back(boolValue); parameterSet.parameter.push_back(byteValue);