diff --git a/include/iso15118/ev/d20/context.hpp b/include/iso15118/ev/d20/context.hpp index 7a146732..17949aa0 100644 --- a/include/iso15118/ev/d20/context.hpp +++ b/include/iso15118/ev/d20/context.hpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace iso15118::ev::d20 { @@ -56,7 +57,7 @@ class Session; class Context { public: - Context(MessageExchange&); + Context(session::feedback::Callbacks, MessageExchange&); template BasePointerType create_state(Args&&... args) { return std::make_unique(*this, std::forward(args)...); @@ -105,6 +106,11 @@ class Context { return session; } + // Contains the EVSE received data + EVSESessionInfo evse_session_info; + + const iso15118::ev::d20::session::Feedback feedback; + private: MessageExchange& message_exchange; diff --git a/include/iso15118/ev/d20/evse_session_info.hpp b/include/iso15118/ev/d20/evse_session_info.hpp new file mode 100644 index 00000000..cc730036 --- /dev/null +++ b/include/iso15118/ev/d20/evse_session_info.hpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +namespace iso15118::ev::d20 { + +// Holds information reported by the EVSE that could be different between sessions +struct EVSESessionInfo { + std::vector auth_services{}; + bool certificate_installation_service{false}; + std::optional> supported_providers{std::nullopt}; + message_20::datatypes::GenChallenge gen_challenge{std::array{}}; // 16 bytes +}; + +} // namespace iso15118::ev::d20 diff --git a/include/iso15118/ev/d20/state/authorization.hpp b/include/iso15118/ev/d20/state/authorization.hpp new file mode 100644 index 00000000..f6b14c27 --- /dev/null +++ b/include/iso15118/ev/d20/state/authorization.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include "../states.hpp" + +namespace iso15118::ev::d20::state { + +struct Authorization : public StateBase { +public: + Authorization(Context& ctx) : StateBase(ctx, StateID::Authorization) { + } + + void enter() final; + + Result feed(Event) final; +}; + +} // namespace iso15118::ev::d20::state \ No newline at end of file diff --git a/include/iso15118/ev/detail/d20/context_helper.hpp b/include/iso15118/ev/detail/d20/context_helper.hpp index 40ab1305..f70f0d76 100644 --- a/include/iso15118/ev/detail/d20/context_helper.hpp +++ b/include/iso15118/ev/detail/d20/context_helper.hpp @@ -2,8 +2,8 @@ // Copyright 2025 Pionix GmbH and Contributors to EVerest #pragma once -// #include -// #include +#include +#include #include // Forward declaration diff --git a/include/iso15118/ev/session/feedback.hpp b/include/iso15118/ev/session/feedback.hpp new file mode 100644 index 00000000..c517e845 --- /dev/null +++ b/include/iso15118/ev/session/feedback.hpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace iso15118::ev::d20::session { + +namespace dt = message_20::datatypes; + +namespace feedback { + +struct Callbacks { + std::function evse_session_info; +}; + +} // namespace feedback + +class Feedback { +public: + Feedback(feedback::Callbacks); + + void evse_session_info(const EVSESessionInfo&) const; + +private: + feedback::Callbacks callbacks; +}; + +} // namespace iso15118::ev::d20::session diff --git a/src/iso15118/CMakeLists.txt b/src/iso15118/CMakeLists.txt index a83fda39..2f48aac4 100644 --- a/src/iso15118/CMakeLists.txt +++ b/src/iso15118/CMakeLists.txt @@ -16,7 +16,9 @@ target_sources(iso15118 ev/d20/context_helper.cpp ev/d20/state/session_setup.cpp ev/d20/state/authorization_setup.cpp + ev/d20/state/authorization.cpp ev/d20/state/dc_charge_parameter_discovery.cpp + ev/session/feedback.cpp io/connection_plain.cpp io/logging.cpp diff --git a/src/iso15118/ev/d20/context.cpp b/src/iso15118/ev/d20/context.cpp index 6c2e1ab4..b9f81856 100644 --- a/src/iso15118/ev/d20/context.cpp +++ b/src/iso15118/ev/d20/context.cpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 Pionix GmbH and Contributors to EVerest -#include - #include +#include +#include namespace iso15118::ev::d20 { @@ -34,7 +34,8 @@ message_20::Type MessageExchange::peek_response_type() const { return response->get_type(); } -Context::Context(MessageExchange& message_exchange_) : message_exchange(message_exchange_) { +Context::Context(session::feedback::Callbacks feedback_callbacks, MessageExchange& message_exchange_) : + feedback(std::move(feedback_callbacks)), message_exchange(message_exchange_) { } std::unique_ptr Context::pull_response() { diff --git a/src/iso15118/ev/d20/state/authorization.cpp b/src/iso15118/ev/d20/state/authorization.cpp new file mode 100644 index 00000000..d899c572 --- /dev/null +++ b/src/iso15118/ev/d20/state/authorization.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include + +namespace iso15118::ev::d20::state { + +message_20::AuthorizationRequest handle_request() { + // TODO(SL): Implement + return message_20::AuthorizationRequest(); +} + +void Authorization::enter() { + // TODO(SL): Adding logging +} + +Result Authorization::feed(Event ev) { + if (ev != Event::V2GTP_MESSAGE) { + return {}; + } else { + return {}; + } +} + +} // namespace iso15118::ev::d20::state diff --git a/src/iso15118/ev/d20/state/authorization_setup.cpp b/src/iso15118/ev/d20/state/authorization_setup.cpp index 5b5c78de..1c1c6ec1 100644 --- a/src/iso15118/ev/d20/state/authorization_setup.cpp +++ b/src/iso15118/ev/d20/state/authorization_setup.cpp @@ -1,14 +1,96 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2025 Pionix GmbH and Contributors to EVerest +// Copyright 2025 Pionix GmbH, Roger Bedell and Contributors to EVerest +#include +#include +#include #include +#include +#include +#include +#include namespace iso15118::ev::d20::state { +namespace { +using ResponseCode = message_20::datatypes::ResponseCode; +bool check_response_code(ResponseCode response_code) { + switch (response_code) { + case ResponseCode::OK: + return true; + [[fallthrough]]; + default: + return false; + } +} +} // namespace + void AuthorizationSetup::enter() { + // TODO(SL): Adding logging } Result AuthorizationSetup::feed([[maybe_unused]] Event ev) { - return {}; -} + if (ev != Event::V2GTP_MESSAGE) { + return {}; + } + + const auto variant = m_ctx.pull_response(); + + if (const auto res = variant->get_if()) { + if (not check_response_code(res->response_code)) { + m_ctx.stop_session(true); // Tell stack to close the tcp/tls connection + return {}; + } + + if (res->certificate_installation_service) { + // TODO(RB): Implement certificate installation service + logf_warning("EVSE supports certificate installation service, but this is not implemented in the EV yet."); + // Remember it in the context for later use + m_ctx.evse_session_info.certificate_installation_service = true; + } + + // Save the offered authorization services in the session context so we can use it later and also report it to + // the EV. + m_ctx.evse_session_info.auth_services = res->authorization_services; + + if (std::holds_alternative(res->authorization_mode)) { + const auto& pnc_auth_mode = + std::get(res->authorization_mode); + m_ctx.evse_session_info.supported_providers = pnc_auth_mode.supported_providers.value(); + m_ctx.evse_session_info.gen_challenge = pnc_auth_mode.gen_challenge; + } else { + // EIM selected, nothing to do here for now since authorization_mode is empty for EIM + } + // Inform the ev about the evse session information + m_ctx.feedback.evse_session_info(m_ctx.evse_session_info); + + // Send request and transition to next state + message_20::AuthorizationRequest req; + setup_header(req.header, m_ctx.get_session()); + // TODO(RB): Choose the authorization service based on user preference and what the evse supports + // For now, we just pick the first one offered by the evse + if (m_ctx.evse_session_info.auth_services.empty()) { + logf_error("No authorization services offered by the EVSE. Abort the session."); + m_ctx.stop_session(true); // Tell stack to close the tcp/tls connection + return {}; + } + req.selected_authorization_service = m_ctx.evse_session_info.auth_services.front(); + if (req.selected_authorization_service == message_20::datatypes::Authorization::EIM) { + req.authorization_mode = message_20::datatypes::EIM_ASReqAuthorizationMode{}; + } else if (req.selected_authorization_service == message_20::datatypes::Authorization::PnC) { + // TODO(RB): Fill in the PnC authorization mode data + req.authorization_mode = message_20::datatypes::PnC_ASReqAuthorizationMode{}; + } else { + logf_error("Unknown authorization service selected. Abort the session."); + m_ctx.stop_session(true); // Tell stack to close the tcp/tls connection + return {}; + } + m_ctx.respond(req); + return m_ctx.create_state(); + } else { + logf_error("expected AuthorizationSetupResponse! But code type id: %d", variant->get_type()); + m_ctx.stop_session(true); // Tell stack to close the tcp/tls connection + return {}; + } +} } // namespace iso15118::ev::d20::state diff --git a/src/iso15118/ev/session/feedback.cpp b/src/iso15118/ev/session/feedback.cpp new file mode 100644 index 00000000..387d9a44 --- /dev/null +++ b/src/iso15118/ev/session/feedback.cpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +namespace iso15118::ev::d20::session { + +Feedback::Feedback(feedback::Callbacks callbacks_) : callbacks(std::move(callbacks_)) { +} + +void Feedback::evse_session_info(const d20::EVSESessionInfo& info) const { + call_if_available(callbacks.evse_session_info, info); +} + +} // namespace iso15118::ev::d20::session diff --git a/test/iso15118/ev/fsm/CMakeLists.txt b/test/iso15118/ev/fsm/CMakeLists.txt index a5d1aefa..f0bc38c3 100644 --- a/test/iso15118/ev/fsm/CMakeLists.txt +++ b/test/iso15118/ev/fsm/CMakeLists.txt @@ -14,4 +14,5 @@ function(create_ev_fsm_test_target NAME) catch_discover_tests(test_ev_fsm_${NAME}) endfunction() -create_ev_fsm_test_target(session_setup) \ No newline at end of file +create_ev_fsm_test_target(session_setup) +create_ev_fsm_test_target(authorization_setup) diff --git a/test/iso15118/ev/fsm/authorization_setup.cpp b/test/iso15118/ev/fsm/authorization_setup.cpp new file mode 100644 index 00000000..804b9c8d --- /dev/null +++ b/test/iso15118/ev/fsm/authorization_setup.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 Pionix GmbH and Contributors to EVerest +#include + +#include "helper.hpp" + +#include +#include +#include +#include + +using namespace iso15118; + +SCENARIO("ISO15118-20 EV authorization setup state transitions") { + + const ev::d20::session::feedback::Callbacks callbacks{}; + + auto state_helper = FsmStateHelper(callbacks); + + auto ctx = state_helper.get_context(); + + GIVEN("Good case - authorization setup response with OK and EIM") { + + // setup the state and context to something reasonable + const auto header = message_20::Header{{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, 1691411798}; + + ctx.get_session().set_id(header.session_id); + + fsm::v2::FSM fsm{ctx.create_state()}; + + const auto res = message_20::AuthorizationSetupResponse{ + header, message_20::datatypes::ResponseCode::OK, {message_20::datatypes::Authorization::EIM}, false}; + + state_helper.handle_response(res); + + const auto result = fsm.feed(ev::d20::Event::V2GTP_MESSAGE); + + THEN("Check if passes to authorization state and sends EIM AuthorizationRequest") { + REQUIRE(result.transitioned() == true); + REQUIRE(fsm.get_current_state_id() == ev::d20::StateID::Authorization); + + const auto request_message = ctx.get_request(); + REQUIRE(request_message.has_value()); + + const auto& request = request_message.value(); + REQUIRE(request.header.session_id == header.session_id); + REQUIRE(request.selected_authorization_service == message_20::datatypes::Authorization::EIM); + REQUIRE( + std::holds_alternative(request.authorization_mode)); + } + } + + // PnC is not yet implemented properly in the EV, but we can at least test that the state machine transitions + // correctly + GIVEN("Good case - authorization setup response with OK and PnC") { + + // setup the state and context to something reasonable + const auto header = message_20::Header{{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, 1691411798}; + + ctx.get_session().set_id(header.session_id); + + fsm::v2::FSM fsm{ctx.create_state()}; + + const auto res = message_20::AuthorizationSetupResponse{ + header, message_20::datatypes::ResponseCode::OK, {message_20::datatypes::Authorization::PnC}, false}; + + state_helper.handle_response(res); + + const auto result = fsm.feed(ev::d20::Event::V2GTP_MESSAGE); + + THEN("Check if passes to authorization state and sends Pnc AuthorizationRequest") { + REQUIRE(result.transitioned() == true); + REQUIRE(fsm.get_current_state_id() == ev::d20::StateID::Authorization); + + const auto request_message = ctx.get_request(); + REQUIRE(request_message.has_value()); + + const auto& request = request_message.value(); + REQUIRE(request.header.session_id == header.session_id); + REQUIRE(request.selected_authorization_service == message_20::datatypes::Authorization::PnC); + REQUIRE( + std::holds_alternative(request.authorization_mode)); + // TODO(RB): Add checks for PnC authorization mode + } + } + // TODO(RB): Add more test cases (bad response codes, unsupported authorization modes, + // more than one authorization mode, certificate installation service, etc) +} diff --git a/test/iso15118/ev/fsm/helper.hpp b/test/iso15118/ev/fsm/helper.hpp index 77407c01..fdae168d 100644 --- a/test/iso15118/ev/fsm/helper.hpp +++ b/test/iso15118/ev/fsm/helper.hpp @@ -13,7 +13,7 @@ using namespace iso15118; class FsmStateHelper { public: - FsmStateHelper() : ctx(msg_exch){}; + FsmStateHelper(const ev::d20::session::feedback::Callbacks& callbacks) : ctx(callbacks, msg_exch){}; ev::d20::Context& get_context(); diff --git a/test/iso15118/ev/fsm/session_setup.cpp b/test/iso15118/ev/fsm/session_setup.cpp index 0333278e..c62f5773 100644 --- a/test/iso15118/ev/fsm/session_setup.cpp +++ b/test/iso15118/ev/fsm/session_setup.cpp @@ -14,16 +14,17 @@ using namespace iso15118; SCENARIO("ISO15118-20 EV session setup state transitions") { - auto state_helper = FsmStateHelper(); + const ev::d20::session::feedback::Callbacks callbacks{}; + + auto state_helper = FsmStateHelper(callbacks); + auto ctx = state_helper.get_context(); GIVEN("Good case - new session") { fsm::v2::FSM fsm{ctx.create_state()}; - const auto header = message_20::Header{ - .session_id = std::array{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, - .timestamp = 1691411798, - }; + const auto header = message_20::Header{{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, 1691411798}; + const auto res = message_20::SessionSetupResponse{ header, message_20::datatypes::ResponseCode::OK_NewSessionEstablished, "everest se"}; @@ -60,10 +61,8 @@ SCENARIO("ISO15118-20 EV session setup state transitions") { 0x51, 0x13, 0xbb, 0x5, 0x95, 0x30, 0x66, 0x11, 0x46, 0xb8, 0x94, 0x3e, 0x59, 0x6d, 0x35, 0xae, 0x9, 0x76, 0xfa, 0x2a, 0x3b, 0xb0, 0x63, 0x6e, 0x12, 0x7f, 0x10, 0xdb, 0x60, 0xd6, 0xb7, 0x4d}); - const auto header = message_20::Header{ - .session_id = std::array{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, - .timestamp = 1691411798, - }; + const auto header = message_20::Header{{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, 1691411798}; + const auto res = message_20::SessionSetupResponse{ header, message_20::datatypes::ResponseCode::OK_OldSessionJoined, "everest se"}; @@ -101,10 +100,8 @@ SCENARIO("ISO15118-20 EV session setup state transitions") { 0x51, 0x13, 0xbb, 0x5, 0x95, 0x30, 0x66, 0x11, 0x46, 0xb8, 0x94, 0x3e, 0x59, 0x6d, 0x35, 0xae, 0x9, 0x76, 0xfa, 0x2a, 0x3b, 0xb0, 0x63, 0x6e, 0x12, 0x7f, 0x10, 0xdb, 0x60, 0xd6, 0xb7, 0x4d}); - const auto header = message_20::Header{ - .session_id = std::array{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x03}, - .timestamp = 1691411798, - }; + const auto header = message_20::Header{{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x03}, 1691411798}; + const auto res = message_20::SessionSetupResponse{ header, message_20::datatypes::ResponseCode::OK_OldSessionJoined, "everest se"}; @@ -124,10 +121,7 @@ SCENARIO("ISO15118-20 EV session setup state transitions") { // Set the session ID to match the one in the SessionSetupResponse ctx.get_session().set_id(std::array{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}); - const auto header = message_20::Header{ - .session_id = std::array{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, - .timestamp = 1691411798, - }; + const auto header = message_20::Header{{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02}, 1691411798}; // Set a different charger cert hash than the one that was used to create the session ctx.set_charger_cert_hash(io::sha512_hash_t{