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:
137
tools/EVerest-main/modules/EVSE/EvseV2G/BUILD.bazel
Normal file
137
tools/EVerest-main/modules/EVSE/EvseV2G/BUILD.bazel
Normal file
@@ -0,0 +1,137 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")
|
||||
load("//modules:module.bzl", "cc_everest_module")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
|
||||
|
||||
cc_everest_module(
|
||||
name = "EvseV2G",
|
||||
srcs = glob([
|
||||
"*.cpp",
|
||||
"*.hpp",
|
||||
]),
|
||||
impls = [
|
||||
"charger",
|
||||
"connection",
|
||||
"crypto",
|
||||
"extensions",
|
||||
],
|
||||
includes = [
|
||||
"connection",
|
||||
"crypto",
|
||||
],
|
||||
deps = [
|
||||
"//lib/everest/cbv2g:din",
|
||||
"//lib/everest/cbv2g:iso2",
|
||||
"//lib/everest/cbv2g:tp",
|
||||
"//lib/everest/tls",
|
||||
"@libevent//:event",
|
||||
"@libevent//:event_pthreads",
|
||||
],
|
||||
)
|
||||
|
||||
# Shared test helpers: log stub, test stubs, and common includes.
|
||||
_TEST_COPTS = [
|
||||
"-std=c++17",
|
||||
"-DUNIT_TEST",
|
||||
]
|
||||
|
||||
_TEST_INCLUDES = [
|
||||
".",
|
||||
"connection",
|
||||
"crypto",
|
||||
"generated/modules/EvseV2G",
|
||||
]
|
||||
|
||||
cc_library(
|
||||
name = "test_helpers",
|
||||
srcs = ["tests/log.cpp"],
|
||||
hdrs = glob(["tests/*.hpp"]) + glob([
|
||||
"*.hpp",
|
||||
"connection/*.hpp",
|
||||
"crypto/*.hpp",
|
||||
]),
|
||||
copts = _TEST_COPTS,
|
||||
includes = _TEST_INCLUDES,
|
||||
deps = [
|
||||
":ld-ev",
|
||||
"//lib/everest/cbv2g:din",
|
||||
"//lib/everest/cbv2g:iso2",
|
||||
"//lib/everest/cbv2g:tp",
|
||||
"//lib/everest/evse_security:libevse-security",
|
||||
"//lib/everest/framework",
|
||||
"//lib/everest/tls",
|
||||
"//tests:module_adapter_stub",
|
||||
"@everest-core//interfaces:interfaces_lib",
|
||||
"@libevent//:event",
|
||||
"@libevent//:event_pthreads",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "sdp_test",
|
||||
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
|
||||
srcs = [
|
||||
"sdp.cpp",
|
||||
"tests/sdp_test.cpp",
|
||||
"tools.cpp",
|
||||
],
|
||||
copts = _TEST_COPTS,
|
||||
includes = _TEST_INCLUDES,
|
||||
deps = [
|
||||
":test_helpers",
|
||||
"//lib/everest/cbv2g:tp",
|
||||
"//lib/everest/framework",
|
||||
"//lib/everest/tls",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "din_server_test",
|
||||
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
|
||||
srcs = [
|
||||
"din_server.cpp",
|
||||
"tests/din_server_test.cpp",
|
||||
"tools.cpp",
|
||||
],
|
||||
copts = _TEST_COPTS + ["-DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL"],
|
||||
includes = _TEST_INCLUDES,
|
||||
deps = [
|
||||
":test_helpers",
|
||||
"//lib/everest/cbv2g:din",
|
||||
"//lib/everest/cbv2g:iso2",
|
||||
"//lib/everest/cbv2g:tp",
|
||||
"//lib/everest/evse_security:libevse-security",
|
||||
"//lib/everest/framework",
|
||||
"//lib/everest/tls",
|
||||
"//third-party/bazel/openssl:crypto",
|
||||
"//third-party/bazel/openssl:ssl",
|
||||
"@googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "v2g_ctx_test",
|
||||
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
|
||||
srcs = [
|
||||
"tests/tools_test.cpp",
|
||||
"tests/v2g_ctx_test.cpp",
|
||||
"tools.cpp",
|
||||
"v2g_ctx.cpp",
|
||||
],
|
||||
copts = _TEST_COPTS + ["-DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL"],
|
||||
includes = _TEST_INCLUDES,
|
||||
deps = [
|
||||
":test_helpers",
|
||||
"//lib/everest/cbv2g:din",
|
||||
"//lib/everest/cbv2g:iso2",
|
||||
"//lib/everest/cbv2g:tp",
|
||||
"//lib/everest/evse_security:libevse-security",
|
||||
"//lib/everest/framework",
|
||||
"//lib/everest/tls",
|
||||
"//third-party/bazel/openssl:crypto",
|
||||
"//third-party/bazel/openssl:ssl",
|
||||
"@googletest//:gtest_main",
|
||||
"@libevent//:event",
|
||||
"@libevent//:event_pthreads",
|
||||
],
|
||||
)
|
||||
66
tools/EVerest-main/modules/EVSE/EvseV2G/CMakeLists.txt
Normal file
66
tools/EVerest-main/modules/EVSE/EvseV2G/CMakeLists.txt
Normal file
@@ -0,0 +1,66 @@
|
||||
#
|
||||
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
# template version 3
|
||||
#
|
||||
|
||||
# module setup:
|
||||
# - ${MODULE_NAME}: module name
|
||||
ev_setup_cpp_module()
|
||||
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
# insert your custom targets and additional config variables here
|
||||
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"charger/ISO15118_chargerImpl.cpp"
|
||||
"extensions/iso15118_extensionsImpl.cpp"
|
||||
)
|
||||
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
# Add pkg-config functionality
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
# search for libevent.pc
|
||||
pkg_search_module(EVENT REQUIRED libevent)
|
||||
|
||||
target_include_directories(${MODULE_NAME} PRIVATE
|
||||
crypto
|
||||
connection
|
||||
)
|
||||
|
||||
target_link_libraries(${MODULE_NAME} PUBLIC ${EVENT_LIBRARIES} -levent -lpthread -levent_pthreads)
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
cbv2g::din
|
||||
cbv2g::iso2
|
||||
cbv2g::tp
|
||||
)
|
||||
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"connection/connection.cpp"
|
||||
"iso_server.cpp"
|
||||
"din_server.cpp"
|
||||
"log.cpp"
|
||||
"sdp.cpp"
|
||||
"tools.cpp"
|
||||
"v2g_ctx.cpp"
|
||||
"v2g_server.cpp"
|
||||
)
|
||||
|
||||
target_link_libraries(${MODULE_NAME}
|
||||
PRIVATE
|
||||
everest::tls
|
||||
)
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
"crypto/crypto_openssl.cpp"
|
||||
"connection/tls_connection.cpp"
|
||||
)
|
||||
|
||||
if(EVEREST_CORE_BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
|
||||
128
tools/EVerest-main/modules/EVSE/EvseV2G/EvseV2G.cpp
Normal file
128
tools/EVerest-main/modules/EVSE/EvseV2G/EvseV2G.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#include "EvseV2G.hpp"
|
||||
#include "connection/connection.hpp"
|
||||
#include "connection/tls_connection.hpp"
|
||||
#include "log.hpp"
|
||||
#include "sdp.hpp"
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
#include <csignal>
|
||||
#include <everest/tls/openssl_util.hpp>
|
||||
#include <stdexcept>
|
||||
namespace {
|
||||
void log_handler(openssl::log_level_t level, const std::string& str) {
|
||||
switch (level) {
|
||||
case openssl::log_level_t::debug:
|
||||
// ignore debug logs
|
||||
break;
|
||||
case openssl::log_level_t::info:
|
||||
EVLOG_info << str;
|
||||
break;
|
||||
case openssl::log_level_t::warning:
|
||||
EVLOG_warning << str;
|
||||
break;
|
||||
case openssl::log_level_t::error:
|
||||
default:
|
||||
EVLOG_error << str;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct v2g_context* v2g_ctx = nullptr;
|
||||
|
||||
namespace module {
|
||||
|
||||
void EvseV2G::init() {
|
||||
|
||||
std::vector<ISO15118_vasIntf*> r_vas;
|
||||
r_vas.reserve(r_iso15118_vas.size());
|
||||
for (const auto& vas : r_iso15118_vas) {
|
||||
r_vas.emplace_back(vas.get());
|
||||
}
|
||||
/* create v2g context */
|
||||
v2g_ctx = v2g_ctx_create(p_charger.get(), p_extensions.get(), r_security.get(), r_vas);
|
||||
|
||||
if (v2g_ctx == nullptr) {
|
||||
throw std::runtime_error("Failed to create v2g context");
|
||||
}
|
||||
|
||||
(void)openssl::set_log_handler(log_handler);
|
||||
tls::Server::configure_signal_handler(SIGUSR1);
|
||||
v2g_ctx->tls_server = &tls_server;
|
||||
|
||||
this->r_security->subscribe_certificate_store_update(
|
||||
[this](const types::evse_security::CertificateStoreUpdate& update) {
|
||||
if (!update.leaf_certificate_type.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.leaf_certificate_type.value() != types::evse_security::LeafCertificateType::V2G) {
|
||||
return;
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "Certificate store update received, reconfiguring TLS server");
|
||||
auto config = std::make_unique<tls::Server::config_t>();
|
||||
if (build_config(*config, v2g_ctx)) {
|
||||
dlog(DLOG_LEVEL_INFO, "Configuration of TLS server successful, updating it");
|
||||
v2g_ctx->tls_server->update(*config);
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_INFO, "Configuration of TLS server failed, suspending it");
|
||||
v2g_ctx->tls_server->suspend();
|
||||
}
|
||||
});
|
||||
|
||||
invoke_init(*p_charger);
|
||||
invoke_init(*p_extensions);
|
||||
}
|
||||
|
||||
void EvseV2G::ready() {
|
||||
int rv = 0;
|
||||
|
||||
dlog(DLOG_LEVEL_DEBUG, "Starting SDP responder");
|
||||
|
||||
rv = connection_init(v2g_ctx);
|
||||
|
||||
if (rv == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to initialize connection");
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
if (config.enable_sdp_server) {
|
||||
rv = sdp_init(v2g_ctx);
|
||||
|
||||
if (rv == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to start SDP responder");
|
||||
goto err_out;
|
||||
}
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_DEBUG, "starting socket server(s)");
|
||||
if (connection_start_servers(v2g_ctx)) {
|
||||
dlog(DLOG_LEVEL_ERROR, "start_connection_servers() failed");
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
invoke_ready(*p_charger);
|
||||
invoke_ready(*p_extensions);
|
||||
|
||||
rv = sdp_listen(v2g_ctx);
|
||||
|
||||
if (rv == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "sdp_listen() failed");
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
err_out:
|
||||
throw std::runtime_error("Could not initialise EvseV2G module");
|
||||
}
|
||||
|
||||
EvseV2G::~EvseV2G() {
|
||||
v2g_ctx_free(v2g_ctx);
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
89
tools/EVerest-main/modules/EVSE/EvseV2G/EvseV2G.hpp
Normal file
89
tools/EVerest-main/modules/EVSE/EvseV2G/EvseV2G.hpp
Normal file
@@ -0,0 +1,89 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef EVSE_V2G_HPP
|
||||
#define EVSE_V2G_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 2
|
||||
//
|
||||
|
||||
#include "ld-ev.hpp"
|
||||
|
||||
// headers for provided interface implementations
|
||||
#include <generated/interfaces/ISO15118_charger/Implementation.hpp>
|
||||
#include <generated/interfaces/iso15118_extensions/Implementation.hpp>
|
||||
|
||||
// headers for required interface implementations
|
||||
#include <generated/interfaces/ISO15118_vas/Interface.hpp>
|
||||
#include <generated/interfaces/evse_security/Interface.hpp>
|
||||
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
// insert your custom include headers here
|
||||
#include "v2g_ctx.hpp"
|
||||
#include <everest/tls/tls.hpp>
|
||||
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
std::string device;
|
||||
bool supported_DIN70121;
|
||||
bool supported_ISO15118_2;
|
||||
std::string tls_security;
|
||||
bool terminate_connection_on_failed_response;
|
||||
bool tls_key_logging;
|
||||
std::string tls_key_logging_path;
|
||||
int tls_timeout;
|
||||
bool verify_contract_cert_chain;
|
||||
int auth_timeout_pnc;
|
||||
int auth_timeout_eim;
|
||||
bool enable_sdp_server;
|
||||
};
|
||||
|
||||
class EvseV2G : public Everest::ModuleBase {
|
||||
public:
|
||||
EvseV2G() = delete;
|
||||
EvseV2G(const ModuleInfo& info, std::unique_ptr<ISO15118_chargerImplBase> p_charger,
|
||||
std::unique_ptr<iso15118_extensionsImplBase> p_extensions, std::unique_ptr<evse_securityIntf> r_security,
|
||||
std::vector<std::unique_ptr<ISO15118_vasIntf>> r_iso15118_vas, Conf& config) :
|
||||
ModuleBase(info),
|
||||
p_charger(std::move(p_charger)),
|
||||
p_extensions(std::move(p_extensions)),
|
||||
r_security(std::move(r_security)),
|
||||
r_iso15118_vas(std::move(r_iso15118_vas)),
|
||||
config(config){};
|
||||
|
||||
const std::unique_ptr<ISO15118_chargerImplBase> p_charger;
|
||||
const std::unique_ptr<iso15118_extensionsImplBase> p_extensions;
|
||||
const std::unique_ptr<evse_securityIntf> r_security;
|
||||
const std::vector<std::unique_ptr<ISO15118_vasIntf>> r_iso15118_vas;
|
||||
const Conf& config;
|
||||
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
~EvseV2G();
|
||||
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
|
||||
|
||||
protected:
|
||||
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
|
||||
// insert your protected definitions here
|
||||
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
|
||||
|
||||
private:
|
||||
friend class LdEverest;
|
||||
void init();
|
||||
void ready();
|
||||
|
||||
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
|
||||
// insert your private definitions here
|
||||
tls::Server tls_server;
|
||||
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
|
||||
};
|
||||
|
||||
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
|
||||
// insert other definitions here
|
||||
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // EVSE_V2G_HPP
|
||||
@@ -0,0 +1,652 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#include "ISO15118_chargerImpl.hpp"
|
||||
#include "log.hpp"
|
||||
#include "sdp.hpp"
|
||||
#include "tools.hpp"
|
||||
#include "v2g_ctx.hpp"
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
#include <string_view>
|
||||
|
||||
const std::string CERTS_SUB_DIR = "certs"; // relativ path of the certs
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using BidiMode = types::iso15118::SaeJ2847BidiMode;
|
||||
|
||||
namespace module {
|
||||
namespace charger {
|
||||
|
||||
void ISO15118_chargerImpl::init() {
|
||||
if (!v2g_ctx) {
|
||||
dlog(DLOG_LEVEL_ERROR, "v2g_ctx not created");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Configure if_name and auth_mode */
|
||||
v2g_ctx->if_name = mod->config.device.data();
|
||||
dlog(DLOG_LEVEL_DEBUG, "if_name %s", v2g_ctx->if_name);
|
||||
|
||||
/* Configure hlc_protocols */
|
||||
if (mod->config.supported_DIN70121 == true) {
|
||||
v2g_ctx->supported_protocols |= (1 << V2G_PROTO_DIN70121);
|
||||
supp_app_protocols_secc.app_protocols.push_back(types::iso15118::SupportedAppProtocol::DIN70121);
|
||||
}
|
||||
if (mod->config.supported_ISO15118_2 == true) {
|
||||
v2g_ctx->supported_protocols |= (1 << V2G_PROTO_ISO15118_2013);
|
||||
supp_app_protocols_secc.app_protocols.push_back(types::iso15118::SupportedAppProtocol::ISO15118D2);
|
||||
}
|
||||
|
||||
/* Configure tls_security */
|
||||
if (mod->config.tls_security == "force") {
|
||||
v2g_ctx->tls_security = TLS_SECURITY_FORCE;
|
||||
dlog(DLOG_LEVEL_DEBUG, "tls_security force");
|
||||
} else if (mod->config.tls_security == "allow") {
|
||||
v2g_ctx->tls_security = TLS_SECURITY_ALLOW;
|
||||
dlog(DLOG_LEVEL_DEBUG, "tls_security allow");
|
||||
} else {
|
||||
v2g_ctx->tls_security = TLS_SECURITY_PROHIBIT;
|
||||
dlog(DLOG_LEVEL_DEBUG, "tls_security prohibit");
|
||||
}
|
||||
|
||||
v2g_ctx->terminate_connection_on_failed_response = mod->config.terminate_connection_on_failed_response;
|
||||
|
||||
v2g_ctx->tls_key_logging = mod->config.tls_key_logging;
|
||||
v2g_ctx->tls_key_logging_path = mod->config.tls_key_logging_path;
|
||||
|
||||
if (mod->config.tls_key_logging == true) {
|
||||
dlog(DLOG_LEVEL_DEBUG, "tls-key-logging enabled (path: %s)", mod->config.tls_key_logging_path.c_str());
|
||||
}
|
||||
|
||||
v2g_ctx->network_read_timeout_tls = mod->config.tls_timeout;
|
||||
|
||||
/* Configure if the contract certificate chain should be verified locally */
|
||||
v2g_ctx->session.verify_contract_cert_chain = mod->config.verify_contract_cert_chain;
|
||||
|
||||
v2g_ctx->session.auth_timeout_eim = mod->config.auth_timeout_eim;
|
||||
v2g_ctx->session.auth_timeout_pnc = mod->config.auth_timeout_pnc;
|
||||
|
||||
v2g_ctx->supported_vas_services_per_provider.reserve(mod->r_iso15118_vas.size());
|
||||
|
||||
for (size_t i = 0; i < mod->r_iso15118_vas.size(); i++) {
|
||||
auto& supported_vas_services = v2g_ctx->supported_vas_services_per_provider.emplace_back();
|
||||
|
||||
this->mod->r_iso15118_vas.at(i)->subscribe_offered_vas(
|
||||
[&supported_vas_services](const types::iso15118_vas::OfferedServices& offered_services) {
|
||||
for (auto service : offered_services.services) {
|
||||
|
||||
const auto id = static_cast<uint16_t>(service.service_id);
|
||||
if (id == V2G_SERVICE_ID_CHARGING) {
|
||||
continue;
|
||||
}
|
||||
supported_vas_services.push_back(id);
|
||||
|
||||
iso2_ServiceType vas_service{};
|
||||
init_iso2_ServiceType(&vas_service);
|
||||
vas_service.ServiceID = id;
|
||||
if (service.service_name.has_value()) {
|
||||
strncpy_to_v2g(vas_service.ServiceName.characters, sizeof(vas_service.ServiceName.characters),
|
||||
&vas_service.ServiceName.charactersLen, service.service_name.value());
|
||||
vas_service.ServiceName_isUsed = 1;
|
||||
}
|
||||
if (service.service_scope.has_value()) {
|
||||
strncpy_to_v2g(vas_service.ServiceScope.characters, sizeof(vas_service.ServiceScope.characters),
|
||||
&vas_service.ServiceScope.charactersLen, service.service_scope.value());
|
||||
vas_service.ServiceScope_isUsed = 1;
|
||||
}
|
||||
vas_service.FreeService = service.free_service.value_or(true);
|
||||
|
||||
if (id == V2G_SERVICE_ID_CERTIFICATE) {
|
||||
vas_service.ServiceCategory = iso2_serviceCategoryType_ContractCertificate;
|
||||
} else if (id == V2G_SERVICE_ID_INTERNET) {
|
||||
vas_service.ServiceCategory = iso2_serviceCategoryType_Internet;
|
||||
strncpy_to_v2g(vas_service.ServiceName.characters, sizeof(vas_service.ServiceName.characters),
|
||||
&vas_service.ServiceName.charactersLen, "InternetAccess");
|
||||
vas_service.ServiceName_isUsed = true;
|
||||
} else {
|
||||
vas_service.ServiceCategory = iso2_serviceCategoryType_OtherCustom;
|
||||
}
|
||||
|
||||
if (not add_service_to_service_list(v2g_ctx, vas_service)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::ready() {
|
||||
publish_supported_app_protocols_secc(supp_app_protocols_secc);
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_setup(types::iso15118::EVSEID& evse_id,
|
||||
types::iso15118::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) {
|
||||
|
||||
uint8_t len = evse_id.evse_id.length();
|
||||
if (len < iso2_EVSEID_CHARACTER_SIZE) {
|
||||
memcpy(v2g_ctx->evse_v2g_data.evse_id.bytes, reinterpret_cast<uint8_t*>(evse_id.evse_id.data()), len);
|
||||
v2g_ctx->evse_v2g_data.evse_id.bytesLen = len;
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_WARNING, "EVSEID_CHARACTER_SIZE exceeded (received: %u, max: %u)", len,
|
||||
iso2_EVSEID_CHARACTER_SIZE);
|
||||
}
|
||||
|
||||
v2g_ctx->debugMode = debug_mode;
|
||||
|
||||
if (sae_j2847_mode == BidiMode::V2H || sae_j2847_mode == BidiMode::V2G) {
|
||||
struct iso2_ServiceType sae_service;
|
||||
|
||||
init_iso2_ServiceType(&sae_service);
|
||||
|
||||
sae_service.FreeService = 1;
|
||||
sae_service.ServiceCategory = iso2_serviceCategoryType_OtherCustom;
|
||||
|
||||
if (sae_j2847_mode == BidiMode::V2H) {
|
||||
sae_service.ServiceID = 28472;
|
||||
strncpy_to_v2g(sae_service.ServiceName.characters, sizeof(sae_service.ServiceName.characters),
|
||||
&sae_service.ServiceName.charactersLen, "SAE J2847/2 V2H");
|
||||
sae_service.ServiceName_isUsed = true;
|
||||
} else {
|
||||
sae_service.ServiceID = 28473;
|
||||
strncpy_to_v2g(sae_service.ServiceName.characters, sizeof(sae_service.ServiceName.characters),
|
||||
&sae_service.ServiceName.charactersLen, "SAE J2847/2 V2G");
|
||||
sae_service.ServiceName_isUsed = true;
|
||||
}
|
||||
|
||||
add_service_to_service_list(v2g_ctx, sae_service);
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_set_charging_parameters(types::iso15118::SetupPhysicalValues& physical_values) {
|
||||
if (physical_values.ac_nominal_voltage.has_value()) {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_nominal_voltage,
|
||||
physical_values.ac_nominal_voltage.value(), 1, iso2_unitSymbolType_V);
|
||||
v2g_ctx->basic_config.evse_ac_nominal_voltage = physical_values.ac_nominal_voltage.value();
|
||||
}
|
||||
|
||||
if (physical_values.dc_current_regulation_tolerance.has_value()) {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_current_regulation_tolerance,
|
||||
physical_values.dc_current_regulation_tolerance.value(), 1,
|
||||
iso2_unitSymbolType_A);
|
||||
v2g_ctx->evse_v2g_data.evse_current_regulation_tolerance_is_used = 1;
|
||||
}
|
||||
|
||||
if (physical_values.dc_peak_current_ripple.has_value()) {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_peak_current_ripple,
|
||||
physical_values.dc_peak_current_ripple.value(), 1, iso2_unitSymbolType_A);
|
||||
}
|
||||
|
||||
if (physical_values.dc_energy_to_be_delivered.has_value()) {
|
||||
populate_physical_value(&v2g_ctx->evse_v2g_data.evse_energy_to_be_delivered,
|
||||
physical_values.dc_energy_to_be_delivered.value(), iso2_unitSymbolType_Wh);
|
||||
v2g_ctx->evse_v2g_data.evse_energy_to_be_delivered_is_used = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_session_setup(std::vector<types::iso15118::PaymentOption>& payment_options,
|
||||
bool& supported_certificate_service,
|
||||
bool& central_contract_validation_allowed) {
|
||||
if (not v2g_ctx->hlc_pause_active) {
|
||||
v2g_ctx->evse_v2g_data.payment_option_list.clear();
|
||||
if (not payment_options.empty()) {
|
||||
const auto max_payment_options =
|
||||
std::min(static_cast<size_t>(iso2_paymentOptionType_2_ARRAY_SIZE), payment_options.size());
|
||||
for (size_t i = 0; i < max_payment_options; i++) {
|
||||
const auto& payment_option = payment_options.at(i);
|
||||
if (payment_option == types::iso15118::PaymentOption::ExternalPayment) {
|
||||
v2g_ctx->evse_v2g_data.payment_option_list.push_back(iso2_paymentOptionType_ExternalPayment);
|
||||
} else if (payment_option == types::iso15118::PaymentOption::Contract) {
|
||||
v2g_ctx->evse_v2g_data.payment_option_list.push_back(iso2_paymentOptionType_Contract);
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_WARNING, "Unable to configure PaymentOption %s",
|
||||
types::iso15118::payment_option_to_string(payment_option).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (payment_options.empty() or v2g_ctx->evse_v2g_data.payment_option_list.empty()) {
|
||||
dlog(DLOG_LEVEL_ERROR, "No valid PaymentOptions configured, falling back to ExternalPayment");
|
||||
v2g_ctx->evse_v2g_data.payment_option_list.clear();
|
||||
v2g_ctx->evse_v2g_data.payment_option_list.push_back(iso2_paymentOptionType_ExternalPayment);
|
||||
}
|
||||
}
|
||||
|
||||
const auto pnc_enabled =
|
||||
std::find(v2g_ctx->evse_v2g_data.payment_option_list.begin(), v2g_ctx->evse_v2g_data.payment_option_list.end(),
|
||||
iso2_paymentOptionType_Contract) != v2g_ctx->evse_v2g_data.payment_option_list.end();
|
||||
|
||||
using state_t = tls::Server::state_t;
|
||||
const auto tls_server_state = v2g_ctx->tls_server->state();
|
||||
|
||||
const auto tls_server_available =
|
||||
(tls_server_state == state_t::init_complete or tls_server_state == state_t::running);
|
||||
|
||||
if (pnc_enabled and supported_certificate_service and tls_server_available) {
|
||||
// For setting "Certificate" in ServiceList in ServiceDiscoveryRes
|
||||
struct iso2_ServiceType cert_service;
|
||||
|
||||
init_iso2_ServiceType(&cert_service);
|
||||
|
||||
const int16_t cert_parameter_set_id[] = {1}; // parameter-set-ID 1: "Installation" service. TODO: Support of the
|
||||
// "Update" service (parameter-set-ID 2)
|
||||
|
||||
cert_service.FreeService = 1; // true
|
||||
cert_service.ServiceID = 2; // as defined in ISO 15118-2
|
||||
cert_service.ServiceCategory = iso2_serviceCategoryType_ContractCertificate;
|
||||
strncpy_to_v2g(cert_service.ServiceName.characters, sizeof(cert_service.ServiceName.characters),
|
||||
&cert_service.ServiceName.charactersLen, "Certificate");
|
||||
cert_service.ServiceName_isUsed = true;
|
||||
|
||||
add_service_to_service_list(v2g_ctx, cert_service, cert_parameter_set_id,
|
||||
sizeof(cert_parameter_set_id) / sizeof(cert_parameter_set_id[0]));
|
||||
} else {
|
||||
// Make sure Certificate service is not in ServiceList when pnc is not possible
|
||||
remove_service_from_service_list_if_exists(v2g_ctx, V2G_SERVICE_ID_CERTIFICATE);
|
||||
}
|
||||
|
||||
// Reset authorization that may have been provided after the TCP was closed, potentially causing
|
||||
// authorization to be reused in the next session. Resetting it here prevents that.
|
||||
v2g_ctx->evse_v2g_data.evse_processing[PHASE_AUTH] = (uint8_t)iso2_EVSEProcessingType_Ongoing;
|
||||
|
||||
v2g_ctx->evse_v2g_data.central_contract_validation_allowed = central_contract_validation_allowed;
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_bpt_setup(types::iso15118::BptSetup& bpt_config) {
|
||||
EVLOG_warning << "Ignoring handle_bpt_setup call";
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_set_powersupply_capabilities(types::power_supply_DC::Capabilities& capabilities) {
|
||||
|
||||
const auto max_export_current =
|
||||
capabilities.nominal_max_export_current_A.value_or(capabilities.max_export_current_A);
|
||||
const auto min_export_current =
|
||||
capabilities.nominal_min_export_current_A.value_or(capabilities.min_export_current_A);
|
||||
const auto max_export_power = capabilities.nominal_max_export_power_W.value_or(capabilities.max_export_power_W);
|
||||
const auto max_export_voltage =
|
||||
capabilities.nominal_max_export_voltage_V.value_or(capabilities.max_export_voltage_V);
|
||||
const auto min_export_voltage =
|
||||
capabilities.nominal_min_export_voltage_V.value_or(capabilities.min_export_voltage_V);
|
||||
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.power_capabilities.max_current, max_export_current, 1,
|
||||
iso2_unitSymbolType_A);
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.power_capabilities.min_current, min_export_current, 1,
|
||||
iso2_unitSymbolType_A);
|
||||
populate_physical_value(&v2g_ctx->evse_v2g_data.power_capabilities.max_power,
|
||||
static_cast<uint32_t>(max_export_power), iso2_unitSymbolType_W);
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.power_capabilities.max_voltage, max_export_voltage, 1,
|
||||
iso2_unitSymbolType_V);
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.power_capabilities.min_voltage, min_export_voltage, 1,
|
||||
iso2_unitSymbolType_V);
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_authorization_response(
|
||||
types::authorization::AuthorizationStatus& authorization_status,
|
||||
types::authorization::CertificateStatus& certificate_status) {
|
||||
|
||||
v2g_ctx->evse_v2g_data.evse_processing[PHASE_AUTH] = static_cast<uint8_t>(iso2_EVSEProcessingType_Finished);
|
||||
|
||||
if (authorization_status != types::authorization::AuthorizationStatus::Accepted) {
|
||||
v2g_ctx->session.authorization_rejected = true;
|
||||
}
|
||||
|
||||
if (v2g_ctx->session.iso_selected_payment_option == iso2_paymentOptionType_Contract) {
|
||||
v2g_ctx->session.certificate_status = certificate_status;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_ac_contactor_closed(bool& status) {
|
||||
/* signal changes to possible waiters, according to man page, it never returns an error code */
|
||||
pthread_mutex_lock(&v2g_ctx->mqtt_lock);
|
||||
v2g_ctx->contactor_is_closed = status;
|
||||
pthread_cond_signal(&v2g_ctx->mqtt_cond);
|
||||
/* unlock */
|
||||
pthread_mutex_unlock(&v2g_ctx->mqtt_lock);
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_dlink_ready(bool& value) {
|
||||
sdp_set_dlink_ready(v2g_ctx, value);
|
||||
|
||||
// If dlink becomes not ready (false), stop TCP connection in the read thread
|
||||
if (!value) {
|
||||
v2g_ctx->is_connection_terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_cable_check_finished(bool& status) {
|
||||
if (status == true) {
|
||||
v2g_ctx->evse_v2g_data.evse_processing[PHASE_ISOLATION] = (uint8_t)iso2_EVSEProcessingType_Finished;
|
||||
} else {
|
||||
v2g_ctx->evse_v2g_data.evse_processing[PHASE_ISOLATION] = (uint8_t)iso2_EVSEProcessingType_Ongoing;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_receipt_is_required(bool& receipt_required) {
|
||||
v2g_ctx->evse_v2g_data.receipt_required = (int)receipt_required;
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_stop_charging(bool& stop) {
|
||||
// FIXME we need to use locks on v2g-ctx in all commands as they are running in different threads
|
||||
|
||||
if (stop) {
|
||||
// spawn new thread to not block command handler
|
||||
std::thread([stop] {
|
||||
// try to gracefully shutdown charging session
|
||||
v2g_ctx->evse_v2g_data.evse_notification = iso2_EVSENotificationType_StopCharging;
|
||||
memset(v2g_ctx->evse_v2g_data.evse_status_code, iso2_DC_EVSEStatusCodeType_EVSE_Shutdown,
|
||||
sizeof(v2g_ctx->evse_v2g_data.evse_status_code));
|
||||
|
||||
int i;
|
||||
bool timeout_reached = true;
|
||||
// allow 10 seconds for graceful shutdown
|
||||
for (i = 0; i < 10; i++) {
|
||||
if (v2g_ctx->is_connection_terminated) {
|
||||
timeout_reached = false;
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(1s);
|
||||
}
|
||||
|
||||
// If it did not stop within timeout, stop session using FAILED reply
|
||||
if (timeout_reached) {
|
||||
v2g_ctx->stop_hlc = stop;
|
||||
}
|
||||
}).detach();
|
||||
} else {
|
||||
v2g_ctx->stop_hlc = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_pause_charging(bool& pause) {
|
||||
EVLOG_warning << "Pause initialized by the charger is not supported in DIN70121 and ISO15118-2";
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_no_energy_pause_charging(types::iso15118::NoEnergyPauseMode& mode) {
|
||||
|
||||
switch (mode) {
|
||||
case types::iso15118::NoEnergyPauseMode::PauseAfterPrecharge:
|
||||
v2g_ctx->evse_v2g_data.no_energy_pause = NoEnergyPauseStatus::AfterCableCheckPreCharge;
|
||||
break;
|
||||
|
||||
case types::iso15118::NoEnergyPauseMode::PauseBeforeCableCheck:
|
||||
v2g_ctx->evse_v2g_data.no_energy_pause = NoEnergyPauseStatus::BeforeCableCheck;
|
||||
break;
|
||||
|
||||
case types::iso15118::NoEnergyPauseMode::AllowEvToIgnorePause:
|
||||
v2g_ctx->evse_v2g_data.no_energy_pause = NoEnergyPauseStatus::AllowEvToIgnorePause;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool ISO15118_chargerImpl::handle_update_supported_app_protocols(
|
||||
types::iso15118::SupportedAppProtocols& supported_app_protocols) {
|
||||
bool rv{true};
|
||||
v2g_ctx->supported_protocols = 0;
|
||||
|
||||
if (supported_app_protocols.app_protocols.empty()) {
|
||||
dlog(DLOG_LEVEL_WARNING, "No supported app protocols configured");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string configured_protocols;
|
||||
|
||||
for (const auto& protocol : supported_app_protocols.app_protocols) {
|
||||
if (!configured_protocols.empty()) {
|
||||
configured_protocols += ", ";
|
||||
}
|
||||
configured_protocols += types::iso15118::supported_app_protocol_to_string(protocol);
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "Configured charging protocols: [%s]", configured_protocols.c_str());
|
||||
|
||||
for (const auto& protocol : supported_app_protocols.app_protocols) {
|
||||
// Check if the supported app protocol is in the SECC list
|
||||
const bool allowed = std::find(this->supp_app_protocols_secc.app_protocols.begin(),
|
||||
this->supp_app_protocols_secc.app_protocols.end(),
|
||||
protocol) != this->supp_app_protocols_secc.app_protocols.end();
|
||||
|
||||
if (!allowed) {
|
||||
dlog(DLOG_LEVEL_WARNING, "Skip unsupported app protocol: %s",
|
||||
types::iso15118::supported_app_protocol_to_string(protocol).c_str());
|
||||
rv = false;
|
||||
continue;
|
||||
}
|
||||
/* Configure supported app bitmask. This bitmark is used in the supportedAppHandshake handle
|
||||
to select the protocol */
|
||||
switch (protocol) {
|
||||
case types::iso15118::SupportedAppProtocol::DIN70121:
|
||||
v2g_ctx->supported_protocols |= (1 << V2G_PROTO_DIN70121);
|
||||
break;
|
||||
case types::iso15118::SupportedAppProtocol::ISO15118D2:
|
||||
v2g_ctx->supported_protocols |= (1 << V2G_PROTO_ISO15118_2013);
|
||||
break;
|
||||
case types::iso15118::SupportedAppProtocol::ISO15118D20:
|
||||
default:
|
||||
dlog(DLOG_LEVEL_WARNING, "Unsupported app protocol: %s",
|
||||
types::iso15118::supported_app_protocol_to_string(protocol).c_str());
|
||||
rv = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (v2g_ctx->supported_protocols == 0) {
|
||||
dlog(DLOG_LEVEL_WARNING, "No supported app protocols provided");
|
||||
rv = false;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_energy_transfer_modes(
|
||||
std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) {
|
||||
if (v2g_ctx->hlc_pause_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t& energyArrayLen =
|
||||
(v2g_ctx->evse_v2g_data.charge_service.SupportedEnergyTransferMode.EnergyTransferMode.arrayLen);
|
||||
iso2_EnergyTransferModeType* energyArray =
|
||||
v2g_ctx->evse_v2g_data.charge_service.SupportedEnergyTransferMode.EnergyTransferMode.array;
|
||||
energyArrayLen = 0;
|
||||
|
||||
v2g_ctx->is_dc_charger = true;
|
||||
|
||||
const auto max_supported_energy_modes =
|
||||
std::min(static_cast<size_t>(iso2_EnergyTransferModeType_6_ARRAY_SIZE), supported_energy_transfer_modes.size());
|
||||
|
||||
for (size_t i = 0; i < max_supported_energy_modes; i++) {
|
||||
const auto& mode = supported_energy_transfer_modes.at(i);
|
||||
switch (mode) {
|
||||
case types::iso15118::EnergyTransferMode::AC_single_phase_core:
|
||||
energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_AC_single_phase_core;
|
||||
v2g_ctx->is_dc_charger = false;
|
||||
break;
|
||||
case types::iso15118::EnergyTransferMode::AC_three_phase_core:
|
||||
energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_AC_three_phase_core;
|
||||
v2g_ctx->is_dc_charger = false;
|
||||
break;
|
||||
case types::iso15118::EnergyTransferMode::DC_core:
|
||||
energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_DC_core;
|
||||
break;
|
||||
case types::iso15118::EnergyTransferMode::DC_extended:
|
||||
energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_DC_extended;
|
||||
break;
|
||||
case types::iso15118::EnergyTransferMode::DC_combo_core:
|
||||
energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_DC_combo_core;
|
||||
break;
|
||||
case types::iso15118::EnergyTransferMode::DC_unique:
|
||||
energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_DC_unique;
|
||||
break;
|
||||
case types::iso15118::EnergyTransferMode::DC_ACDP_BPT:
|
||||
case types::iso15118::EnergyTransferMode::AC_BPT:
|
||||
case types::iso15118::EnergyTransferMode::AC_BPT_DER:
|
||||
case types::iso15118::EnergyTransferMode::DC_BPT:
|
||||
dlog(DLOG_LEVEL_INFO, "Ignoring bidirectional SupportedEnergyTransferMode");
|
||||
default:
|
||||
if (energyArrayLen == 0) {
|
||||
|
||||
dlog(DLOG_LEVEL_WARNING, "Unable to configure SupportedEnergyTransferMode %s",
|
||||
types::iso15118::energy_transfer_mode_to_string(mode).c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mod->config.supported_DIN70121 and not v2g_ctx->is_dc_charger) {
|
||||
v2g_ctx->supported_protocols &= ~(1 << V2G_PROTO_DIN70121);
|
||||
dlog(DLOG_LEVEL_WARNING, "Removed DIN70121 from the list of supported protocols as AC is enabled");
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_ac_max_current(double& max_current) {
|
||||
v2g_ctx->basic_config.evse_ac_current_limit = max_current;
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_ac_parameters(types::iso15118::AcParameters& ac_parameters) {
|
||||
static bool warning_shown = false;
|
||||
if (not warning_shown) {
|
||||
EVLOG_warning << "Ignoring handle_update_ac_parameters call";
|
||||
warning_shown = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_ac_maximum_limits(types::iso15118::AcEvseMaximumPower& maximum_limits) {
|
||||
if (v2g_ctx->basic_config.evse_ac_nominal_voltage > 0.0f) {
|
||||
v2g_ctx->basic_config.evse_ac_nominal_current =
|
||||
maximum_limits.charge_power.total / v2g_ctx->basic_config.evse_ac_nominal_voltage;
|
||||
} else {
|
||||
EVLOG_error << "Cannot compute nominal current: nominal voltage is 0";
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_ac_minimum_limits(types::iso15118::AcEvseMinimumPower& minimum_limits) {
|
||||
static bool warning_shown = false;
|
||||
if (not warning_shown) {
|
||||
EVLOG_warning << "Ignoring handle_update_ac_minimum_limits call";
|
||||
warning_shown = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_ac_target_values(types::iso15118::AcTargetValues& target_values) {
|
||||
static bool warning_shown = false;
|
||||
if (not warning_shown) {
|
||||
EVLOG_warning << "Ignoring handle_update_ac_target_values call";
|
||||
warning_shown = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_ac_present_power(types::units::Power& present_power) {
|
||||
static bool warning_shown = false;
|
||||
if (not warning_shown) {
|
||||
EVLOG_warning << "Ignoring handle_update_ac_present_power call";
|
||||
warning_shown = true;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_dc_maximum_limits(types::iso15118::DcEvseMaximumLimits& maximum_limits) {
|
||||
if (maximum_limits.evse_maximum_current_limit > 300.) {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_maximum_current_limit,
|
||||
maximum_limits.evse_maximum_current_limit, 1, iso2_unitSymbolType_A);
|
||||
} else {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_maximum_current_limit,
|
||||
maximum_limits.evse_maximum_current_limit, 2, iso2_unitSymbolType_A);
|
||||
}
|
||||
v2g_ctx->evse_v2g_data.evse_maximum_current_limit_is_used = 1;
|
||||
|
||||
populate_physical_value(&v2g_ctx->evse_v2g_data.evse_maximum_power_limit, maximum_limits.evse_maximum_power_limit,
|
||||
iso2_unitSymbolType_W);
|
||||
v2g_ctx->evse_v2g_data.evse_maximum_power_limit_is_used = 1;
|
||||
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_maximum_voltage_limit,
|
||||
maximum_limits.evse_maximum_voltage_limit, 1, iso2_unitSymbolType_V);
|
||||
v2g_ctx->evse_v2g_data.evse_maximum_voltage_limit_is_used = 1;
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_dc_minimum_limits(types::iso15118::DcEvseMinimumLimits& minimum_limits) {
|
||||
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_minimum_current_limit,
|
||||
static_cast<long long int>(minimum_limits.evse_minimum_current_limit), 1,
|
||||
iso2_unitSymbolType_A);
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_minimum_voltage_limit,
|
||||
static_cast<long long int>(minimum_limits.evse_minimum_voltage_limit), 1,
|
||||
iso2_unitSymbolType_V);
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_isolation_status(types::iso15118::IsolationStatus& isolation_status) {
|
||||
v2g_ctx->evse_v2g_data.evse_isolation_status = (uint8_t)isolation_status;
|
||||
v2g_ctx->evse_v2g_data.evse_isolation_status_is_used = 1;
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_dc_present_values(
|
||||
types::iso15118::DcEvsePresentVoltageCurrent& present_voltage_current) {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_present_voltage,
|
||||
present_voltage_current.evse_present_voltage, 1, iso2_unitSymbolType_V);
|
||||
|
||||
if (present_voltage_current.evse_present_current.has_value()) {
|
||||
populate_physical_value_float(&v2g_ctx->evse_v2g_data.evse_present_current,
|
||||
static_cast<float>(present_voltage_current.evse_present_current.value()), 1,
|
||||
iso2_unitSymbolType_A);
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_update_meter_info(types::powermeter::Powermeter& powermeter) {
|
||||
// Signal for ChargingStatus and CurrentDemand that MeterInfo is used
|
||||
v2g_ctx->meter_info.meter_info_is_used = 1;
|
||||
v2g_ctx->meter_info.meter_reading = powermeter.energy_Wh_import.total;
|
||||
|
||||
if (powermeter.meter_id) {
|
||||
uint8_t len = powermeter.meter_id->length();
|
||||
if (len < iso2_MeterID_CHARACTER_SIZE) {
|
||||
memcpy(v2g_ctx->meter_info.meter_id.bytes, powermeter.meter_id->c_str(), len);
|
||||
v2g_ctx->meter_info.meter_id.bytesLen = len;
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_WARNING, "MeterID_CHARACTER_SIZEexceeded (received: %u, max: %u)", len,
|
||||
iso2_MeterID_CHARACTER_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_send_error(types::iso15118::EvseError& error) {
|
||||
switch (error) {
|
||||
case types::iso15118::EvseError::Error_Contactor:
|
||||
break;
|
||||
case types::iso15118::EvseError::Error_RCD:
|
||||
v2g_ctx->evse_v2g_data.rcd = 1;
|
||||
break;
|
||||
case types::iso15118::EvseError::Error_UtilityInterruptEvent:
|
||||
memset(v2g_ctx->evse_v2g_data.evse_status_code, (int)iso2_DC_EVSEStatusCodeType_EVSE_UtilityInterruptEvent,
|
||||
sizeof(v2g_ctx->evse_v2g_data.evse_status_code));
|
||||
break;
|
||||
case types::iso15118::EvseError::Error_Malfunction:
|
||||
memset(v2g_ctx->evse_v2g_data.evse_status_code, (int)iso2_DC_EVSEStatusCodeType_EVSE_Malfunction,
|
||||
sizeof(v2g_ctx->evse_v2g_data.evse_status_code));
|
||||
break;
|
||||
case types::iso15118::EvseError::Error_EmergencyShutdown:
|
||||
/* signal changes to possible waiters, according to man page, it never returns an error code */
|
||||
pthread_mutex_lock(&v2g_ctx->mqtt_lock);
|
||||
v2g_ctx->intl_emergency_shutdown = true;
|
||||
pthread_cond_signal(&v2g_ctx->mqtt_cond);
|
||||
/* unlock */
|
||||
pthread_mutex_unlock(&v2g_ctx->mqtt_lock);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ISO15118_chargerImpl::handle_reset_error() {
|
||||
v2g_ctx->evse_v2g_data.rcd = 0;
|
||||
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_INIT] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_AUTH] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_PARAMETER] = iso2_DC_EVSEStatusCodeType_EVSE_Ready; // [V2G-DC-453]
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_ISOLATION] =
|
||||
iso2_DC_EVSEStatusCodeType_EVSE_IsolationMonitoringActive;
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_PRECHARGE] = iso2_DC_EVSEStatusCodeType_EVSE_Ready;
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_CHARGE] = iso2_DC_EVSEStatusCodeType_EVSE_Ready;
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_WELDING] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
v2g_ctx->evse_v2g_data.evse_status_code[PHASE_STOP] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
|
||||
// Todo(sl): check if emergency should be cleared here?
|
||||
}
|
||||
|
||||
} // namespace charger
|
||||
} // namespace module
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CHARGER_ISO15118_CHARGER_IMPL_HPP
|
||||
#define CHARGER_ISO15118_CHARGER_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/ISO15118_charger/Implementation.hpp>
|
||||
|
||||
#include "../EvseV2G.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
#include "v2g.hpp"
|
||||
extern struct v2g_context* v2g_ctx;
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace charger {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class ISO15118_chargerImpl : public ISO15118_chargerImplBase {
|
||||
public:
|
||||
ISO15118_chargerImpl() = delete;
|
||||
ISO15118_chargerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EvseV2G>& mod, Conf& config) :
|
||||
ISO15118_chargerImplBase(ev, "charger"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void handle_setup(types::iso15118::EVSEID& evse_id, types::iso15118::SaeJ2847BidiMode& sae_j2847_mode,
|
||||
bool& debug_mode) override;
|
||||
virtual void handle_set_charging_parameters(types::iso15118::SetupPhysicalValues& physical_values) override;
|
||||
virtual void handle_session_setup(std::vector<types::iso15118::PaymentOption>& payment_options,
|
||||
bool& supported_certificate_service,
|
||||
bool& central_contract_validation_allowed) override;
|
||||
virtual void handle_bpt_setup(types::iso15118::BptSetup& bpt_config) override;
|
||||
virtual void handle_set_powersupply_capabilities(types::power_supply_DC::Capabilities& capabilities) override;
|
||||
virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status,
|
||||
types::authorization::CertificateStatus& certificate_status) override;
|
||||
virtual void handle_ac_contactor_closed(bool& status) override;
|
||||
virtual void handle_dlink_ready(bool& value) override;
|
||||
virtual void handle_cable_check_finished(bool& status) override;
|
||||
virtual void handle_receipt_is_required(bool& receipt_required) override;
|
||||
virtual void handle_stop_charging(bool& stop) override;
|
||||
virtual void handle_pause_charging(bool& pause) override;
|
||||
virtual void handle_no_energy_pause_charging(types::iso15118::NoEnergyPauseMode& mode) override;
|
||||
virtual bool
|
||||
handle_update_supported_app_protocols(types::iso15118::SupportedAppProtocols& supported_app_protocols) override;
|
||||
virtual void handle_update_energy_transfer_modes(
|
||||
std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) override;
|
||||
virtual void handle_update_ac_max_current(double& max_current) override;
|
||||
virtual void handle_update_ac_parameters(types::iso15118::AcParameters& ac_parameters) override;
|
||||
virtual void handle_update_ac_maximum_limits(types::iso15118::AcEvseMaximumPower& maximum_limits) override;
|
||||
virtual void handle_update_ac_minimum_limits(types::iso15118::AcEvseMinimumPower& minimum_limits) override;
|
||||
virtual void handle_update_ac_target_values(types::iso15118::AcTargetValues& target_values) override;
|
||||
virtual void handle_update_ac_present_power(types::units::Power& present_power) override;
|
||||
virtual void handle_update_dc_maximum_limits(types::iso15118::DcEvseMaximumLimits& maximum_limits) override;
|
||||
virtual void handle_update_dc_minimum_limits(types::iso15118::DcEvseMinimumLimits& minimum_limits) override;
|
||||
virtual void handle_update_isolation_status(types::iso15118::IsolationStatus& isolation_status) override;
|
||||
virtual void
|
||||
handle_update_dc_present_values(types::iso15118::DcEvsePresentVoltageCurrent& present_voltage_current) override;
|
||||
virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) override;
|
||||
virtual void handle_send_error(types::iso15118::EvseError& error) override;
|
||||
virtual void handle_reset_error() override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<EvseV2G>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
types::iso15118::SupportedAppProtocols supp_app_protocols_secc;
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace charger
|
||||
} // namespace module
|
||||
|
||||
#endif // CHARGER_ISO15118_CHARGER_IMPL_HPP
|
||||
@@ -0,0 +1,691 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
|
||||
#include "connection.hpp"
|
||||
#include "log.hpp"
|
||||
#include "tls_connection.hpp"
|
||||
#include "tools.hpp"
|
||||
#include "v2g_server.hpp"
|
||||
|
||||
#include <generated/types/evse_manager.hpp>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cstring>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fstream>
|
||||
#include <inttypes.h>
|
||||
#include <iostream>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DEFAULT_SOCKET_BACKLOG 3
|
||||
#define DEFAULT_TCP_PORT 61341
|
||||
#define DEFAULT_TLS_PORT 64109
|
||||
#define ERROR_SESSION_ALREADY_STARTED 2
|
||||
#define CLIENT_FIN_TIMEOUT 3000
|
||||
|
||||
/*!
|
||||
* \brief connection_create_socket This function creates a tcp/tls socket
|
||||
* \param sockaddr to bind the socket to an interface
|
||||
* \return Returns \c 0 on success, otherwise \c -1
|
||||
*/
|
||||
static int connection_create_socket(struct sockaddr_in6* sockaddr) {
|
||||
socklen_t addrlen = sizeof(*sockaddr);
|
||||
int s, enable = 1;
|
||||
static bool error_once = false;
|
||||
|
||||
/* create socket */
|
||||
s = socket(AF_INET6, SOCK_STREAM, 0);
|
||||
if (s == -1) {
|
||||
if (!error_once) {
|
||||
dlog(DLOG_LEVEL_ERROR, "socket() failed: %s", strerror(errno));
|
||||
error_once = true;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(enable)) == -1) {
|
||||
if (!error_once) {
|
||||
dlog(DLOG_LEVEL_ERROR, "setsockopt(SO_REUSEPORT) failed: %s", strerror(errno));
|
||||
error_once = true;
|
||||
}
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* bind it to interface */
|
||||
if (bind(s, reinterpret_cast<struct sockaddr*>(sockaddr), addrlen) == -1) {
|
||||
if (!error_once) {
|
||||
dlog(DLOG_LEVEL_WARNING, "bind() failed: %s", strerror(errno));
|
||||
dlog(DLOG_LEVEL_WARNING,
|
||||
"Verify that the configured interface has a valid IPv6 link local address configured.");
|
||||
error_once = true;
|
||||
}
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* listen on this socket */
|
||||
if (listen(s, DEFAULT_SOCKET_BACKLOG) == -1) {
|
||||
if (!error_once) {
|
||||
dlog(DLOG_LEVEL_ERROR, "listen() failed: %s", strerror(errno));
|
||||
error_once = true;
|
||||
}
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* retrieve the actual port number we are listening on */
|
||||
if (getsockname(s, reinterpret_cast<struct sockaddr*>(sockaddr), &addrlen) == -1) {
|
||||
if (!error_once) {
|
||||
dlog(DLOG_LEVEL_ERROR, "getsockname() failed: %s", strerror(errno));
|
||||
error_once = true;
|
||||
}
|
||||
close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief check_interface This function checks the interface name. The interface name is
|
||||
* configured automatically in case it is pre-initialized to “auto.
|
||||
* \param sockaddr to bind the socket to an interface
|
||||
* \return Returns \c 0 on success, otherwise \c -1
|
||||
*/
|
||||
int check_interface(struct v2g_context* v2g_ctx) {
|
||||
if (v2g_ctx == nullptr || v2g_ctx->if_name == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct ipv6_mreq mreq = {};
|
||||
std::memset(&mreq, 0, sizeof(mreq));
|
||||
|
||||
if (strcmp(v2g_ctx->if_name, "auto") == 0) {
|
||||
v2g_ctx->if_name = choose_first_ipv6_interface();
|
||||
}
|
||||
|
||||
if (v2g_ctx->if_name == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mreq.ipv6mr_interface = if_nametoindex(v2g_ctx->if_name);
|
||||
if (!mreq.ipv6mr_interface) {
|
||||
dlog(DLOG_LEVEL_ERROR, "No such interface: %s", v2g_ctx->if_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (v2g_ctx->if_name == nullptr) ? -1 : 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief connection_init This function initilizes the tcp and tls interface.
|
||||
* \param v2g_context is the V2G context.
|
||||
* \return Returns \c 0 on success, otherwise \c -1
|
||||
*/
|
||||
int connection_init(struct v2g_context* v2g_ctx) {
|
||||
if (check_interface(v2g_ctx) == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (v2g_ctx->tls_security != TLS_SECURITY_FORCE) {
|
||||
v2g_ctx->local_tcp_addr = static_cast<sockaddr_in6*>(calloc(1, sizeof(*v2g_ctx->local_tcp_addr)));
|
||||
if (v2g_ctx->local_tcp_addr == nullptr) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory for TCP address");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (v2g_ctx->tls_security != TLS_SECURITY_PROHIBIT) {
|
||||
v2g_ctx->local_tls_addr = static_cast<sockaddr_in6*>(calloc(1, sizeof(*v2g_ctx->local_tls_addr)));
|
||||
if (!v2g_ctx->local_tls_addr) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory for TLS address");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
while (1) {
|
||||
if (v2g_ctx->local_tcp_addr) {
|
||||
get_interface_ipv6_address(v2g_ctx->if_name, ADDR6_TYPE_LINKLOCAL, v2g_ctx->local_tcp_addr);
|
||||
if (v2g_ctx->local_tls_addr) {
|
||||
// Handle allowing TCP with TLS (TLS_SECURITY_ALLOW)
|
||||
memcpy(v2g_ctx->local_tls_addr, v2g_ctx->local_tcp_addr, sizeof(*v2g_ctx->local_tls_addr));
|
||||
}
|
||||
} else {
|
||||
// Handle forcing TLS security (TLS_SECURITY_FORCE)
|
||||
get_interface_ipv6_address(v2g_ctx->if_name, ADDR6_TYPE_LINKLOCAL, v2g_ctx->local_tls_addr);
|
||||
}
|
||||
|
||||
if (v2g_ctx->local_tcp_addr) {
|
||||
char buffer[INET6_ADDRSTRLEN];
|
||||
|
||||
/*
|
||||
* When we bind with port = 0, the kernel assigns a dynamic port from the range configured
|
||||
* in /proc/sys/net/ipv4/ip_local_port_range. This is on a recent Ubuntu Linux e.g.
|
||||
* $ cat /proc/sys/net/ipv4/ip_local_port_range
|
||||
* 32768 60999
|
||||
* However, in ISO15118 spec the IANA range with 49152 to 65535 is referenced. So we have the
|
||||
* problem that the kernel (without further configuration - and we want to avoid this) could
|
||||
* hand out a port which is not "range compatible".
|
||||
* To fulfill the ISO15118 standard, we simply try to bind to static port numbers.
|
||||
*/
|
||||
v2g_ctx->local_tcp_addr->sin6_port = htons(DEFAULT_TCP_PORT);
|
||||
v2g_ctx->tcp_socket = connection_create_socket(v2g_ctx->local_tcp_addr);
|
||||
if (v2g_ctx->tcp_socket < 0) {
|
||||
/* retry until interface is ready */
|
||||
sleep(1);
|
||||
continue;
|
||||
}
|
||||
if (inet_ntop(AF_INET6, &v2g_ctx->local_tcp_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) {
|
||||
dlog(DLOG_LEVEL_INFO, "TCP server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16,
|
||||
v2g_ctx->if_name, buffer, v2g_ctx->local_tcp_addr->sin6_scope_id,
|
||||
ntohs(v2g_ctx->local_tcp_addr->sin6_port));
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_ERROR, "TCP server on %s is listening, but inet_ntop failed: %s", v2g_ctx->if_name,
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (v2g_ctx->local_tls_addr) {
|
||||
char buffer[INET6_ADDRSTRLEN];
|
||||
|
||||
/* see comment above for reason */
|
||||
v2g_ctx->local_tls_addr->sin6_port = htons(DEFAULT_TLS_PORT);
|
||||
|
||||
v2g_ctx->tls_socket.fd = connection_create_socket(v2g_ctx->local_tls_addr);
|
||||
if (v2g_ctx->tls_socket.fd < 0) {
|
||||
if (v2g_ctx->tcp_socket != -1) {
|
||||
/* free the TCP socket */
|
||||
close(v2g_ctx->tcp_socket);
|
||||
}
|
||||
/* retry until interface is ready */
|
||||
sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inet_ntop(AF_INET6, &v2g_ctx->local_tls_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) {
|
||||
dlog(DLOG_LEVEL_INFO, "TLS server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16,
|
||||
v2g_ctx->if_name, buffer, v2g_ctx->local_tls_addr->sin6_scope_id,
|
||||
ntohs(v2g_ctx->local_tls_addr->sin6_port));
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_INFO, "TLS server on %s is listening, but inet_ntop failed: %s", v2g_ctx->if_name,
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
/* Sockets should be ready, leave the loop */
|
||||
break;
|
||||
}
|
||||
|
||||
if (v2g_ctx->local_tls_addr) {
|
||||
return tls::connection_init(v2g_ctx);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief is_sequence_timeout This function checks if a sequence timeout has occurred.
|
||||
* \param ts_start Is the time after waiting of the next request message.
|
||||
* \param ctx is the V2G context.
|
||||
* \return Returns \c true if a timeout has occurred, otherwise \c false
|
||||
*/
|
||||
bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx) {
|
||||
struct timespec ts_current;
|
||||
int sequence_timeout = V2G_SEQUENCE_TIMEOUT_60S;
|
||||
|
||||
if (((clock_gettime(CLOCK_MONOTONIC, &ts_current)) != 0) ||
|
||||
(timespec_to_ms(timespec_sub(ts_current, ts_start)) > sequence_timeout)) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Sequence timeout has occurred (message: %s)", v2g_msg_type[ctx->current_v2g_msg]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief connection_read This function reads from socket until requested bytes are received or sequence
|
||||
* timeout is reached
|
||||
* \param conn is the v2g connection context
|
||||
* \param buf is the buffer to store the v2g message
|
||||
* \param count is the number of bytes to read
|
||||
* \return Returns \c true if a timeout has occurred, otherwise \c false
|
||||
*/
|
||||
ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t count) {
|
||||
struct timespec ts_start;
|
||||
int bytes_read = 0;
|
||||
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "clock_gettime(ts_start) failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* loop until we got all requested bytes or sequence timeout DIN [V2G-DC-432]*/
|
||||
while ((bytes_read < count) && (is_sequence_timeout(ts_start, conn->ctx) == false) &&
|
||||
(conn->ctx->is_connection_terminated == false)) { // [V2G2-536]
|
||||
|
||||
int num_of_bytes;
|
||||
|
||||
if (conn->is_tls_connection) {
|
||||
return -1; // shouldn't be using this function
|
||||
}
|
||||
/* use select for timeout handling */
|
||||
struct timeval tv;
|
||||
fd_set read_fds;
|
||||
|
||||
FD_ZERO(&read_fds);
|
||||
FD_SET(conn->conn.socket_fd, &read_fds);
|
||||
|
||||
tv.tv_sec = conn->ctx->network_read_timeout / 1000;
|
||||
tv.tv_usec = (conn->ctx->network_read_timeout % 1000) * 1000;
|
||||
|
||||
num_of_bytes = select(conn->conn.socket_fd + 1, &read_fds, nullptr, nullptr, &tv);
|
||||
|
||||
if (num_of_bytes == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Zero fds ready means we timed out, so let upper loop check our sequence timeout */
|
||||
if (num_of_bytes == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
num_of_bytes = (int)read(conn->conn.socket_fd, &buf[bytes_read], count - bytes_read);
|
||||
|
||||
if (num_of_bytes == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* return when peer closed connection */
|
||||
if (num_of_bytes == 0)
|
||||
return bytes_read;
|
||||
|
||||
bytes_read += num_of_bytes;
|
||||
}
|
||||
|
||||
if (conn->ctx->is_connection_terminated == true) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Reading from tcp-socket aborted");
|
||||
return -2;
|
||||
}
|
||||
|
||||
return (ssize_t)bytes_read; // [V2G2-537] read bytes are currupted if reading from socket was interrupted
|
||||
// (V2G_SECC_Sequence_Timeout)
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief connection_read This function writes to socket until bytes are written to the socket
|
||||
* \param conn is the v2g connection context
|
||||
* \param buf is the buffer where the v2g message is stored
|
||||
* \param count is the number of bytes to write
|
||||
* \return Returns \c true if a timeout has occurred, otherwise \c false
|
||||
*/
|
||||
ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t count) {
|
||||
int bytes_written = 0;
|
||||
|
||||
/* loop until we got all requested bytes out */
|
||||
while (bytes_written < count) {
|
||||
int num_of_bytes = (int)write(conn->conn.socket_fd, &buf[bytes_written], count - bytes_written);
|
||||
if (conn->is_tls_connection) {
|
||||
return -1; // shouldn't be using this function
|
||||
}
|
||||
if (num_of_bytes == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* return when peer closed connection */
|
||||
if (num_of_bytes == 0)
|
||||
return bytes_written;
|
||||
|
||||
bytes_written += num_of_bytes;
|
||||
}
|
||||
|
||||
return (ssize_t)bytes_written;
|
||||
}
|
||||
|
||||
static std::optional<types::evse_manager::HlcSessionFailedReasonEnum>
|
||||
map_v2g_msg_to_hlc_failed_reason(V2gMsgTypeId msg, bool tls_handshake_failed) {
|
||||
using Reason = types::evse_manager::HlcSessionFailedReasonEnum;
|
||||
if (tls_handshake_failed) {
|
||||
return Reason::FailedTLSHandshake;
|
||||
}
|
||||
switch (msg) {
|
||||
case V2G_SUPPORTED_APP_PROTOCOL_MSG:
|
||||
case V2G_SESSION_SETUP_MSG:
|
||||
return Reason::ProtocolNegotiationFailed;
|
||||
case V2G_SERVICE_DISCOVERY_MSG:
|
||||
case V2G_SERVICE_DETAIL_MSG:
|
||||
case V2G_CHARGE_PARAMETER_DISCOVERY_MSG:
|
||||
return Reason::ChargingParametersNotAccepted;
|
||||
case V2G_PAYMENT_SERVICE_SELECTION_MSG:
|
||||
case V2G_PAYMENT_DETAILS_MSG:
|
||||
case V2G_AUTHORIZATION_MSG:
|
||||
case V2G_CERTIFICATE_UPDATE_MSG:
|
||||
case V2G_CERTIFICATE_INSTALLATION_MSG:
|
||||
return Reason::AuthorizationFailed;
|
||||
case V2G_CABLE_CHECK_MSG:
|
||||
case V2G_PRE_CHARGE_MSG:
|
||||
case V2G_POWER_DELIVERY_MSG:
|
||||
return Reason::EnergyTransferSetupFailed;
|
||||
case V2G_CURRENT_DEMAND_MSG:
|
||||
case V2G_CHARGING_STATUS_MSG:
|
||||
case V2G_METERING_RECEIPT_MSG:
|
||||
return Reason::ChargingInterrupted;
|
||||
case V2G_SESSION_STOP_MSG:
|
||||
case V2G_WELDING_DETECTION_MSG:
|
||||
return std::nullopt;
|
||||
default:
|
||||
return Reason::UnexpectedSessionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief connection_teardown This function must be called on connection teardown.
|
||||
* \param conn is the V2G connection context
|
||||
*/
|
||||
void connection_teardown(struct v2g_connection* conn) {
|
||||
if (conn->ctx->session.is_charging == true) {
|
||||
conn->ctx->p_charger->publish_current_demand_finished(nullptr);
|
||||
|
||||
if (conn->ctx->is_dc_charger == true) {
|
||||
conn->ctx->p_charger->publish_dc_open_contactor(nullptr);
|
||||
} else {
|
||||
conn->ctx->p_charger->publish_ac_open_contactor(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
const V2gMsgTypeId last_msg = conn->last_v2g_msg_at_disconnect;
|
||||
const bool evse_initiated_stop = conn->ctx->stop_hlc || conn->ctx->intl_emergency_shutdown;
|
||||
|
||||
/* init charging session */
|
||||
v2g_ctx_init_charging_session(conn->ctx, true);
|
||||
|
||||
if (!evse_initiated_stop && (conn->d_link_action == dLinkAction::D_LINK_ACTION_TERMINATE ||
|
||||
conn->d_link_action == dLinkAction::D_LINK_ACTION_ERROR)) {
|
||||
if (const auto reason = map_v2g_msg_to_hlc_failed_reason(last_msg, conn->tls_handshake_failed)) {
|
||||
conn->ctx->p_charger->publish_hlc_session_failed(*reason);
|
||||
}
|
||||
}
|
||||
|
||||
/* print dlink status */
|
||||
switch (conn->d_link_action) {
|
||||
case dLinkAction::D_LINK_ACTION_ERROR:
|
||||
conn->ctx->p_charger->publish_dlink_error(nullptr);
|
||||
dlog(DLOG_LEVEL_TRACE, "d_link/error");
|
||||
break;
|
||||
case dLinkAction::D_LINK_ACTION_TERMINATE:
|
||||
conn->ctx->p_charger->publish_dlink_terminate(nullptr);
|
||||
dlog(DLOG_LEVEL_TRACE, "d_link/terminate");
|
||||
break;
|
||||
case dLinkAction::D_LINK_ACTION_PAUSE:
|
||||
conn->ctx->p_charger->publish_dlink_pause(nullptr);
|
||||
dlog(DLOG_LEVEL_TRACE, "d_link/pause");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void wait_for_peer_close(int fd, int timeout_ms) {
|
||||
struct pollfd pfd = {};
|
||||
pfd.fd = fd;
|
||||
pfd.events = POLLIN | POLLHUP;
|
||||
|
||||
int rc = poll(&pfd, 1, timeout_ms);
|
||||
if (rc <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pfd.revents & (POLLIN | POLLHUP)) {
|
||||
char buf[64];
|
||||
while (recv(fd, buf, sizeof(buf), MSG_DONTWAIT) > 0) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the 'main' function of a thread, which handles a TCP connection.
|
||||
*/
|
||||
static void* connection_handle_tcp(void* data) {
|
||||
struct v2g_connection* conn = static_cast<struct v2g_connection*>(data);
|
||||
int rv = 0;
|
||||
bool error_occurred{false};
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "Started new TCP connection thread");
|
||||
|
||||
remove_service_from_service_list_if_exists(conn->ctx, V2G_SERVICE_ID_CERTIFICATE);
|
||||
|
||||
/* check if the v2g-session is already running in another thread, if not, handle v2g-connection */
|
||||
if (conn->ctx->state == 0) {
|
||||
int rv2 = v2g_handle_connection(conn);
|
||||
|
||||
if (rv2 != 0) {
|
||||
dlog(DLOG_LEVEL_INFO, "v2g_handle_connection exited with %d", rv2);
|
||||
}
|
||||
} else {
|
||||
rv = ERROR_SESSION_ALREADY_STARTED;
|
||||
dlog(DLOG_LEVEL_WARNING, "%s", "Closing tcp-connection. v2g-session is already running");
|
||||
}
|
||||
|
||||
/* tear down connection gracefully */
|
||||
dlog(DLOG_LEVEL_INFO, "Closing TCP connection");
|
||||
|
||||
/* some EV's did not like the immediate shutdown. Therefore we sleep for 2 seconds */
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
|
||||
if (shutdown(conn->conn.socket_fd, SHUT_WR) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "shutdown() failed: %s", strerror(errno));
|
||||
error_occurred = true;
|
||||
}
|
||||
|
||||
/* wait briefly for peer FIN or timeout */
|
||||
wait_for_peer_close(conn->conn.socket_fd, CLIENT_FIN_TIMEOUT);
|
||||
|
||||
if (close(conn->conn.socket_fd) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "close() failed: %s", strerror(errno));
|
||||
error_occurred = true;
|
||||
}
|
||||
|
||||
if (not error_occurred) {
|
||||
dlog(DLOG_LEVEL_INFO, "TCP connection closed gracefully");
|
||||
}
|
||||
|
||||
conn->ctx->connection_initiated = false;
|
||||
|
||||
if (rv != ERROR_SESSION_ALREADY_STARTED) {
|
||||
/* cleanup and notify lower layers */
|
||||
connection_teardown(conn);
|
||||
}
|
||||
|
||||
free(conn);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void* connection_server(void* data) {
|
||||
struct v2g_context* ctx = static_cast<v2g_context*>(data);
|
||||
ctx->connection_initiated = false;
|
||||
struct v2g_connection* conn = NULL;
|
||||
pthread_attr_t attr;
|
||||
|
||||
/* create the thread in detached state so we don't need to join every single one */
|
||||
if (pthread_attr_init(&attr) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_attr_init failed: %s", strerror(errno));
|
||||
goto thread_exit;
|
||||
}
|
||||
if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_attr_setdetachstate failed: %s", strerror(errno));
|
||||
goto thread_exit;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
char client_addr[INET6_ADDRSTRLEN];
|
||||
struct sockaddr_in6 addr;
|
||||
socklen_t addrlen = sizeof(addr);
|
||||
|
||||
/* cleanup old one and create new connection context */
|
||||
free(conn);
|
||||
conn = static_cast<v2g_connection*>(calloc(1, sizeof(*conn)));
|
||||
if (!conn) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Calloc failed: %s", strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
/* setup common stuff */
|
||||
conn->ctx = ctx;
|
||||
conn->read = &connection_read;
|
||||
conn->write = &connection_write;
|
||||
conn->is_tls_connection = false;
|
||||
|
||||
/* wait for an incoming connection */
|
||||
conn->conn.socket_fd = accept(ctx->tcp_socket, (struct sockaddr*)&addr, &addrlen);
|
||||
if (conn->conn.socket_fd == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Accept(tcp) failed: %s", strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inet_ntop(AF_INET6, &addr, client_addr, sizeof(client_addr)) != NULL) {
|
||||
dlog(DLOG_LEVEL_INFO, "Incoming connection on %s from [%s]:%" PRIu16, ctx->if_name, client_addr,
|
||||
ntohs(addr.sin6_port));
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_ERROR, "Incoming connection on %s, but inet_ntop failed: %s", ctx->if_name,
|
||||
strerror(errno));
|
||||
}
|
||||
|
||||
// store the port to create a udp socket
|
||||
conn->ctx->udp_port = ntohs(addr.sin6_port);
|
||||
|
||||
if (ctx->connection_initiated) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Incoming connection on %s, but there is already an active connection.",
|
||||
ctx->if_name);
|
||||
connection_teardown(conn);
|
||||
free(conn);
|
||||
conn = NULL;
|
||||
continue;
|
||||
}
|
||||
ctx->connection_initiated = true;
|
||||
|
||||
if (pthread_create(&conn->thread_id, &attr, connection_handle_tcp, conn) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_create() failed: %s", strerror(errno));
|
||||
ctx->connection_initiated = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* is up to the thread to cleanup conn */
|
||||
conn = NULL;
|
||||
}
|
||||
|
||||
thread_exit:
|
||||
if (pthread_attr_destroy(&attr) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_attr_destroy failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
/* clean up if dangling */
|
||||
free(conn);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int connection_start_servers(struct v2g_context* ctx) {
|
||||
int rv, tcp_started = 0;
|
||||
|
||||
if (ctx->tcp_socket != -1) {
|
||||
rv = pthread_create(&ctx->tcp_thread, NULL, connection_server, ctx);
|
||||
if (rv != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_create(tcp) failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
tcp_started = 1;
|
||||
}
|
||||
|
||||
if (ctx->tls_socket.fd != -1) {
|
||||
rv = tls::connection_start_server(ctx);
|
||||
if (rv != 0) {
|
||||
if (tcp_started) {
|
||||
pthread_cancel(ctx->tcp_thread);
|
||||
pthread_join(ctx->tcp_thread, NULL);
|
||||
}
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_create(tls) failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_udp_socket(const uint16_t udp_port, const char* interface_name) {
|
||||
constexpr auto LINK_LOCAL_MULTICAST = "ff02::1";
|
||||
|
||||
int udp_socket = socket(AF_INET6, SOCK_DGRAM, 0);
|
||||
if (udp_socket < 0) {
|
||||
EVLOG_error << "Could not create socket: " << strerror(errno);
|
||||
return udp_socket;
|
||||
}
|
||||
|
||||
// source setup
|
||||
|
||||
// find port between 49152-65535
|
||||
auto could_bind = false;
|
||||
auto source_port = 49152;
|
||||
for (; source_port < 65535; source_port++) {
|
||||
sockaddr_in6 source_address = {AF_INET6, htons(source_port)};
|
||||
if (bind(udp_socket, reinterpret_cast<sockaddr*>(&source_address), sizeof(sockaddr_in6)) == 0) {
|
||||
could_bind = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!could_bind) {
|
||||
EVLOG_error << "Could not bind: " << strerror(errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
EVLOG_info << "UDP socket bound to source port: " << source_port;
|
||||
|
||||
const auto index = if_nametoindex(interface_name);
|
||||
auto mreq = ipv6_mreq{};
|
||||
mreq.ipv6mr_interface = index;
|
||||
if (inet_pton(AF_INET6, LINK_LOCAL_MULTICAST, &mreq.ipv6mr_multiaddr) <= 0) {
|
||||
EVLOG_error << "Failed to setup multicast address" << strerror(errno);
|
||||
return -1;
|
||||
}
|
||||
if (setsockopt(udp_socket, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
|
||||
EVLOG_error << "Could not add multicast group membership: " << strerror(errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setsockopt(udp_socket, IPPROTO_IPV6, IPV6_MULTICAST_IF, &index, sizeof(index)) < 0) {
|
||||
EVLOG_error << "Could not set interface name: " << interface_name << "with error: " << strerror(errno);
|
||||
}
|
||||
|
||||
// destination setup
|
||||
sockaddr_in6 destination_address = {AF_INET6, htons(udp_port)};
|
||||
if (inet_pton(AF_INET6, LINK_LOCAL_MULTICAST, &destination_address.sin6_addr) <= 0) {
|
||||
EVLOG_error << "Failed to setup server address" << strerror(errno);
|
||||
}
|
||||
const auto connected =
|
||||
connect(udp_socket, reinterpret_cast<sockaddr*>(&destination_address), sizeof(sockaddr_in6)) == 0;
|
||||
if (!connected) {
|
||||
EVLOG_error << "Could not connect: " << strerror(errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return udp_socket;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022 chargebyte GmbH
|
||||
// Copyright (C) 2022 Contributors to EVerest
|
||||
|
||||
#ifndef CONNECTION_H
|
||||
#define CONNECTION_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include "v2g_ctx.hpp"
|
||||
|
||||
/*!
|
||||
* \brief initialise TCP/TLS connections
|
||||
* \param ctx the V2G context
|
||||
* \return 0 on success
|
||||
*/
|
||||
int connection_init(struct v2g_context* ctx);
|
||||
|
||||
/*!
|
||||
* \brief start TCP/TLS servers
|
||||
* \param ctx the V2G context
|
||||
* \return 0 on success
|
||||
*/
|
||||
int connection_start_servers(struct v2g_context* ctx);
|
||||
int create_udp_socket(const uint16_t udp_port, const char* interface_name);
|
||||
|
||||
/*!
|
||||
* \brief check for V2G message sequence timeout
|
||||
* \param ts_start start time
|
||||
* \param ctx the V2G context
|
||||
* \return true on timeout
|
||||
*/
|
||||
bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx);
|
||||
|
||||
/*!
|
||||
* \brief actions to take on connection close
|
||||
* \param conn v2g connection context
|
||||
*/
|
||||
void connection_teardown(struct v2g_connection* conn);
|
||||
|
||||
/*!
|
||||
* \brief connection_read This abstracts a read from the connection socket, so that higher level functions
|
||||
* are not required to distinguish between TCP and TLS connections.
|
||||
* \param conn v2g connection context
|
||||
* \param buf buffer to store received message sequence.
|
||||
* \param count number of read bytes.
|
||||
* \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and
|
||||
* -2 for closed connection */
|
||||
ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count);
|
||||
|
||||
/*!
|
||||
* \brief connection_write This abstracts a write to the connection socket, so that higher level functions
|
||||
* are not required to distinguish between TCP and TLS connections.
|
||||
* \param conn v2g connection context
|
||||
* \param buf buffer to store received message sequence.
|
||||
* \param count size of the buffer
|
||||
* \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and
|
||||
* -2 for closed connection */
|
||||
ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count);
|
||||
|
||||
#endif /* CONNECTION_H */
|
||||
@@ -0,0 +1,338 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "tls_connection.hpp"
|
||||
#include "connection.hpp"
|
||||
#include "log.hpp"
|
||||
#include "v2g.hpp"
|
||||
#include "v2g_server.hpp"
|
||||
#include <everest/tls/tls.hpp>
|
||||
#include <new>
|
||||
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <memory>
|
||||
#include <sys/types.h>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
// used when ctx->network_read_timeout_tls is 0
|
||||
constexpr int default_timeout_ms = 1000;
|
||||
|
||||
void process_connection_thread(std::shared_ptr<tls::ServerConnection> con, struct v2g_context* ctx) {
|
||||
assert(con != nullptr);
|
||||
assert(ctx != nullptr);
|
||||
|
||||
openssl::pkey_ptr contract_public_key{nullptr, nullptr};
|
||||
auto connection = std::make_unique<v2g_connection>();
|
||||
connection->ctx = ctx;
|
||||
connection->is_tls_connection = true;
|
||||
connection->read = &tls::connection_read;
|
||||
connection->write = &tls::connection_write;
|
||||
connection->tls_connection = con.get();
|
||||
connection->pubkey = &contract_public_key;
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "Incoming TLS connection");
|
||||
|
||||
bool loop{true};
|
||||
bool tls_accepted{false};
|
||||
while (loop) {
|
||||
loop = false;
|
||||
const auto result = con->accept();
|
||||
switch (result) {
|
||||
case tls::Connection::result_t::success:
|
||||
tls_accepted = true;
|
||||
if (ctx->state == 0) {
|
||||
const auto rv = ::v2g_handle_connection(connection.get());
|
||||
dlog(DLOG_LEVEL_INFO, "v2g_dispatch_connection exited with %d", rv);
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_INFO, "%s", "Closing tls-connection. v2g-session is already running");
|
||||
}
|
||||
|
||||
con->shutdown();
|
||||
break;
|
||||
case tls::Connection::result_t::want_read:
|
||||
case tls::Connection::result_t::want_write:
|
||||
loop = con->wait_for(result, default_timeout_ms) == tls::Connection::result_t::success;
|
||||
break;
|
||||
case tls::Connection::result_t::closed:
|
||||
case tls::Connection::result_t::timeout:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
connection->tls_handshake_failed = !tls_accepted;
|
||||
connection->ctx->connection_initiated = false;
|
||||
|
||||
::connection_teardown(connection.get());
|
||||
}
|
||||
|
||||
void handle_new_connection_cb(tls::Server::ConnectionPtr&& con, struct v2g_context* ctx) {
|
||||
assert(con != nullptr);
|
||||
assert(ctx != nullptr);
|
||||
if (ctx->connection_initiated) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Incoming TLS connection on %s, but there is already an active connection.",
|
||||
ctx->if_name);
|
||||
return;
|
||||
}
|
||||
ctx->connection_initiated = true;
|
||||
// create a thread to process this connection
|
||||
try {
|
||||
// passing unique pointers through thread parameters is problematic
|
||||
std::shared_ptr<tls::ServerConnection> connection(con.release());
|
||||
std::thread connection_loop(process_connection_thread, connection, ctx);
|
||||
connection_loop.detach();
|
||||
} catch (const std::system_error&) {
|
||||
// unable to start thread
|
||||
dlog(DLOG_LEVEL_ERROR, "pthread_create() failed: %s", strerror(errno));
|
||||
con->shutdown();
|
||||
ctx->connection_initiated = false;
|
||||
}
|
||||
}
|
||||
|
||||
void server_loop_thread(struct v2g_context* ctx) {
|
||||
assert(ctx != nullptr);
|
||||
assert(ctx->tls_server != nullptr);
|
||||
const auto res = ctx->tls_server->serve([ctx](auto con) { handle_new_connection_cb(std::move(con), ctx); });
|
||||
if (res != tls::Server::state_t::stopped) {
|
||||
dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve");
|
||||
}
|
||||
}
|
||||
|
||||
tls::Server::OptionalConfig configure_ssl(struct v2g_context* ctx) {
|
||||
try {
|
||||
dlog(DLOG_LEVEL_WARNING, "configure_ssl");
|
||||
auto config = std::make_unique<tls::Server::config_t>();
|
||||
|
||||
// The config of interest is from Evse Security, no point in updating
|
||||
// config when there is a problem
|
||||
|
||||
if (build_config(*config, ctx)) {
|
||||
return {{std::move(config)}};
|
||||
}
|
||||
} catch (const std::bad_alloc&) {
|
||||
dlog(DLOG_LEVEL_ERROR, "unable to create TLS config");
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace tls {
|
||||
|
||||
int connection_init(struct v2g_context* ctx) {
|
||||
using state_t = tls::Server::state_t;
|
||||
|
||||
assert(ctx != nullptr);
|
||||
assert(ctx->tls_server != nullptr);
|
||||
assert(ctx->r_security != nullptr);
|
||||
|
||||
int res{-1};
|
||||
tls::Server::config_t config;
|
||||
|
||||
// build_config can fail due to issues with Evse Security,
|
||||
// this can be retried later. Not treated as an error.
|
||||
(void)build_config(config, ctx);
|
||||
|
||||
// apply config
|
||||
ctx->tls_server->stop();
|
||||
ctx->tls_server->wait_stopped();
|
||||
const auto result = ctx->tls_server->init(config, [ctx]() { return configure_ssl(ctx); });
|
||||
if ((result == state_t::init_complete) || (result == state_t::init_socket)) {
|
||||
res = 0;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int connection_start_server(struct v2g_context* ctx) {
|
||||
assert(ctx != nullptr);
|
||||
assert(ctx->tls_server != nullptr);
|
||||
|
||||
// only starts the TLS server
|
||||
|
||||
int res = 0;
|
||||
try {
|
||||
ctx->tls_server->stop();
|
||||
ctx->tls_server->wait_stopped();
|
||||
if (ctx->tls_server->state() == tls::Server::state_t::stopped) {
|
||||
// need to re-initialise
|
||||
tls::connection_init(ctx);
|
||||
}
|
||||
std::thread serve_loop(server_loop_thread, ctx);
|
||||
serve_loop.detach();
|
||||
ctx->tls_server->wait_running();
|
||||
} catch (const std::system_error&) {
|
||||
// unable to start thread (caller logs failure)
|
||||
res = -1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, const std::size_t count) {
|
||||
assert(conn != nullptr);
|
||||
assert(conn->tls_connection != nullptr);
|
||||
|
||||
ssize_t result{0};
|
||||
std::size_t bytes_read{0};
|
||||
timespec ts_start{};
|
||||
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "clock_gettime(ts_start) failed: %s", strerror(errno));
|
||||
result = -1;
|
||||
}
|
||||
|
||||
while ((bytes_read < count) && (result >= 0)) {
|
||||
const std::size_t remaining = count - bytes_read;
|
||||
std::size_t bytes_in{0};
|
||||
auto* ptr = reinterpret_cast<std::byte*>(&buf[bytes_read]);
|
||||
|
||||
const auto read_res = conn->tls_connection->read(ptr, remaining, bytes_in);
|
||||
switch (read_res) {
|
||||
case tls::Connection::result_t::success:
|
||||
bytes_read += bytes_in;
|
||||
break;
|
||||
case tls::Connection::result_t::want_read:
|
||||
case tls::Connection::result_t::want_write:
|
||||
conn->tls_connection->wait_for(read_res, default_timeout_ms);
|
||||
break;
|
||||
case tls::Connection::result_t::timeout:
|
||||
// is_sequence_timeout() is used to manage timeouts. Just loop and wait for more bytes
|
||||
break;
|
||||
case tls::Connection::result_t::closed:
|
||||
default:
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (conn->ctx->is_connection_terminated) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Reading from tcp-socket aborted");
|
||||
conn->tls_connection->shutdown();
|
||||
result = -2;
|
||||
}
|
||||
|
||||
if (::is_sequence_timeout(ts_start, conn->ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (result < 0) ? result : static_cast<ssize_t>(bytes_read);
|
||||
}
|
||||
|
||||
ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count) {
|
||||
assert(conn != nullptr);
|
||||
assert(conn->tls_connection != nullptr);
|
||||
|
||||
ssize_t result{0};
|
||||
std::size_t bytes_written{0};
|
||||
|
||||
while ((bytes_written < count) && (result >= 0)) {
|
||||
const std::size_t remaining = count - bytes_written;
|
||||
std::size_t bytes_out{0};
|
||||
const auto* ptr = reinterpret_cast<std::byte*>(&buf[bytes_written]);
|
||||
|
||||
const auto write_res = conn->tls_connection->write(ptr, remaining, bytes_out);
|
||||
switch (write_res) {
|
||||
case tls::Connection::result_t::success:
|
||||
bytes_written += bytes_out;
|
||||
break;
|
||||
case tls::Connection::result_t::want_read:
|
||||
case tls::Connection::result_t::want_write:
|
||||
conn->tls_connection->wait_for(write_res, default_timeout_ms);
|
||||
break;
|
||||
case tls::Connection::result_t::timeout:
|
||||
// is_sequence_timeout() is used to manage timeouts. Just loop and wait for more bytes
|
||||
break;
|
||||
case tls::Connection::result_t::closed:
|
||||
default:
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((result == -1) && (conn->tls_connection->state() == tls::Connection::state_t::closed)) {
|
||||
// if the connection has closed - return the number of bytes sent
|
||||
result = 0;
|
||||
}
|
||||
|
||||
return (result < 0) ? result : static_cast<ssize_t>(bytes_written);
|
||||
}
|
||||
|
||||
bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) {
|
||||
assert(ctx != nullptr);
|
||||
assert(ctx->r_security != nullptr);
|
||||
|
||||
using types::evse_security::CaCertificateType;
|
||||
using types::evse_security::EncodingFormat;
|
||||
using types::evse_security::GetCertificateInfoStatus;
|
||||
using types::evse_security::LeafCertificateType;
|
||||
|
||||
/*
|
||||
* libevse-security checks for an optional password and when one
|
||||
* isn't set is uses an empty string as the password rather than nullptr.
|
||||
* hence private keys are always encrypted.
|
||||
*/
|
||||
|
||||
bool bResult{false};
|
||||
|
||||
config.cipher_list = "ECDHE-ECDSA-AES128-SHA256";
|
||||
config.ciphersuites = ""; // disable TLS 1.3
|
||||
config.verify_client = false; // contract certificate managed in-band in 15118-2
|
||||
|
||||
// use the existing configured socket
|
||||
// TODO(james-ctc): switch to server socket init code otherwise there
|
||||
// may be issues with reinitialisation
|
||||
config.socket = ctx->tls_socket.fd;
|
||||
config.io_timeout_ms = static_cast<std::int32_t>(ctx->network_read_timeout_tls);
|
||||
|
||||
config.tls_key_logging = ctx->tls_key_logging;
|
||||
config.tls_key_logging_path = ctx->tls_key_logging_path;
|
||||
config.host = ctx->if_name;
|
||||
|
||||
// information from libevse-security
|
||||
const auto cert_info =
|
||||
ctx->r_security->call_get_all_valid_certificates_info(LeafCertificateType::V2G, EncodingFormat::PEM, true);
|
||||
if (cert_info.status != GetCertificateInfoStatus::Accepted) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Not Accepted");
|
||||
} else {
|
||||
if (!cert_info.info.empty()) {
|
||||
// process all known certificate chains
|
||||
for (const auto& chain : cert_info.info) {
|
||||
const auto cert_path = chain.certificate.value_or("");
|
||||
const auto key_path = chain.key;
|
||||
const auto root_pem = chain.certificate_root.value_or("");
|
||||
|
||||
// workaround (see above libevse-security comment)
|
||||
const auto key_password = chain.password.value_or("");
|
||||
|
||||
auto& ref = config.chains.emplace_back();
|
||||
ref.certificate_chain_file = cert_path.c_str();
|
||||
ref.private_key_file = key_path.c_str();
|
||||
ref.private_key_password = key_password.c_str();
|
||||
ref.trust_anchor_pem = root_pem.c_str();
|
||||
|
||||
if (chain.ocsp) {
|
||||
for (const auto& ocsp : chain.ocsp.value()) {
|
||||
const char* file{nullptr};
|
||||
if (ocsp.ocsp_path) {
|
||||
file = ocsp.ocsp_path.value().c_str();
|
||||
}
|
||||
ref.ocsp_response_files.push_back(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bResult = true;
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Empty response");
|
||||
}
|
||||
}
|
||||
|
||||
return bResult;
|
||||
}
|
||||
|
||||
} // namespace tls
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef TLS_CONNECTION_HPP_
|
||||
#define TLS_CONNECTION_HPP_
|
||||
|
||||
#include <cstddef>
|
||||
#include <everest/tls/tls.hpp>
|
||||
#include <unistd.h>
|
||||
|
||||
struct v2g_context;
|
||||
struct v2g_connection;
|
||||
|
||||
namespace tls {
|
||||
|
||||
/*!
|
||||
* \param ctx v2g connection context
|
||||
* \return returns 0 on succss and -1 on error
|
||||
*/
|
||||
int connection_init(struct v2g_context* ctx);
|
||||
|
||||
/*!
|
||||
* \param ctx v2g connection context
|
||||
* \return returns 0 on succss and -1 on error
|
||||
*/
|
||||
int connection_start_server(struct v2g_context* ctx);
|
||||
|
||||
/*!
|
||||
* \brief connection_read This abstracts a read from the connection socket, so that higher level functions
|
||||
* are not required to distinguish between TCP and TLS connections.
|
||||
* \param conn v2g connection context
|
||||
* \param buf buffer to store received message sequence.
|
||||
* \param count number of read bytes.
|
||||
* \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and
|
||||
* -2 for closed connection */
|
||||
ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count);
|
||||
|
||||
/*!
|
||||
* \brief connection_write This abstracts a write to the connection socket, so that higher level functions
|
||||
* are not required to distinguish between TCP and TLS connections.
|
||||
* \param conn v2g connection context
|
||||
* \param buf buffer to store received message sequence.
|
||||
* \param count size of the buffer
|
||||
* \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and
|
||||
* -2 for closed connection */
|
||||
ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count);
|
||||
|
||||
/*!
|
||||
* \brief build_config This builds the TLS server configuration based on the v2g context.
|
||||
* \param config TLS server configuration to be filled
|
||||
* \param ctx v2g connection context
|
||||
* \return Returns true if the configuration was built successfully, otherwise false.
|
||||
*/
|
||||
bool build_config(tls::Server::config_t& config, struct v2g_context* ctx);
|
||||
|
||||
} // namespace tls
|
||||
|
||||
#endif // TLS_CONNECTION_HPP_
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CRTYPTO_COMMON_HPP_
|
||||
#define CRTYPTO_COMMON_HPP_
|
||||
|
||||
#include <everest/tls/openssl_util.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace crypto {
|
||||
|
||||
using verify_result_t = openssl::verify_result_t;
|
||||
|
||||
constexpr std::size_t MAX_EXI_SIZE = 8192;
|
||||
|
||||
} // namespace crypto
|
||||
|
||||
#endif // CRTYPTO_COMMON_HPP_
|
||||
@@ -0,0 +1,209 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
|
||||
#include "crypto_openssl.hpp"
|
||||
#include "iso_server.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <cbv2g/common/exi_bitstream.h>
|
||||
#include <cbv2g/exi_v2gtp.h> //for V2GTP_HEADER_LENGTHs
|
||||
#include <cbv2g/iso_2/iso2_msgDefDatatypes.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefDecoder.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefEncoder.h>
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/store.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
namespace crypto ::openssl {
|
||||
using ::openssl::bn_const_t;
|
||||
using ::openssl::bn_t;
|
||||
using ::openssl::log_error;
|
||||
using ::openssl::sha_256;
|
||||
using ::openssl::sha_256_digest_t;
|
||||
using ::openssl::verify;
|
||||
|
||||
bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, EVP_PKEY* pkey,
|
||||
struct iso2_exiFragment* iso2_exi_fragment) {
|
||||
assert(pkey != nullptr);
|
||||
assert(iso2_signature != nullptr);
|
||||
assert(iso2_exi_fragment != nullptr);
|
||||
|
||||
bool bRes{true};
|
||||
|
||||
// signature information
|
||||
const struct iso2_ReferenceType* req_ref = &iso2_signature->SignedInfo.Reference.array[0];
|
||||
const auto signature_len = iso2_signature->SignatureValue.CONTENT.bytesLen;
|
||||
const auto* signature = &iso2_signature->SignatureValue.CONTENT.bytes[0];
|
||||
|
||||
// build data to check signature against
|
||||
std::array<std::uint8_t, MAX_EXI_SIZE> exi_buffer{};
|
||||
exi_bitstream_t stream;
|
||||
exi_bitstream_init(&stream, exi_buffer.data(), MAX_EXI_SIZE, 0, NULL);
|
||||
|
||||
auto err = encode_iso2_exiFragment(&stream, iso2_exi_fragment);
|
||||
if (err != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Unable to encode fragment, error code = %d", err);
|
||||
bRes = false;
|
||||
}
|
||||
|
||||
sha_256_digest_t digest;
|
||||
|
||||
// calculate hash of data
|
||||
if (bRes) {
|
||||
const auto frag_data_len = exi_bitstream_get_length(&stream);
|
||||
bRes = sha_256(exi_buffer.data(), frag_data_len, digest);
|
||||
}
|
||||
|
||||
// check hash matches the value in the message
|
||||
if (bRes) {
|
||||
if (req_ref->DigestValue.bytesLen != digest.size()) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid digest length %u in signature", req_ref->DigestValue.bytesLen);
|
||||
bRes = false;
|
||||
}
|
||||
}
|
||||
if (bRes) {
|
||||
if (std::memcmp(req_ref->DigestValue.bytes, digest.data(), digest.size()) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid digest in signature");
|
||||
bRes = false;
|
||||
}
|
||||
}
|
||||
|
||||
// verify the signature
|
||||
if (bRes) {
|
||||
struct iso2_xmldsigFragment sig_fragment {};
|
||||
init_iso2_xmldsigFragment(&sig_fragment);
|
||||
sig_fragment.SignedInfo_isUsed = 1;
|
||||
sig_fragment.SignedInfo = iso2_signature->SignedInfo;
|
||||
|
||||
/** \req [V2G2-771] Don't use following fields */
|
||||
sig_fragment.SignedInfo.Id_isUsed = 0;
|
||||
sig_fragment.SignedInfo.CanonicalizationMethod.ANY_isUsed = 0;
|
||||
sig_fragment.SignedInfo.SignatureMethod.HMACOutputLength_isUsed = 0;
|
||||
sig_fragment.SignedInfo.SignatureMethod.ANY_isUsed = 0;
|
||||
for (auto* ref = sig_fragment.SignedInfo.Reference.array;
|
||||
ref != (sig_fragment.SignedInfo.Reference.array + sig_fragment.SignedInfo.Reference.arrayLen); ++ref) {
|
||||
ref->Type_isUsed = 0;
|
||||
ref->Transforms.Transform.ANY_isUsed = 0;
|
||||
ref->Transforms.Transform.XPath_isUsed = 0;
|
||||
ref->DigestMethod.ANY_isUsed = 0;
|
||||
}
|
||||
|
||||
stream.byte_pos = 0;
|
||||
stream.bit_count = 0;
|
||||
err = encode_iso2_xmldsigFragment(&stream, &sig_fragment);
|
||||
|
||||
if (err != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Unable to encode XML signature fragment, error code = %d", err);
|
||||
bRes = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (bRes) {
|
||||
// hash again (different data) buffer_pos has been updated ...
|
||||
const auto frag_data_len = exi_bitstream_get_length(&stream);
|
||||
bRes = sha_256(exi_buffer.data(), frag_data_len, digest);
|
||||
}
|
||||
|
||||
if (bRes) {
|
||||
/* Validate the ecdsa signature using the public key */
|
||||
if (signature_len != ::openssl::signature_size) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Signature len is invalid (%i)", signature_len);
|
||||
bRes = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (bRes) {
|
||||
const std::uint8_t* r = &signature[0];
|
||||
const std::uint8_t* s = &signature[32];
|
||||
bRes = verify(pkey, r, s, digest);
|
||||
}
|
||||
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool load_contract_root_cert(::openssl::certificate_list& trust_anchors, const char* V2G_file_path,
|
||||
const char* MO_file_path) {
|
||||
trust_anchors.clear();
|
||||
|
||||
auto mo_certs = ::openssl::load_certificates(MO_file_path);
|
||||
trust_anchors = std::move(mo_certs);
|
||||
|
||||
auto v2g_certs = ::openssl::load_certificates(V2G_file_path);
|
||||
trust_anchors.insert(trust_anchors.end(), std::make_move_iterator(v2g_certs.begin()),
|
||||
std::make_move_iterator(v2g_certs.end()));
|
||||
|
||||
if (trust_anchors.empty()) {
|
||||
log_error("Unable to load any MO or V2G root(s)");
|
||||
}
|
||||
|
||||
return !trust_anchors.empty();
|
||||
}
|
||||
|
||||
int load_certificate(::openssl::certificate_list* chain, const std::uint8_t* bytes, std::uint16_t bytesLen) {
|
||||
assert(chain != nullptr);
|
||||
int result{-1};
|
||||
|
||||
auto tmp_cert = ::openssl::der_to_certificate(bytes, bytesLen);
|
||||
if (tmp_cert != nullptr) {
|
||||
chain->push_back(std::move(tmp_cert));
|
||||
result = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int parse_contract_certificate(::openssl::certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen) {
|
||||
crt = ::openssl::der_to_certificate(buf, buflen);
|
||||
return (crt == nullptr) ? -1 : 0;
|
||||
}
|
||||
|
||||
std::string getEmaidFromContractCert(const ::openssl::certificate_ptr& crt) {
|
||||
std::string cert_emaid;
|
||||
const auto subject = ::openssl::certificate_subject(crt.get());
|
||||
if (auto itt = subject.find("CN"); itt != subject.end()) {
|
||||
cert_emaid = itt->second;
|
||||
}
|
||||
|
||||
return cert_emaid;
|
||||
}
|
||||
|
||||
std::string chain_to_pem(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain) {
|
||||
assert(chain != nullptr);
|
||||
|
||||
std::string contract_cert_chain_pem(::openssl::certificate_to_pem(cert.get()));
|
||||
for (const auto& crt : *chain) {
|
||||
const auto pem = ::openssl::certificate_to_pem(crt.get());
|
||||
if (pem.empty()) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Unable to encode certificate chain");
|
||||
break;
|
||||
}
|
||||
contract_cert_chain_pem.append(pem);
|
||||
}
|
||||
|
||||
return contract_cert_chain_pem;
|
||||
}
|
||||
|
||||
verify_result_t verify_certificate(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain,
|
||||
const char* v2g_root_cert_path, const char* mo_root_cert_path,
|
||||
bool /* debugMode */) {
|
||||
assert(chain != nullptr);
|
||||
|
||||
verify_result_t result{verify_result_t::Verified};
|
||||
::openssl::certificate_list trust_anchors;
|
||||
|
||||
if (!load_contract_root_cert(trust_anchors, v2g_root_cert_path, mo_root_cert_path)) {
|
||||
result = verify_result_t::NoCertificateAvailable;
|
||||
} else {
|
||||
result = ::openssl::verify_certificate(cert.get(), trust_anchors, *chain);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace crypto::openssl
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef CRYPTO_OPENSSL_HPP_
|
||||
#define CRYPTO_OPENSSL_HPP_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "crypto_common.hpp"
|
||||
#include <everest/tls/openssl_util.hpp>
|
||||
|
||||
/**
|
||||
* \file OpenSSL implementation
|
||||
*/
|
||||
|
||||
struct evp_pkey_st;
|
||||
struct iso2_SignatureType;
|
||||
struct iso2_exiFragment;
|
||||
struct x509_st;
|
||||
struct v2g_connection;
|
||||
|
||||
namespace crypto::openssl {
|
||||
|
||||
/**
|
||||
* \brief check the signature of a signed 15118 message
|
||||
* \param iso2_signature the signature to check
|
||||
* \param public_key the public key from the contract certificate
|
||||
* \param iso2_exi_fragment the signed data
|
||||
* \return true when the signature is valid
|
||||
*/
|
||||
bool check_iso2_signature(const struct iso2_SignatureType* iso2_signature, evp_pkey_st* pkey,
|
||||
struct iso2_exiFragment* iso2_exi_fragment);
|
||||
|
||||
/**
|
||||
* \brief load the trust anchor for the contract certificate.
|
||||
* Use the mobility operator certificate if it exists otherwise
|
||||
* the V2G certificate
|
||||
* \param contract_root_crt the retrieved trust anchor
|
||||
* \param V2G_file_path the file containing the V2G trust anchor (PEM format)
|
||||
* \param MO_file_path the file containing the mobility operator trust anchor (PEM format)
|
||||
* \return true when a certificate was found
|
||||
*/
|
||||
bool load_contract_root_cert(::openssl::certificate_list& trust_anchors, const char* V2G_file_path,
|
||||
const char* MO_file_path);
|
||||
|
||||
/**
|
||||
* \brief clear certificate and public key from previous connection
|
||||
* \param conn the V2G connection data
|
||||
* \note not needed for the OpenSSL implementation
|
||||
*/
|
||||
constexpr void free_connection_crypto_data(v2g_connection* conn) {
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief load a contract certificate's certification path certificate from the V2G message as DER bytes
|
||||
* \param chain the certificate path certificates (this certificate is added to the list)
|
||||
* \param bytes the DER (ASN.1) X509v3 certificate in the V2G message
|
||||
* \param bytesLen the length of the DER encoded certificate
|
||||
* \return 0 when certificate successfully loaded
|
||||
*/
|
||||
int load_certificate(::openssl::certificate_list* chain, const std::uint8_t* bytes, std::uint16_t bytesLen);
|
||||
|
||||
/**
|
||||
* \brief load the contract certificate from the V2G message as DER bytes
|
||||
* \param crt the certificate
|
||||
* \param bytes the DER (ASN.1) X509v3 certificate in the V2G message
|
||||
* \param bytesLen the length of the DER encoded certificate
|
||||
* \return 0 when certificate successfully loaded
|
||||
*/
|
||||
int parse_contract_certificate(::openssl::certificate_ptr& crt, const std::uint8_t* buf, std::size_t buflen);
|
||||
|
||||
/**
|
||||
* \brief get the EMAID from the certificate (CommonName from the SubjectName)
|
||||
* \param crt the certificate
|
||||
* \return the EMAD or empty on error
|
||||
*/
|
||||
std::string getEmaidFromContractCert(const ::openssl::certificate_ptr& crt);
|
||||
|
||||
/**
|
||||
* \brief convert a list of certificates into a PEM string starting with the contract certificate
|
||||
* \param contract_crt the contract certificate (when not the first certificate in the chain)
|
||||
* \param chain the certification path chain (might include the contract certificate as the first item)
|
||||
* \return PEM string or empty on error
|
||||
*/
|
||||
std::string chain_to_pem(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain);
|
||||
|
||||
/**
|
||||
* \brief verify certification path of the contract certificate through to a trust anchor
|
||||
* \param contract_crt (single certificate or chain with the contract certificate as the first item)
|
||||
* \param chain intermediate certificates (may be nullptr)
|
||||
* \param v2g_root_cert_path V2G trust anchor file name
|
||||
* \param mo_root_cert_path mobility operator trust anchor file name
|
||||
* \param debugMode additional information on verification failures
|
||||
* \result a subset of possible verification failures where known or 'verified' on success
|
||||
*/
|
||||
verify_result_t verify_certificate(const ::openssl::certificate_ptr& cert, const ::openssl::certificate_list* chain,
|
||||
const char* v2g_root_cert_path, const char* mo_root_cert_path, bool debugMode);
|
||||
|
||||
} // namespace crypto::openssl
|
||||
|
||||
#endif // CRYPTO_OPENSSL_HPP_
|
||||
1133
tools/EVerest-main/modules/EVSE/EvseV2G/din_server.cpp
Normal file
1133
tools/EVerest-main/modules/EVSE/EvseV2G/din_server.cpp
Normal file
File diff suppressed because it is too large
Load Diff
120
tools/EVerest-main/modules/EVSE/EvseV2G/din_server.hpp
Normal file
120
tools/EVerest-main/modules/EVSE/EvseV2G/din_server.hpp
Normal file
@@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 chargebyte GmbH
|
||||
// Copyright (C) 2023 Contributors to EVerest
|
||||
|
||||
#ifndef DIN_SERVER_HPP
|
||||
#define DIN_SERVER_HPP
|
||||
|
||||
#include "v2g.hpp"
|
||||
|
||||
/*!
|
||||
* \brief The din_state_id enum
|
||||
*/
|
||||
enum din_state_id {
|
||||
WAIT_FOR_SESSIONSETUP = 0,
|
||||
WAIT_FOR_SERVICEDISCOVERY,
|
||||
WAIT_FOR_PAYMENTSERVICESELECTION,
|
||||
WAIT_FOR_AUTHORIZATION,
|
||||
WAIT_FOR_CHARGEPARAMETERDISCOVERY,
|
||||
WAIT_FOR_CABLECHECK,
|
||||
WAIT_FOR_PRECHARGE,
|
||||
WAIT_FOR_PRECHARGE_POWERDELIVERY,
|
||||
WAIT_FOR_CURRENTDEMAND,
|
||||
WAIT_FOR_CURRENTDEMAND_POWERDELIVERY,
|
||||
WAIT_FOR_WELDINGDETECTION_SESSIONSTOP,
|
||||
WAIT_FOR_SESSIONSTOP,
|
||||
WAIT_FOR_TERMINATED_SESSION
|
||||
};
|
||||
|
||||
static const char* dinResponse[] = {"Response OK",
|
||||
"New Session Established",
|
||||
"Old Session Joined",
|
||||
"Certificate Expires Soon",
|
||||
"Response Failed",
|
||||
"Sequence Error",
|
||||
"Service ID Invalid",
|
||||
"Unknown Session",
|
||||
"Service Selection Invalid",
|
||||
"Payment Selection Invalid",
|
||||
"Certificate Expired",
|
||||
"Signature Error",
|
||||
"No Certificate Available",
|
||||
"Cert Chain Error",
|
||||
"Challenge Invalid",
|
||||
"Contract Canceled",
|
||||
"Wrong Charge Parameter",
|
||||
"Power Delivery Not Applied",
|
||||
"Tariff Selection Invalid",
|
||||
"Charging Profile Invalid",
|
||||
"EVSE Present Voltage To Low",
|
||||
"Metering Signature Not Valid",
|
||||
"Wrong Energy Transfer Type"};
|
||||
|
||||
/*!
|
||||
* \brief The din_state struct
|
||||
*/
|
||||
struct din_state {
|
||||
const char* description;
|
||||
int allowed_requests;
|
||||
};
|
||||
|
||||
static const struct din_state din_states[] = {
|
||||
/* [V2G-DC-437] Expected req msg after supportedAppProtocolRes */
|
||||
[WAIT_FOR_SESSIONSETUP] = {"Waiting for SessionSetupReq", 1 << V2G_SESSION_SETUP_MSG},
|
||||
/* [V2G-DC-439] Expected req msg after SessionSetupRes */
|
||||
[WAIT_FOR_SERVICEDISCOVERY] = {"Waiting for ServiceDiscoveryReq, SessionStopReq",
|
||||
1 << V2G_SERVICE_DISCOVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-441] Expected req msg after ServiceDiscoveryRes */
|
||||
[WAIT_FOR_PAYMENTSERVICESELECTION] = {"Waiting for ServicePaymentSelectionReq, SessionStopReq",
|
||||
1 << V2G_PAYMENT_SERVICE_SELECTION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-444] [V2G-DC-497] Expected req msg after ServicePaymentSelectionRes */
|
||||
[WAIT_FOR_AUTHORIZATION] = {"Waiting for ContractAuthenticationReq, SessionStopReq",
|
||||
1 << V2G_AUTHORIZATION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-498], [V2G-DC-495] Expected req msg after ContractAuthenticationRes*/
|
||||
[WAIT_FOR_CHARGEPARAMETERDISCOVERY] = {"Waiting for ChargeParameterDiscoveryReq, SessionStopReq",
|
||||
1 << V2G_CHARGE_PARAMETER_DISCOVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-453], [V2G-DC-499] Expected req msg after ChargeParameterDiscoveryRes, CableCheckRes */
|
||||
[WAIT_FOR_CABLECHECK] = {"Waiting for CableCheckReq, SessionStopReq",
|
||||
1 << V2G_CABLE_CHECK_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-455] Expected req msgs after CableCheckRes */
|
||||
[WAIT_FOR_PRECHARGE] = {"Waiting for PreChargeReq, SessionStopReq",
|
||||
1 << V2G_PRE_CHARGE_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-458] Expected req msg after PreChargeRes */
|
||||
[WAIT_FOR_PRECHARGE_POWERDELIVERY] = {"Waiting for PowerDeliveryReq, PreChargeReq, SessionStopRes",
|
||||
1 << V2G_POWER_DELIVERY_MSG | 1 << V2G_PRE_CHARGE_MSG |
|
||||
1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-DC-462] Expected req msg after PowerDeliveryRes (Ready to Charge = true) */
|
||||
[WAIT_FOR_CURRENTDEMAND] = {"Waiting for CurrentDemandReq", 1 << V2G_CURRENT_DEMAND_MSG},
|
||||
/* [V2G-DC-462], [V2G-DC-465] Expected req msg after PowerDeliveryRes (Ready to Charge = true), CurrentDemandRes */
|
||||
[WAIT_FOR_CURRENTDEMAND_POWERDELIVERY] = {"Waiting for CurrentDemandReq, PowerDeliveryReq",
|
||||
1 << V2G_CURRENT_DEMAND_MSG | 1 << V2G_POWER_DELIVERY_MSG},
|
||||
/* [V2G-DC-459], [V2G-DC-469] Expected req msg after PowerDeliveryRes, WeldingDetectionRes */
|
||||
[WAIT_FOR_WELDINGDETECTION_SESSIONSTOP] = {"Waiting for WeldingDetectionReq, SessionStopReq ",
|
||||
1 << V2G_WELDING_DETECTION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
[WAIT_FOR_SESSIONSTOP] = {"Waiting for SessionStopReq", 1 << V2G_SESSION_STOP_MSG},
|
||||
[WAIT_FOR_TERMINATED_SESSION] = {"Terminate session", 0}};
|
||||
|
||||
/*!
|
||||
* \brief din_handle_request This function handles the incoming request message of a connected EV.
|
||||
* It analyzes the incoming DIN request EXI stream and configures the response EXI stream
|
||||
* \param conn This structure provides the EXI streams
|
||||
* \return This function returns \c 1 if the connection must be closed immediately,
|
||||
* when this function returns \c 0 the req handle is successful,
|
||||
* when this function returns \c 2 the reply needs to be sent and the connection needs to be closed afterwards
|
||||
*/
|
||||
enum v2g_event din_handle_request(v2g_connection* conn);
|
||||
|
||||
namespace utils {
|
||||
enum v2g_event din_validate_response_code(din_responseCodeType* const din_response_code,
|
||||
struct v2g_connection const* conn);
|
||||
} // namespace utils
|
||||
|
||||
namespace states {
|
||||
|
||||
enum v2g_event handle_din_session_setup(struct v2g_connection* conn);
|
||||
enum v2g_event handle_din_service_discovery(struct v2g_connection* conn);
|
||||
enum v2g_event handle_din_contract_authentication(struct v2g_connection* conn);
|
||||
|
||||
} // namespace states
|
||||
|
||||
#endif /* DIN_SERVER_HPP */
|
||||
42
tools/EVerest-main/modules/EVSE/EvseV2G/docs/index.rst
Normal file
42
tools/EVerest-main/modules/EVSE/EvseV2G/docs/index.rst
Normal file
@@ -0,0 +1,42 @@
|
||||
.. _everest_modules_handwritten_EvseV2G:
|
||||
|
||||
.. *******************************************
|
||||
.. EvseV2G
|
||||
.. *******************************************
|
||||
|
||||
This module includes a DIN70121 and ISO15118-2 implementation provided by chargebyte GmbH
|
||||
|
||||
Feature List
|
||||
============
|
||||
|
||||
This document contains feature lists for DIN70121 and ISO15118-2 features, which EvseV2G supports.
|
||||
These lists serve as a quick overview of which features are supported.
|
||||
|
||||
DIN70121
|
||||
--------
|
||||
|
||||
=============== ==================
|
||||
Feature Supported
|
||||
=============== ==================
|
||||
DC ✔️
|
||||
ExternalPayment ✔️
|
||||
=============== ==================
|
||||
|
||||
ISO15118-2
|
||||
----------
|
||||
|
||||
======================= ==================
|
||||
Feature Supported
|
||||
======================= ==================
|
||||
AC ✔️
|
||||
DC ✔️
|
||||
TCP & TLS 1.2 ✔️
|
||||
ExternalPayment ✔️
|
||||
Plug&Charge ✔️
|
||||
CertificateInstallation ✔️
|
||||
CertificateUpdate
|
||||
Pause/Resume ✔️
|
||||
Schedule Renegotation
|
||||
Smart Charging
|
||||
Internet Service
|
||||
======================= ==================
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "iso15118_extensionsImpl.hpp"
|
||||
#include "log.hpp"
|
||||
#include "v2g_ctx.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace extensions {
|
||||
|
||||
void iso15118_extensionsImpl::init() {
|
||||
if (!v2g_ctx) {
|
||||
dlog(DLOG_LEVEL_ERROR, "v2g_ctx not created");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void iso15118_extensionsImpl::ready() {
|
||||
}
|
||||
|
||||
void iso15118_extensionsImpl::handle_set_get_certificate_response(
|
||||
types::iso15118::ResponseExiStreamStatus& certificate_response) {
|
||||
pthread_mutex_lock(&v2g_ctx->mqtt_lock);
|
||||
if (certificate_response.exi_response.has_value() and not certificate_response.exi_response.value().empty()) {
|
||||
v2g_ctx->evse_v2g_data.cert_install_res_b64_buffer = std::string(certificate_response.exi_response.value());
|
||||
}
|
||||
v2g_ctx->evse_v2g_data.cert_install_status =
|
||||
(certificate_response.status == types::iso15118::Status::Accepted) ? true : false;
|
||||
pthread_cond_signal(&v2g_ctx->mqtt_cond);
|
||||
/* unlock */
|
||||
pthread_mutex_unlock(&v2g_ctx->mqtt_lock);
|
||||
}
|
||||
|
||||
} // namespace extensions
|
||||
} // namespace module
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#ifndef EXTENSIONS_ISO15118_EXTENSIONS_IMPL_HPP
|
||||
#define EXTENSIONS_ISO15118_EXTENSIONS_IMPL_HPP
|
||||
|
||||
//
|
||||
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
|
||||
// template version 3
|
||||
//
|
||||
|
||||
#include <generated/interfaces/iso15118_extensions/Implementation.hpp>
|
||||
|
||||
#include "../EvseV2G.hpp"
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
#include "v2g.hpp"
|
||||
extern struct v2g_context* v2g_ctx;
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
namespace module {
|
||||
namespace extensions {
|
||||
|
||||
struct Conf {};
|
||||
|
||||
class iso15118_extensionsImpl : public iso15118_extensionsImplBase {
|
||||
public:
|
||||
iso15118_extensionsImpl() = delete;
|
||||
iso15118_extensionsImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<EvseV2G>& mod, Conf& config) :
|
||||
iso15118_extensionsImplBase(ev, "extensions"), mod(mod), config(config){};
|
||||
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
// insert your public definitions here
|
||||
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
|
||||
|
||||
protected:
|
||||
// command handler functions (virtual)
|
||||
virtual void
|
||||
handle_set_get_certificate_response(types::iso15118::ResponseExiStreamStatus& certificate_response) override;
|
||||
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
// insert your protected definitions here
|
||||
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<EvseV2G>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
// insert your private definitions here
|
||||
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
|
||||
};
|
||||
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
// insert other definitions here
|
||||
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
|
||||
|
||||
} // namespace extensions
|
||||
} // namespace module
|
||||
|
||||
#endif // EXTENSIONS_ISO15118_EXTENSIONS_IMPL_HPP
|
||||
2400
tools/EVerest-main/modules/EVSE/EvseV2G/iso_server.cpp
Normal file
2400
tools/EVerest-main/modules/EVSE/EvseV2G/iso_server.cpp
Normal file
File diff suppressed because it is too large
Load Diff
149
tools/EVerest-main/modules/EVSE/EvseV2G/iso_server.hpp
Normal file
149
tools/EVerest-main/modules/EVSE/EvseV2G/iso_server.hpp
Normal file
@@ -0,0 +1,149 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 chargebyte GmbH
|
||||
// Copyright (C) 2023 Contributors to EVerest
|
||||
|
||||
#ifndef ISO_SERVER_HPP
|
||||
#define ISO_SERVER_HPP
|
||||
|
||||
#include "v2g.hpp"
|
||||
|
||||
struct iso_state {
|
||||
const char* description;
|
||||
int allowed_requests;
|
||||
};
|
||||
|
||||
enum class iso_ac_state_id {
|
||||
WAIT_FOR_SESSIONSETUP = 0,
|
||||
WAIT_FOR_SERVICEDISCOVERY,
|
||||
WAIT_FOR_SVCDETAIL_PAYMENTSVCSEL,
|
||||
WAIT_FOR_PAYMENTDETAILS_CERTINST_CERTUPD,
|
||||
WAIT_FOR_PAYMENTDETAILS,
|
||||
WAIT_FOR_AUTHORIZATION,
|
||||
WAIT_FOR_CHARGEPARAMETERDISCOVERY,
|
||||
WAIT_FOR_POWERDELIVERY,
|
||||
WAIT_FOR_CHARGINGSTATUS,
|
||||
WAIT_FOR_CHARGINGSTATUS_POWERDELIVERY,
|
||||
WAIT_FOR_METERINGRECEIPT,
|
||||
WAIT_FOR_SESSIONSTOP,
|
||||
WAIT_FOR_TERMINATED_SESSION
|
||||
};
|
||||
|
||||
enum class iso_dc_state_id {
|
||||
WAIT_FOR_SESSIONSETUP = 0,
|
||||
WAIT_FOR_SERVICEDISCOVERY,
|
||||
WAIT_FOR_SVCDETAIL_PAYMENTSVCSEL,
|
||||
WAIT_FOR_PAYMENTDETAILS_CERTINST_CERTUPD,
|
||||
WAIT_FOR_PAYMENTDETAILS,
|
||||
WAIT_FOR_AUTHORIZATION,
|
||||
WAIT_FOR_CHARGEPARAMETERDISCOVERY,
|
||||
WAIT_FOR_CABLECHECK,
|
||||
WAIT_FOR_PRECHARGE,
|
||||
WAIT_FOR_PRECHARGE_POWERDELIVERY,
|
||||
WAIT_FOR_CURRENTDEMAND_POWERDELIVERY,
|
||||
WAIT_FOR_CURRENTDEMAND,
|
||||
WAIT_FOR_METERINGRECEIPT,
|
||||
WAIT_FOR_WELDINGDETECTION_SESSIONSTOP,
|
||||
WAIT_FOR_TERMINATED_SESSION
|
||||
};
|
||||
|
||||
static const char* isoResponse[] = {
|
||||
"Response OK",
|
||||
"New Session Established",
|
||||
"Old Session Joined",
|
||||
"Certificate Expires Soon",
|
||||
"Response FAILED",
|
||||
"Sequence Error",
|
||||
"Service ID Invalid",
|
||||
"Unknown Session",
|
||||
"Service Selection Invalid",
|
||||
"Payment Selection Invalid",
|
||||
"Certificate Expired",
|
||||
"Signature Error",
|
||||
"No Certificate Available",
|
||||
"Cert Chain Error",
|
||||
"Challenge Invalid",
|
||||
"Contract Canceled",
|
||||
"Wrong Charge Parameter",
|
||||
"Power Delivery Not Applied",
|
||||
"Tariff Selection Invalid",
|
||||
"Charging Profile Invalid",
|
||||
"Metering Signature Not Valid",
|
||||
"No Charge Service Selected",
|
||||
"Wrong Energy Transfer Mode",
|
||||
"Contactor Error",
|
||||
"Certificate Not Allowed At This EVSE",
|
||||
"Certificate Revoked",
|
||||
};
|
||||
|
||||
static const struct iso_state iso_ac_states[] = {
|
||||
{"Waiting for SessionSetupReq", 1 << V2G_SESSION_SETUP_MSG},
|
||||
/* [V2G-543] Expected req msg after SessionSetupRes */
|
||||
{"Waiting for ServiceDiscoveryReq, SessionStopReq", 1 << V2G_SERVICE_DISCOVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-545] Expected req msg after ServiceDiscoveryRes */
|
||||
{"Waiting for ServiceDetailReq, PaymentServiceSelectionReq, SessionStopReq",
|
||||
1 << V2G_SERVICE_DETAIL_MSG | 1 << V2G_PAYMENT_SERVICE_SELECTION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-551] Expected req msg after ServicePaymentSelectionRes */
|
||||
{"Waiting for PaymentDetailsReq, CertificateInstallationReq, CertificateUpdateReq, SessionStopReq",
|
||||
1 << V2G_PAYMENT_DETAILS_MSG | 1 << V2G_CERTIFICATE_INSTALLATION_MSG | 1 << V2G_CERTIFICATE_UPDATE_MSG |
|
||||
1 << V2G_SESSION_STOP_MSG},
|
||||
{"Waiting for PaymentDetailsReq, SessionStopReq", 1 << V2G_PAYMENT_DETAILS_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
{"Waiting for AuthorizationReq, SessionStopReq", 1 << V2G_AUTHORIZATION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
{"Waiting for ChargeParameterDiscoveryReq, SessionStopReq",
|
||||
1 << V2G_CHARGE_PARAMETER_DISCOVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
{"Waiting for PowerDeliveryReq, SessionStopReq", 1 << V2G_POWER_DELIVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
{"Waiting for ChargingStatusReq", 1 << V2G_CHARGING_STATUS_MSG},
|
||||
{"Waiting for ChargingStatusReq, PowerDeliveryReq", 1 << V2G_CHARGING_STATUS_MSG | 1 << V2G_POWER_DELIVERY_MSG},
|
||||
{"Waiting for MeteringReceiptReq", 1 << V2G_METERING_RECEIPT_MSG},
|
||||
{"Waiting for SessionStopReq", 1 << V2G_SESSION_STOP_MSG},
|
||||
{"Closing session", 0}};
|
||||
|
||||
static const struct iso_state iso_dc_states[] = {
|
||||
/* [V2G-541] Expected req msg after SupportedAppProtocolRes */
|
||||
{"Waiting for SessionSetupReq", 1 << V2G_SESSION_SETUP_MSG},
|
||||
/* [V2G-543] Expected req msg after SessionSetupRes */
|
||||
{"Waiting for ServiceDiscoveryReq, SessionStopReq", 1 << V2G_SERVICE_DISCOVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-545] Expected req msg after ServiceDiscoveryRes */
|
||||
{"Waiting for ServiceDetailReq, ServicePaymentSelectionReq, SessionStopReq",
|
||||
1 << V2G_SERVICE_DETAIL_MSG | 1 << V2G_PAYMENT_SERVICE_SELECTION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-551] Expected req msg after ServicePaymentSelectionRes */
|
||||
{"Waiting for PaymentDetailsReq, AuthorizationReq, CertificateInstallationReq, CertificateUpdateReq, "
|
||||
"SessionStopReq",
|
||||
1 << V2G_PAYMENT_DETAILS_MSG | 1 << V2G_AUTHORIZATION_MSG | 1 << V2G_CERTIFICATE_INSTALLATION_MSG |
|
||||
1 << V2G_CERTIFICATE_UPDATE_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-557], [V2G-554], [V2G-558] Expected req msg after CertificateInstallationRes or CertificateUpdateRes */
|
||||
{"Waiting for PaymentDetailsReq, SessionStopReq", 1 << V2G_PAYMENT_DETAILS_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-560], [V2G-687] Expected req msg after PaymentDetailsRes, ContractAuthenticationRes */
|
||||
{"Waiting for AuthorizationReq, SessionStopReq", 1 << V2G_AUTHORIZATION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-573], [V2G-813],[V2G-688] Expected req msg after AuthorizationRes or PowerDeliveryRes or
|
||||
ChargeParameterDiscoveryRes */
|
||||
{"Waiting for ChargeParameterDiscoveryReq, SessionStopReq",
|
||||
1 << V2G_CHARGE_PARAMETER_DISCOVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-582], [V2G-621] Expected req msg after CableCheckRes or ChargeParameterDiscoveryRes */
|
||||
{"Waiting for CableCheckReq, SessionStopReq", 1 << V2G_CABLE_CHECK_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-584] Expected req msg after CableCheckRes */
|
||||
{"Waiting for PreChargeReq, SessionStopReq", 1 << V2G_PRE_CHARGE_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-587] Expected req msg after PreChargeRes */
|
||||
{"Waiting for PreChargeReq, PowerDeliveryReq, SessionStopReq",
|
||||
1 << V2G_PRE_CHARGE_MSG | 1 << V2G_POWER_DELIVERY_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
/* [V2G-797] Expected req msg after CurrentDemandRes or MeteringReceiptRes*/
|
||||
{"Waiting for CurrentDemandReq, PowerDeliveryReq", 1 << V2G_CURRENT_DEMAND_MSG | 1 << V2G_POWER_DELIVERY_MSG},
|
||||
/* [V2G-590] Expected req msg after PowerDeliveryRes or CurrentDemandRes or MeteringReceiptRes*/
|
||||
{"Waiting for CurrentDemandReq", 1 << V2G_CURRENT_DEMAND_MSG},
|
||||
/* [V2G-795] Expected req msg after CurrentDemandRes */
|
||||
{"Waiting for MeteringReceiptReq", 1 << V2G_METERING_RECEIPT_MSG},
|
||||
/* [V2G-597], [V2G-601] Expected req msg after PowerDeliveryRes or WeldingDetectionRes*/
|
||||
{"Waiting for WeldingDetectionReq, SessionStopReq", 1 << V2G_WELDING_DETECTION_MSG | 1 << V2G_SESSION_STOP_MSG},
|
||||
{"Closing session", 0}};
|
||||
|
||||
/*!
|
||||
* \brief iso_handle_request This is the main protocol handler. This function analyzes the received
|
||||
* request msg and configures the next response msg.
|
||||
* \param conn \c v2g_connection struct and holds the v2g_connection information
|
||||
* \return when this function returns -1 then the connection is aborted without sending the reply,
|
||||
* when this function returns 0 then the reply is sent,
|
||||
* when this function returns 1 then the reply is sent and the connection is closed afterwards,
|
||||
* when this function returns 2 then no reply is sent but the connection is kept open
|
||||
*/
|
||||
enum v2g_event iso_handle_request(v2g_connection* conn);
|
||||
|
||||
#endif /* ISO_SERVER_HPP */
|
||||
131
tools/EVerest-main/modules/EVSE/EvseV2G/log.cpp
Normal file
131
tools/EVerest-main/modules/EVSE/EvseV2G/log.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#include "log.hpp"
|
||||
#include <everest/logging.hpp> // for logging
|
||||
#include <stdarg.h> // for va_list, va_{start,end}()
|
||||
#include <stdio.h> // for v*printf()
|
||||
#include <stdlib.h> // for atoi()
|
||||
#include <string.h> // for strlen()
|
||||
#include <sys/time.h> // for gettimeofday()
|
||||
#include <time.h> // for strftime()
|
||||
|
||||
dloglevel_t minloglevel_current = DLOG_LEVEL_INFO;
|
||||
|
||||
static const char* debug_level_logstring_map[DLOG_LEVEL_NUMLEVELS] = {
|
||||
// tailing space, no need to add it later when printing
|
||||
// try to keep the strings almost same length, looks better
|
||||
"[(LOG)] ", "[ERROR] ", "[WARN] ", "[INFO] ", "[DEBUG] ", "[TRACE] "};
|
||||
|
||||
const char* debug_level_mqtt_string_map[DLOG_LEVEL_NUMLEVELS] = {"always", "error", "warning",
|
||||
"info", "debug", "trace"};
|
||||
|
||||
// FIXME: inline?
|
||||
void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname,
|
||||
const char* format, ...) {
|
||||
// fast exit
|
||||
if (loglevel > minloglevel_current) {
|
||||
return;
|
||||
}
|
||||
char* format_copy = NULL;
|
||||
FILE* outstream = stderr; // change output target here, if desired
|
||||
|
||||
struct timeval debug_tval;
|
||||
struct tm tm;
|
||||
char log_datetimestamp[16]; // length due to format [00:00:00.000], rounded up to fit 32-bit alignment
|
||||
gettimeofday(&debug_tval, NULL); // ignore return value
|
||||
size_t offset =
|
||||
strftime(log_datetimestamp, sizeof(log_datetimestamp), "[%H:%M:%S", gmtime_r(&debug_tval.tv_sec, &tm));
|
||||
if (offset < 1) {
|
||||
// in our use of strftime(), this is an error
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(log_datetimestamp + offset, sizeof(log_datetimestamp) - offset, ".%03ld] ", debug_tval.tv_usec / 1000);
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
|
||||
// print the user given part
|
||||
// strip possible newline character from user-given string
|
||||
// FIXME: could be skipped
|
||||
if (format) {
|
||||
size_t formatlen = std::string(format).size();
|
||||
format_copy = static_cast<char*>(calloc(1, formatlen + 1)); // additional byte for terminating \0
|
||||
memcpy(format_copy, format, formatlen);
|
||||
if ((formatlen >= 1) && (format_copy[formatlen - 1] == '\n')) {
|
||||
format_copy[formatlen - 1] = '\0';
|
||||
}
|
||||
}
|
||||
char output[256];
|
||||
if (format_copy != NULL) {
|
||||
vsnprintf(output, sizeof(output), format_copy, args);
|
||||
}
|
||||
// force EOL
|
||||
fputs("\n", outstream);
|
||||
fflush(outstream);
|
||||
va_end(args);
|
||||
if (format_copy) {
|
||||
free(format_copy);
|
||||
}
|
||||
|
||||
switch (loglevel) {
|
||||
case DLOG_LEVEL_ERROR:
|
||||
EVLOG_error << output;
|
||||
break;
|
||||
case DLOG_LEVEL_WARNING:
|
||||
EVLOG_warning << output;
|
||||
break;
|
||||
case DLOG_LEVEL_INFO:
|
||||
EVLOG_info << output;
|
||||
break;
|
||||
case DLOG_LEVEL_DEBUG:
|
||||
EVLOG_debug << output;
|
||||
break;
|
||||
case DLOG_LEVEL_TRACE:
|
||||
EVLOG_verbose << output;
|
||||
break;
|
||||
default:
|
||||
EVLOG_critical << "Unknown log level";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void dlog_level_inc(void) {
|
||||
dloglevel_t minloglevel_new = (dloglevel_t)((int)minloglevel_current + 1);
|
||||
if (minloglevel_new == DLOG_LEVEL_NUMLEVELS) {
|
||||
// wrap to bottom, but not DLOG_LEVEL_ALWAYS
|
||||
minloglevel_new = DLOG_LEVEL_ERROR;
|
||||
}
|
||||
dlog_level_set(minloglevel_new);
|
||||
}
|
||||
|
||||
void dlog_level_set(const dloglevel_t loglevel) {
|
||||
// no sanity checks currently
|
||||
const dloglevel_t minloglevel_old = minloglevel_current;
|
||||
dloglevel_t newloglevel = loglevel;
|
||||
if (newloglevel >= DLOG_LEVEL_NUMLEVELS) {
|
||||
// set something illegally high
|
||||
newloglevel = (dloglevel_t)(int)(DLOG_LEVEL_NUMLEVELS - 1);
|
||||
}
|
||||
if (newloglevel <= DLOG_LEVEL_ALWAYS) {
|
||||
// set something illegally low
|
||||
newloglevel = (dloglevel_t)(int)(DLOG_LEVEL_ALWAYS + 1);
|
||||
}
|
||||
if (newloglevel != minloglevel_current) {
|
||||
minloglevel_current = newloglevel;
|
||||
dlog(DLOG_LEVEL_ALWAYS, "switched log level from %d (\"%s\") to %d (\"%s\")", minloglevel_old,
|
||||
debug_level_logstring_map[minloglevel_old], newloglevel, debug_level_logstring_map[newloglevel]);
|
||||
}
|
||||
}
|
||||
|
||||
dloglevel_t dlog_level_get(void) {
|
||||
return minloglevel_current;
|
||||
}
|
||||
|
||||
static const char* dlog_level_get_string(const dloglevel_t loglevel) {
|
||||
if ((loglevel < 1) || loglevel >= DLOG_LEVEL_NUMLEVELS) {
|
||||
return "invalid_level";
|
||||
}
|
||||
return debug_level_mqtt_string_map[loglevel];
|
||||
}
|
||||
73
tools/EVerest-main/modules/EVSE/EvseV2G/log.hpp
Normal file
73
tools/EVerest-main/modules/EVSE/EvseV2G/log.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#ifndef LOG_H
|
||||
#define LOG_H
|
||||
|
||||
/**
|
||||
* @brief Describe the intended log level of a message, or the maximum level a message must have to be displayed.
|
||||
*/
|
||||
typedef enum dloglevel_t {
|
||||
DLOG_LEVEL_ALWAYS = 0, ///< internal use only, for notification of log level change
|
||||
DLOG_LEVEL_ERROR, ///< error
|
||||
DLOG_LEVEL_WARNING, ///< warning, not leading to unexpected behavior such as termination
|
||||
DLOG_LEVEL_INFO, ///< informational message
|
||||
DLOG_LEVEL_DEBUG, ///< message to help debug daemon activity
|
||||
DLOG_LEVEL_TRACE, ///< message to provide extra internal information
|
||||
DLOG_LEVEL_NUMLEVELS, ///< don't use, only for internal detection of upper range
|
||||
} dloglevel_t;
|
||||
|
||||
/**
|
||||
* @brief Internal: Issue a log message. Please use the dlog() macro instead.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname,
|
||||
const char* format, ...);
|
||||
|
||||
/**
|
||||
* @brief Increase the log level to the next higher step (more messages). At the highest step, the level rolls over to
|
||||
* the lowest.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void dlog_level_inc(void);
|
||||
|
||||
/**
|
||||
* @brief Set the log level.
|
||||
* @param[in] loglevel the log level the logger shall use, of type enum dloglevel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
void dlog_level_set(const dloglevel_t loglevel);
|
||||
|
||||
/**
|
||||
* @brief Get the log level.
|
||||
*
|
||||
* @return dloglevel_t the currently valid log level
|
||||
*/
|
||||
dloglevel_t dlog_level_get(void);
|
||||
|
||||
/**
|
||||
* @brief Set the log level from an MQTT topic string.
|
||||
* @param[in] loglevel the log level the logger shall use, as an MQTT string
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// dloglevel_t dlog_level_set_from_mqtt_string(const char *level_string);
|
||||
|
||||
/**
|
||||
* @brief Issue a log message.
|
||||
*
|
||||
* @param[in] level the log level this message belongs to (type enum dloglevel)
|
||||
* @param[in] printf()-like format string and parameters, without tailing '\n'
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// this is a macro, so that when dlog() is used, it gets expanded at the caller's location
|
||||
#define dlog(level, ...) \
|
||||
do { \
|
||||
dlog_func((level), __FILE__, __LINE__, __func__, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
#endif /* LOG_H */
|
||||
101
tools/EVerest-main/modules/EVSE/EvseV2G/manifest.yaml
Normal file
101
tools/EVerest-main/modules/EVSE/EvseV2G/manifest.yaml
Normal file
@@ -0,0 +1,101 @@
|
||||
description: >-
|
||||
This module includes a DIN70121 and ISO15118-2 implementation provided by
|
||||
chargebyte GmbH
|
||||
config:
|
||||
device:
|
||||
description: >-
|
||||
Ethernet device used for HLC. Any local interface that has an ipv6
|
||||
link-local and a MAC addr will work
|
||||
type: string
|
||||
default: eth0
|
||||
supported_DIN70121:
|
||||
description: The EVSE supports the DIN SPEC
|
||||
type: boolean
|
||||
default: true
|
||||
supported_ISO15118_2:
|
||||
description: The EVSE supports ISO15118-2
|
||||
type: boolean
|
||||
default: true
|
||||
tls_security:
|
||||
description: >-
|
||||
Controls how to handle encrypted communication
|
||||
type: string
|
||||
enum:
|
||||
- prohibit
|
||||
- allow
|
||||
- force
|
||||
default: allow
|
||||
terminate_connection_on_failed_response:
|
||||
description: >-
|
||||
Controls how to handle a failed response code of the EVSE. If true the
|
||||
V2G connection is terminated immediately on a failed response code, otherwise
|
||||
the EV is responsible for closing of the V2G communication session with SessionStop.
|
||||
type: boolean
|
||||
default: false
|
||||
tls_key_logging:
|
||||
description: >-
|
||||
Enable/Disable the export of TLS session keys (pre-master-secret)
|
||||
during a TLS handshake. This log file can be used to decrypt TLS
|
||||
sessions. Note that this option is for testing and simulation
|
||||
purpose only
|
||||
type: boolean
|
||||
default: false
|
||||
tls_key_logging_path:
|
||||
description: >-
|
||||
Output directory for the TLS key log file
|
||||
type: string
|
||||
default: /tmp
|
||||
tls_timeout:
|
||||
description: >-
|
||||
Set the TLS timeout in ms when establishing a tls connection
|
||||
type: integer
|
||||
default: 15000
|
||||
verify_contract_cert_chain:
|
||||
description: >-
|
||||
Specifies if the EVSE should verify the contract certificate
|
||||
chain locally.
|
||||
type: boolean
|
||||
default: false
|
||||
auth_timeout_pnc:
|
||||
description: >-
|
||||
Defines how many seconds the EVSE should wait for authorization in PnC case,
|
||||
before the charging session is aborted.
|
||||
Write 0 if the EVSE should wait indefinitely for PnC authorization.
|
||||
type: integer
|
||||
default: 55
|
||||
auth_timeout_eim:
|
||||
description: >-
|
||||
Defines how many seconds the EVSE should wait for authorization in EIM case,
|
||||
before the charging session is aborted.
|
||||
Write 0 if the EVSE should wait indefinitely for EIM authorization.
|
||||
type: integer
|
||||
default: 300
|
||||
enable_sdp_server:
|
||||
description: >-
|
||||
Enable the built-in SDP server
|
||||
type: boolean
|
||||
default: true
|
||||
provides:
|
||||
charger:
|
||||
interface: ISO15118_charger
|
||||
description: >-
|
||||
This module implements the ISO15118-2 implementation of
|
||||
an AC or DC charger
|
||||
extensions:
|
||||
interface: iso15118_extensions
|
||||
description: >-
|
||||
This interface is used to share data between ISO15118 and OCPP modules
|
||||
to support the requirements of the OCPP protocol
|
||||
requires:
|
||||
security:
|
||||
interface: evse_security
|
||||
iso15118_vas:
|
||||
interface: ISO15118_vas
|
||||
min_connections: 0
|
||||
max_connections: 128
|
||||
metadata:
|
||||
license: https://opensource.org/licenses/Apache-2.0
|
||||
authors:
|
||||
- Fabian Hartung
|
||||
- Mohannad Oraby
|
||||
- Sebastian Lukas
|
||||
361
tools/EVerest-main/modules/EVSE/EvseV2G/sdp.cpp
Normal file
361
tools/EVerest-main/modules/EVSE/EvseV2G/sdp.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#include "sdp.hpp"
|
||||
#include "log.hpp"
|
||||
#include "tools.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <inttypes.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
#include <poll.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define DEBUG 1
|
||||
|
||||
/* defines for V2G SDP implementation */
|
||||
#define SDP_SRV_PORT 15118
|
||||
|
||||
#define SDP_VERSION 0x01
|
||||
#define SDP_INVERSE_VERSION 0xfe
|
||||
|
||||
#define SDP_HEADER_LEN 8
|
||||
#define SDP_REQUEST_PAYLOAD_LEN 2
|
||||
#define SDP_RESPONSE_PAYLOAD_LEN 20
|
||||
|
||||
#define SDP_REQUEST_TYPE 0x9000
|
||||
#define SDP_RESPONSE_TYPE 0x9001
|
||||
|
||||
#define POLL_TIMEOUT 20
|
||||
|
||||
/* link-local multicast address ff02::1 aka ip6-allnodes */
|
||||
#define IN6ADDR_ALLNODES \
|
||||
{ 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 }
|
||||
|
||||
/* bundles various aspects of a SDP query */
|
||||
struct sdp_query {
|
||||
struct v2g_context* v2g_ctx;
|
||||
|
||||
struct sockaddr_in6 remote_addr;
|
||||
|
||||
enum sdp_security security_requested;
|
||||
enum sdp_transport_protocol proto_requested;
|
||||
};
|
||||
|
||||
/*
|
||||
* Fills the SDP header into a given buffer
|
||||
*/
|
||||
int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t payload_len) {
|
||||
int offset = 0;
|
||||
|
||||
buffer[offset++] = SDP_VERSION;
|
||||
buffer[offset++] = SDP_INVERSE_VERSION;
|
||||
|
||||
/* payload is network byte order */
|
||||
buffer[offset++] = (payload_type >> 8) & 0xff;
|
||||
buffer[offset++] = payload_type & 0xff;
|
||||
|
||||
/* payload_length is network byte order */
|
||||
buffer[offset++] = (payload_len >> 24) & 0xff;
|
||||
buffer[offset++] = (payload_len >> 16) & 0xff;
|
||||
buffer[offset++] = (payload_len >> 8) & 0xff;
|
||||
buffer[offset++] = payload_len & 0xff;
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
int sdp_validate_header(uint8_t* buffer, uint16_t expected_payload_type, uint32_t expected_payload_len) {
|
||||
uint16_t payload_type;
|
||||
uint32_t payload_len;
|
||||
|
||||
if (buffer[0] != SDP_VERSION) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid SDP version");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (buffer[1] != SDP_INVERSE_VERSION) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid SDP inverse version");
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload_type = (buffer[2] << 8) + buffer[3];
|
||||
if (payload_type != expected_payload_type) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid payload type: expected %" PRIu16 ", received %" PRIu16, expected_payload_type,
|
||||
payload_type);
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload_len = (buffer[4] << 24) + (buffer[5] << 16) + (buffer[6] << 8) + buffer[7];
|
||||
if (payload_len != expected_payload_len) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid payload length: expected %" PRIu32 ", received %" PRIu32, expected_payload_len,
|
||||
payload_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sdp_create_response(uint8_t* buffer, struct sockaddr_in6* addr, enum sdp_security security,
|
||||
enum sdp_transport_protocol proto) {
|
||||
int offset = SDP_HEADER_LEN;
|
||||
|
||||
/* fill in first the payload */
|
||||
|
||||
/* address is already network byte order */
|
||||
memcpy(&buffer[offset], &addr->sin6_addr, sizeof(addr->sin6_addr));
|
||||
offset += sizeof(addr->sin6_addr);
|
||||
|
||||
memcpy(&buffer[offset], &addr->sin6_port, sizeof(addr->sin6_port));
|
||||
offset += sizeof(addr->sin6_port);
|
||||
|
||||
buffer[offset++] = security;
|
||||
buffer[offset++] = proto;
|
||||
|
||||
/* now fill in the header with payload length */
|
||||
sdp_write_header(buffer, SDP_RESPONSE_TYPE, offset - SDP_HEADER_LEN);
|
||||
|
||||
return offset;
|
||||
}
|
||||
/*
|
||||
* Sends a SDP response packet
|
||||
*/
|
||||
int sdp_send_response(int sdp_socket, struct sdp_query* sdp_query) {
|
||||
uint8_t buffer[SDP_HEADER_LEN + SDP_RESPONSE_PAYLOAD_LEN];
|
||||
int rv = 0;
|
||||
|
||||
/* at the moment we only understand TCP protocol */
|
||||
if (sdp_query->proto_requested != SDP_TRANSPORT_PROTOCOL_TCP) {
|
||||
dlog(DLOG_LEVEL_ERROR, "SDP requested unsupported protocol 0x%02x, announcing nothing",
|
||||
sdp_query->proto_requested);
|
||||
return 1;
|
||||
}
|
||||
|
||||
using state_t = tls::Server::state_t;
|
||||
const auto tls_server_state = sdp_query->v2g_ctx->tls_server->state();
|
||||
|
||||
const auto tls_server_available =
|
||||
(tls_server_state == state_t::init_complete or tls_server_state == state_t::running);
|
||||
|
||||
switch (sdp_query->security_requested) {
|
||||
case SDP_SECURITY_TLS:
|
||||
if (sdp_query->v2g_ctx->local_tls_addr and tls_server_available) {
|
||||
dlog(DLOG_LEVEL_INFO, "SDP requested TLS, announcing TLS");
|
||||
sdp_create_response(buffer, sdp_query->v2g_ctx->local_tls_addr, SDP_SECURITY_TLS,
|
||||
SDP_TRANSPORT_PROTOCOL_TCP);
|
||||
break;
|
||||
}
|
||||
if (sdp_query->v2g_ctx->local_tcp_addr) {
|
||||
dlog(DLOG_LEVEL_INFO, "SDP requested TLS, announcing NO-TLS");
|
||||
sdp_create_response(buffer, sdp_query->v2g_ctx->local_tcp_addr, SDP_SECURITY_NONE,
|
||||
SDP_TRANSPORT_PROTOCOL_TCP);
|
||||
break;
|
||||
}
|
||||
dlog(DLOG_LEVEL_ERROR, "SDP requested TLS, announcing nothing");
|
||||
return 1;
|
||||
|
||||
case SDP_SECURITY_NONE:
|
||||
if (sdp_query->v2g_ctx->local_tcp_addr) {
|
||||
dlog(DLOG_LEVEL_INFO, "SDP requested NO-TLS, announcing NO-TLS");
|
||||
sdp_create_response(buffer, sdp_query->v2g_ctx->local_tcp_addr, SDP_SECURITY_NONE,
|
||||
SDP_TRANSPORT_PROTOCOL_TCP);
|
||||
break;
|
||||
}
|
||||
if (sdp_query->v2g_ctx->local_tls_addr and tls_server_available) {
|
||||
dlog(DLOG_LEVEL_INFO, "SDP requested NO-TLS, announcing TLS");
|
||||
sdp_create_response(buffer, sdp_query->v2g_ctx->local_tls_addr, SDP_SECURITY_TLS,
|
||||
SDP_TRANSPORT_PROTOCOL_TCP);
|
||||
break;
|
||||
}
|
||||
dlog(DLOG_LEVEL_ERROR, "SDP requested NO-TLS, announcing nothing");
|
||||
return 1;
|
||||
|
||||
default:
|
||||
dlog(DLOG_LEVEL_ERROR, "SDP requested unsupported security 0x%02x, announcing nothing",
|
||||
sdp_query->security_requested);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (sendto(sdp_socket, buffer, sizeof(buffer), 0, (struct sockaddr*)&sdp_query->remote_addr,
|
||||
sizeof(struct sockaddr_in6)) != sizeof(buffer)) {
|
||||
rv = -1;
|
||||
}
|
||||
if (DEBUG) {
|
||||
char addrbuf[INET6_ADDRSTRLEN] = {0};
|
||||
const char* addr;
|
||||
int saved_errno = errno;
|
||||
|
||||
addr = inet_ntop(AF_INET6, &sdp_query->remote_addr.sin6_addr, addrbuf, sizeof(addrbuf));
|
||||
if (rv == 0) {
|
||||
dlog(DLOG_LEVEL_INFO, "sendto([%s]:%" PRIu16 ") succeeded", addr, ntohs(sdp_query->remote_addr.sin6_port));
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_ERROR, "sendto([%s]:%" PRIu16 ") failed: %s", addr, ntohs(sdp_query->remote_addr.sin6_port),
|
||||
strerror(saved_errno));
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
int sdp_init(struct v2g_context* v2g_ctx) {
|
||||
struct sockaddr_in6 sdp_addr = {AF_INET6, htons(SDP_SRV_PORT)};
|
||||
struct ipv6_mreq mreq = {{IN6ADDR_ALLNODES}, 0};
|
||||
int enable = 1;
|
||||
|
||||
mreq.ipv6mr_interface = if_nametoindex(v2g_ctx->if_name);
|
||||
if (!mreq.ipv6mr_interface) {
|
||||
dlog(DLOG_LEVEL_ERROR, "No such interface: %s", v2g_ctx->if_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* create receiving socket */
|
||||
v2g_ctx->sdp_socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (v2g_ctx->sdp_socket == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "socket() failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setsockopt(v2g_ctx->sdp_socket, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(enable)) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "setsockopt(SO_REUSEPORT) failed: %s", strerror(errno));
|
||||
close(v2g_ctx->sdp_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sdp_addr.sin6_addr = in6addr_any;
|
||||
|
||||
if (bind(v2g_ctx->sdp_socket, (struct sockaddr*)&sdp_addr, sizeof(sdp_addr)) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "bind() failed: %s", strerror(errno));
|
||||
close(v2g_ctx->sdp_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "SDP socket setup succeeded");
|
||||
|
||||
/* bind only to specified device */
|
||||
if (setsockopt(v2g_ctx->sdp_socket, SOL_SOCKET, SO_BINDTODEVICE, v2g_ctx->if_name, strlen(v2g_ctx->if_name)) ==
|
||||
-1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "setsockopt(SO_BINDTODEVICE) failed: %s", strerror(errno));
|
||||
close(v2g_ctx->sdp_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_TRACE, "bind only to specified device");
|
||||
|
||||
/* join multicast group */
|
||||
if (setsockopt(v2g_ctx->sdp_socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "setsockopt(IPV6_JOIN_GROUP) failed: %s", strerror(errno));
|
||||
close(v2g_ctx->sdp_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_TRACE, "joined multicast group");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sdp_listen(struct v2g_context* v2g_ctx) {
|
||||
/* Init pollfd struct */
|
||||
struct pollfd pollfd = {v2g_ctx->sdp_socket, POLLIN, 0};
|
||||
|
||||
while (!v2g_ctx->shutdown) {
|
||||
uint8_t buffer[SDP_HEADER_LEN + SDP_REQUEST_PAYLOAD_LEN];
|
||||
char addrbuf[INET6_ADDRSTRLEN] = {0};
|
||||
const char* addr = addrbuf;
|
||||
struct sdp_query sdp_query = {
|
||||
.v2g_ctx = v2g_ctx,
|
||||
};
|
||||
socklen_t addrlen = sizeof(sdp_query.remote_addr);
|
||||
|
||||
/* Track V2G communication setup timeout [V2G2-723] */
|
||||
long long int dlink_ready_time = v2g_ctx->sdp_dlink_ready_time.load();
|
||||
|
||||
/* Cancel timeout if a V2G TCP/TLS connection was established */
|
||||
if (v2g_ctx->connection_initiated && dlink_ready_time != 0) {
|
||||
dlog(DLOG_LEVEL_INFO, "V2G TCP/TLS connection established, SDP communication setup timeout cancelled");
|
||||
v2g_ctx->sdp_dlink_ready_time = 0;
|
||||
}
|
||||
|
||||
/* Check if V2G communication setup timeout has expired */
|
||||
if (dlink_ready_time != 0 && !v2g_ctx->connection_initiated) {
|
||||
long long int elapsed = getmonotonictime() - dlink_ready_time;
|
||||
if (elapsed >= V2G_COMMUNICATION_SETUP_TIMEOUT) {
|
||||
dlog(DLOG_LEVEL_WARNING,
|
||||
"V2G communication setup timeout (%dms) expired - signaling dlink_error to EvseManager [V2G2-723]",
|
||||
V2G_COMMUNICATION_SETUP_TIMEOUT);
|
||||
v2g_ctx->p_charger->publish_dlink_error(nullptr);
|
||||
v2g_ctx->sdp_dlink_ready = false;
|
||||
v2g_ctx->sdp_dlink_ready_time = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if data was received on socket */
|
||||
signed status = poll(&pollfd, 1, POLL_TIMEOUT);
|
||||
|
||||
if (status == -1) {
|
||||
if (errno == EINTR) { // If the call did not succeed because it was interrupted
|
||||
continue;
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_ERROR, "poll() failed: %s", strerror(errno));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/* If new data was received, handle sdp request */
|
||||
if (status > 0) {
|
||||
ssize_t len = recvfrom(v2g_ctx->sdp_socket, buffer, sizeof(buffer), 0,
|
||||
(struct sockaddr*)&sdp_query.remote_addr, &addrlen);
|
||||
if (len == -1) {
|
||||
if (errno != EINTR)
|
||||
dlog(DLOG_LEVEL_ERROR, "recvfrom() failed: %s", strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
addr = inet_ntop(AF_INET6, &sdp_query.remote_addr.sin6_addr, addrbuf, sizeof(addrbuf));
|
||||
|
||||
if (len != sizeof(buffer)) {
|
||||
dlog(DLOG_LEVEL_WARNING, "Discarded packet from [%s]:%" PRIu16 " due to unexpected length %zd", addr,
|
||||
ntohs(sdp_query.remote_addr.sin6_port), len);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sdp_validate_header(buffer, SDP_REQUEST_TYPE, SDP_REQUEST_PAYLOAD_LEN)) {
|
||||
dlog(DLOG_LEVEL_WARNING, "Packet with invalid SDP header received from [%s]:%" PRIu16, addr,
|
||||
ntohs(sdp_query.remote_addr.sin6_port));
|
||||
continue;
|
||||
}
|
||||
|
||||
sdp_query.security_requested = (sdp_security)buffer[SDP_HEADER_LEN + 0];
|
||||
sdp_query.proto_requested = (sdp_transport_protocol)buffer[SDP_HEADER_LEN + 1];
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "Received packet from [%s]:%" PRIu16 " with security 0x%02x and protocol 0x%02x",
|
||||
addr, ntohs(sdp_query.remote_addr.sin6_port), sdp_query.security_requested, sdp_query.proto_requested);
|
||||
|
||||
if (!v2g_ctx->sdp_dlink_ready) {
|
||||
dlog(DLOG_LEVEL_INFO, "SDP request discarded: dlink not ready");
|
||||
continue;
|
||||
}
|
||||
|
||||
sdp_send_response(v2g_ctx->sdp_socket, &sdp_query);
|
||||
}
|
||||
}
|
||||
|
||||
if (close(v2g_ctx->sdp_socket) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "close() failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void sdp_set_dlink_ready(struct v2g_context* v2g_ctx, bool ready) {
|
||||
v2g_ctx->sdp_dlink_ready = ready;
|
||||
v2g_ctx->sdp_dlink_ready_time = ready ? getmonotonictime() : 0;
|
||||
dlog(DLOG_LEVEL_INFO, "SDP dlink_ready set to %s", ready ? "true" : "false");
|
||||
}
|
||||
27
tools/EVerest-main/modules/EVSE/EvseV2G/sdp.hpp
Normal file
27
tools/EVerest-main/modules/EVSE/EvseV2G/sdp.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#ifndef SDP_H
|
||||
#define SDP_H
|
||||
|
||||
#include "v2g.hpp"
|
||||
|
||||
enum sdp_security {
|
||||
SDP_SECURITY_TLS = 0x00,
|
||||
SDP_SECURITY_NONE = 0x10,
|
||||
};
|
||||
|
||||
enum sdp_transport_protocol {
|
||||
SDP_TRANSPORT_PROTOCOL_TCP = 0x00,
|
||||
SDP_TRANSPORT_PROTOCOL_UDP = 0x10,
|
||||
};
|
||||
|
||||
int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t payload_len);
|
||||
int sdp_validate_header(uint8_t* buffer, uint16_t expected_payload_type, uint32_t expected_payload_len);
|
||||
int sdp_create_response(uint8_t* buffer, struct sockaddr_in6* addr, enum sdp_security security,
|
||||
enum sdp_transport_protocol proto);
|
||||
int sdp_init(struct v2g_context* v2g_ctx);
|
||||
int sdp_listen(struct v2g_context* v2g_ctx);
|
||||
void sdp_set_dlink_ready(struct v2g_context* v2g_ctx, bool ready);
|
||||
|
||||
#endif /* SDP_H */
|
||||
212
tools/EVerest-main/modules/EVSE/EvseV2G/tests/CMakeLists.txt
Normal file
212
tools/EVerest-main/modules/EVSE/EvseV2G/tests/CMakeLists.txt
Normal file
@@ -0,0 +1,212 @@
|
||||
get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR)
|
||||
find_package(libevent)
|
||||
find_package(OpenSSL 3)
|
||||
|
||||
set(LIB_EVEREST_TLS_TESTS_DIR "${PROJECT_SOURCE_DIR}/lib/everest/tls/tests")
|
||||
set(TESTS_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/tests/include")
|
||||
|
||||
set(TLS_TEST_FILES
|
||||
alt_openssl-pki.conf
|
||||
iso_pkey.asn1
|
||||
openssl-pki.conf
|
||||
ocsp_response.der
|
||||
pki.sh
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${TLS_TEST_FILES}
|
||||
COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/pki
|
||||
COMMAND cd pki && cp ${TLS_TEST_FILES} ${CMAKE_CURRENT_BINARY_DIR}/
|
||||
WORKING_DIRECTORY ${LIB_EVEREST_TLS_TESTS_DIR}
|
||||
)
|
||||
|
||||
add_custom_target(v2g_test_files_target
|
||||
DEPENDS ${TLS_TEST_FILES}
|
||||
)
|
||||
|
||||
set(TLS_GTEST_NAME v2g_openssl_test)
|
||||
add_executable(${TLS_GTEST_NAME})
|
||||
add_dependencies(${TLS_GTEST_NAME} v2g_test_files_target)
|
||||
|
||||
add_dependencies(${TLS_GTEST_NAME} generate_cpp_files)
|
||||
|
||||
target_include_directories(${TLS_GTEST_NAME} PRIVATE
|
||||
.. ../crypto
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
|
||||
)
|
||||
|
||||
target_compile_definitions(${TLS_GTEST_NAME} PRIVATE
|
||||
-DUNIT_TEST
|
||||
)
|
||||
|
||||
target_sources(${TLS_GTEST_NAME} PRIVATE
|
||||
${LIB_EVEREST_TLS_TESTS_DIR}/gtest_main.cpp
|
||||
log.cpp
|
||||
openssl_test.cpp
|
||||
../crypto/crypto_openssl.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${TLS_GTEST_NAME} PRIVATE
|
||||
GTest::gtest
|
||||
cbv2g::din
|
||||
cbv2g::iso2
|
||||
cbv2g::tp
|
||||
everest::framework
|
||||
everest::evse_security
|
||||
everest::tls
|
||||
)
|
||||
|
||||
set(V2G_MAIN_NAME v2g_server)
|
||||
add_executable(${V2G_MAIN_NAME})
|
||||
|
||||
add_dependencies(${V2G_MAIN_NAME} generate_cpp_files)
|
||||
|
||||
target_include_directories(${V2G_MAIN_NAME} PRIVATE
|
||||
.. ../connection ${TESTS_INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
|
||||
${CMAKE_BINARY_DIR}/generated/include
|
||||
)
|
||||
|
||||
target_compile_definitions(${V2G_MAIN_NAME} PRIVATE
|
||||
-DUNIT_TEST
|
||||
)
|
||||
|
||||
target_sources(${V2G_MAIN_NAME} PRIVATE
|
||||
../connection/connection.cpp
|
||||
../connection/tls_connection.cpp
|
||||
../tools.cpp
|
||||
../v2g_ctx.cpp
|
||||
log.cpp
|
||||
requirement.cpp
|
||||
v2g_main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${V2G_MAIN_NAME} PRIVATE
|
||||
cbv2g::din
|
||||
cbv2g::iso2
|
||||
cbv2g::tp
|
||||
everest::log
|
||||
everest::framework
|
||||
everest::evse_security
|
||||
everest::tls
|
||||
-levent -lpthread -levent_pthreads
|
||||
)
|
||||
|
||||
# runs fine locally, fails in CI
|
||||
add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME})
|
||||
ev_register_test_target(${TLS_GTEST_NAME})
|
||||
|
||||
|
||||
set(DIN_SERVER_NAME din_server_test)
|
||||
add_executable(${DIN_SERVER_NAME})
|
||||
|
||||
target_include_directories(${DIN_SERVER_NAME} PRIVATE
|
||||
.. ../connection ${TESTS_INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
|
||||
${CMAKE_BINARY_DIR}/generated/include
|
||||
)
|
||||
add_dependencies(${DIN_SERVER_NAME} generate_cpp_files)
|
||||
|
||||
target_compile_definitions(${DIN_SERVER_NAME} PRIVATE
|
||||
-DUNIT_TEST
|
||||
-DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL
|
||||
)
|
||||
|
||||
target_sources(${DIN_SERVER_NAME} PRIVATE
|
||||
din_server_test.cpp
|
||||
log.cpp
|
||||
../din_server.cpp
|
||||
../tools.cpp # TODO: Maybe mock this one
|
||||
)
|
||||
|
||||
target_link_libraries(${DIN_SERVER_NAME}
|
||||
PRIVATE
|
||||
GTest::gtest_main
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
cbv2g::din
|
||||
cbv2g::iso2
|
||||
cbv2g::tp
|
||||
everest::framework
|
||||
everest::evse_security
|
||||
everest::tls
|
||||
)
|
||||
|
||||
add_test(${DIN_SERVER_NAME} ${DIN_SERVER_NAME})
|
||||
ev_register_test_target(${DIN_SERVER_NAME})
|
||||
|
||||
set(SDP_NAME sdp_test)
|
||||
add_executable(${SDP_NAME})
|
||||
target_include_directories(${SDP_NAME} PRIVATE
|
||||
.. ../connection ${TESTS_INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
|
||||
${CMAKE_BINARY_DIR}/generated/include
|
||||
)
|
||||
add_dependencies(${SDP_NAME} generate_cpp_files)
|
||||
|
||||
target_compile_definitions(${SDP_NAME} PRIVATE
|
||||
-DUNIT_TEST
|
||||
)
|
||||
|
||||
target_sources(${SDP_NAME} PRIVATE
|
||||
sdp_test.cpp
|
||||
log.cpp
|
||||
../sdp.cpp
|
||||
../tools.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${SDP_NAME}
|
||||
PRIVATE
|
||||
GTest::gtest_main
|
||||
cbv2g::tp
|
||||
everest::framework
|
||||
everest::tls
|
||||
)
|
||||
|
||||
add_test(${SDP_NAME} ${SDP_NAME})
|
||||
ev_register_test_target(${SDP_NAME})
|
||||
|
||||
set(V2GCTX_NAME v2g_ctx_test)
|
||||
add_executable(${V2GCTX_NAME})
|
||||
|
||||
target_include_directories(${V2GCTX_NAME} PRIVATE
|
||||
.. ../connection ${TESTS_INCLUDE_DIR}
|
||||
${GENERATED_INCLUDE_DIR}
|
||||
${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME}
|
||||
${CMAKE_BINARY_DIR}/generated/include
|
||||
)
|
||||
add_dependencies(${V2GCTX_NAME} generate_cpp_files)
|
||||
|
||||
target_compile_definitions(${V2GCTX_NAME} PRIVATE
|
||||
-DUNIT_TEST
|
||||
-DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL
|
||||
)
|
||||
|
||||
target_sources(${V2GCTX_NAME} PRIVATE
|
||||
v2g_ctx_test.cpp
|
||||
log.cpp
|
||||
tools_test.cpp
|
||||
../v2g_ctx.cpp
|
||||
../tools.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(${V2GCTX_NAME}
|
||||
PRIVATE
|
||||
GTest::gtest_main
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
cbv2g::din
|
||||
cbv2g::iso2
|
||||
cbv2g::tp
|
||||
everest::framework
|
||||
everest::evse_security
|
||||
everest::tls
|
||||
-levent -lpthread -levent_pthreads
|
||||
)
|
||||
|
||||
add_test(${V2GCTX_NAME} ${V2GCTX_NAME})
|
||||
ev_register_test_target(${V2GCTX_NAME})
|
||||
@@ -0,0 +1,121 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef ISO15118_CHARGERIMPLSTUB_H_
|
||||
#define ISO15118_CHARGERIMPLSTUB_H_
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#include "ModuleAdapterStub.hpp"
|
||||
|
||||
#include <generated/interfaces/ISO15118_charger/Implementation.hpp>
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
namespace module::stub {
|
||||
|
||||
struct ISO15118_chargerImplStub : public ISO15118_chargerImplBase {
|
||||
ISO15118_chargerImplStub(ModuleAdapterStub& adapter) : ISO15118_chargerImplBase(&adapter, "EvseV2G"){};
|
||||
ISO15118_chargerImplStub(ModuleAdapterStub* adapter) : ISO15118_chargerImplBase(adapter, "EvseV2G"){};
|
||||
|
||||
virtual void init() {
|
||||
}
|
||||
virtual void ready() {
|
||||
}
|
||||
|
||||
virtual void handle_setup(types::iso15118::EVSEID& evse_id, types::iso15118::SaeJ2847BidiMode& sae_j2847_mode,
|
||||
bool& debug_mode) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_setup called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_energy_transfer_modes(
|
||||
std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_energy_transfer_modes called" << std::endl;
|
||||
}
|
||||
virtual void handle_set_charging_parameters(types::iso15118::SetupPhysicalValues& physical_values) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_set_charging_parameters called" << std::endl;
|
||||
}
|
||||
virtual void handle_session_setup(std::vector<types::iso15118::PaymentOption>& payment_options,
|
||||
bool& supported_certificate_service, bool& central_contract_validation_allowed) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_session_setup called" << std::endl;
|
||||
}
|
||||
virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status,
|
||||
types::authorization::CertificateStatus& certificate_status) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_authorization_response called" << std::endl;
|
||||
}
|
||||
virtual void handle_bpt_setup(types::iso15118::BptSetup& config) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_bpt_setup called" << std::endl;
|
||||
}
|
||||
virtual void handle_set_powersupply_capabilities(types::power_supply_DC::Capabilities& capabilities) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_set_powersupply_capabilities called" << std::endl;
|
||||
}
|
||||
virtual void handle_ac_contactor_closed(bool& status) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_ac_contactor_closed called" << std::endl;
|
||||
}
|
||||
virtual void handle_dlink_ready(bool& value) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_dlink_ready called" << std::endl;
|
||||
}
|
||||
virtual void handle_cable_check_finished(bool& status) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_cable_check_finished called" << std::endl;
|
||||
}
|
||||
virtual void handle_receipt_is_required(bool& receipt_required) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_receipt_is_required called" << std::endl;
|
||||
}
|
||||
virtual void handle_stop_charging(bool& stop) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_stop_charging called" << std::endl;
|
||||
}
|
||||
virtual void handle_pause_charging(bool& pause) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_pause_charging called" << std::endl;
|
||||
}
|
||||
virtual void handle_no_energy_pause_charging(types::iso15118::NoEnergyPauseMode& mode) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_no_energy_pause_charging called" << std::endl;
|
||||
}
|
||||
virtual bool
|
||||
handle_update_supported_app_protocols(types::iso15118::SupportedAppProtocols& supported_app_protocols) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_supported_app_protocols called" << std::endl;
|
||||
return true;
|
||||
}
|
||||
virtual void handle_update_ac_max_current(double& max_current) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_ac_max_current called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_ac_parameters(types::iso15118::AcParameters& ac_parameters) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_ac_parameters called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_ac_maximum_limits(types::iso15118::AcEvseMaximumPower& maximum_limits) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_ac_maximum_limits called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_ac_minimum_limits(types::iso15118::AcEvseMinimumPower& minimum_limits) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_ac_minimum_limits called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_ac_target_values(types::iso15118::AcTargetValues& target_values) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_ac_target_values called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_ac_present_power(types::units::Power& present_power) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_ac_present_power called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_dc_maximum_limits(types::iso15118::DcEvseMaximumLimits& maximum_limits) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_dc_maximum_limits called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_dc_minimum_limits(types::iso15118::DcEvseMinimumLimits& minimum_limits) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_dc_minimum_limits called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_isolation_status(types::iso15118::IsolationStatus& isolation_status) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_isolation_status called" << std::endl;
|
||||
}
|
||||
virtual void
|
||||
handle_update_dc_present_values(types::iso15118::DcEvsePresentVoltageCurrent& present_voltage_current) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_dc_present_values called" << std::endl;
|
||||
}
|
||||
virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_update_meter_info called" << std::endl;
|
||||
}
|
||||
virtual void handle_send_error(types::iso15118::EvseError& error) {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_send_error called" << std::endl;
|
||||
}
|
||||
virtual void handle_reset_error() {
|
||||
std::cout << "ISO15118_chargerImplBase::handle_reset_error called" << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace module::stub
|
||||
|
||||
#endif // ISO15118_CHARGERIMPLSTUB_H_
|
||||
51
tools/EVerest-main/modules/EVSE/EvseV2G/tests/README.md
Normal file
51
tools/EVerest-main/modules/EVSE/EvseV2G/tests/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
# Tests
|
||||
|
||||
Building tests:
|
||||
|
||||
```sh
|
||||
$ cd EVerest
|
||||
$ mkdir build
|
||||
$ cd build
|
||||
$ cmake -GNinja -DEVEREST_CORE_BUILD_TESTING=ON ..
|
||||
$ ninja install
|
||||
```
|
||||
|
||||
`touch release.json` may be needed if it hasn't been created
|
||||
(then re-run `ninja install`).
|
||||
|
||||
## Run EVerest in SIL
|
||||
|
||||
1. start MQTT broker
|
||||
2. from `build/run-scripts` run `./run-sil-dc-tls.sh`
|
||||
3. from `build/run-scripts` run `./nodered-sil-dc.sh`
|
||||
4. open web browser [EVerest Node-RED dashboard](http://localhost:1880/ui/)
|
||||
|
||||
## Unit tests
|
||||
|
||||
- `./v2g_openssl_test`
|
||||
- automatically runs `pki.sh`
|
||||
- run from the directory containing the executable
|
||||
|
||||
### Standalone V2G TLS server
|
||||
|
||||
Tests the Server class via the functions in connection.cpp and
|
||||
tls_connection.cpp.
|
||||
|
||||
- `./v2g_server -i <interface name>`
|
||||
- connects to IPv6 only with a link local address
|
||||
- requires `boost` library so LD_LIBRARY_PATH may need to be set
|
||||
- displays the address it is listening on. e.g.
|
||||
`[fe80::ae91:a1ff:fec9:a947%3]:64109`
|
||||
- supports multiple connections
|
||||
- gracefully terminates after 80 seconds
|
||||
- `valgrind` can be used to check memory allocations
|
||||
(has some leaks - possibly in v2g_ctx_start_events thread)
|
||||
- requires client certificate
|
||||
- s_client echos back what is typed with a delay since V2G has a long timeout
|
||||
|
||||
The connect argument must match what was displayed by `v2g_server`
|
||||
|
||||
```sh
|
||||
openssl s_client -connect [fe80::ae91:a1ff:fec9:a947%3]:64109 -verify 2 -CAfile server_root_cert.pem -cert client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname evse.pionix.de -status
|
||||
```
|
||||
@@ -0,0 +1,563 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <din_server.hpp>
|
||||
|
||||
#include <cstring>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "ISO15118_chargerImplStub.hpp"
|
||||
#include "cbv2g/din/din_msgDefDatatypes.h"
|
||||
#include "utest_log.hpp"
|
||||
#include "v2g.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
void publish_dc_ev_maximum_limits(struct v2g_context* ctx, const float& v2g_dc_ev_max_current_limit,
|
||||
const unsigned int& v2g_dc_ev_max_current_limit_is_used,
|
||||
const float& v2g_dc_ev_max_power_limit,
|
||||
const unsigned int& v2g_dc_ev_max_power_limit_is_used,
|
||||
const float& v2g_dc_ev_max_voltage_limit,
|
||||
const unsigned int& v2g_dc_ev_max_voltage_limit_is_used) {
|
||||
}
|
||||
|
||||
void stop_timer(struct event** event_timer, char const* const timer_name, struct v2g_context* ctx) {
|
||||
}
|
||||
|
||||
void log_selected_energy_transfer_type(int selected_energy_transfer_mode) {
|
||||
}
|
||||
|
||||
uint64_t v2g_session_id_from_exi(bool is_iso, void* exi_in) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void publish_dc_ev_target_voltage_current(struct v2g_context* ctx, const float& v2g_dc_ev_target_voltage,
|
||||
const float& v2g_dc_ev_target_current) {
|
||||
}
|
||||
|
||||
void publish_dc_ev_remaining_time(struct v2g_context* ctx, const float& v2g_dc_ev_remaining_time_to_full_soc,
|
||||
const unsigned int& v2g_dc_ev_remaining_time_to_full_soc_is_used,
|
||||
const float& v2g_dc_ev_remaining_time_to_bulk_soc,
|
||||
const unsigned int& v2g_dc_ev_remaining_time_to_bulk_soc_is_used) {
|
||||
}
|
||||
|
||||
namespace {
|
||||
class DinServerTest : public testing::Test {
|
||||
protected:
|
||||
std::unique_ptr<v2g_connection> conn;
|
||||
std::unique_ptr<v2g_context> ctx;
|
||||
std::unique_ptr<din_exiDocument> exi_in;
|
||||
std::unique_ptr<din_exiDocument> exi_out;
|
||||
|
||||
module::stub::ModuleAdapterStub adapter;
|
||||
module::stub::ISO15118_chargerImplStub charger;
|
||||
|
||||
DinServerTest() : charger(adapter) {
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
conn = std::make_unique<v2g_connection>();
|
||||
ctx = std::make_unique<v2g_context>();
|
||||
exi_in = std::make_unique<din_exiDocument>();
|
||||
exi_out = std::make_unique<din_exiDocument>();
|
||||
|
||||
module::stub::clear_logs();
|
||||
conn->ctx = ctx.get();
|
||||
conn->ctx->p_charger = &charger;
|
||||
|
||||
conn->exi_in.dinEXIDocument = exi_in.get();
|
||||
conn->exi_out.dinEXIDocument = exi_out.get();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
}
|
||||
};
|
||||
|
||||
class DinServerTestValidateResponseCode
|
||||
: public DinServerTest,
|
||||
public testing::WithParamInterface<
|
||||
std::tuple<int /*din_responseCodeType*/, bool, bool, bool, int /*V2gMsgTypeId*/, uint64_t, uint64_t, bool>> {
|
||||
};
|
||||
|
||||
// For all test cases:
|
||||
// TODO: Define helper functions to set the conn and ctx variables
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// Potential test for SessionSetup:
|
||||
// Bad Case:
|
||||
// Setting no EvseID -> A check should be added -> But a default value is in ctx provided.
|
||||
TEST_F(DinServerTest, session_setup_generating_new_session_id) {
|
||||
// Setting up session_setup_req
|
||||
auto& session_setup_req = exi_in->V2G_Message.Body.SessionSetupReq;
|
||||
exi_in->V2G_Message.Body.SessionSetupReq_isUsed = true;
|
||||
init_din_SessionSetupReqType(&session_setup_req);
|
||||
|
||||
const uint8_t evcc_id[8] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11};
|
||||
memcpy(session_setup_req.EVCCID.bytes, evcc_id, sizeof(evcc_id));
|
||||
session_setup_req.EVCCID.bytesLen = sizeof(evcc_id);
|
||||
|
||||
// Setting up conn
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG;
|
||||
|
||||
ctx->evse_v2g_data.session_id = 0;
|
||||
ctx->evse_v2g_data.date_time_now_is_used = 0;
|
||||
|
||||
ctx->ev_v2g_data.received_session_id = 0;
|
||||
|
||||
std::string evse_id = std::string("DE*PNX*TET1*234");
|
||||
strcpy(reinterpret_cast<char*>(ctx->evse_v2g_data.evse_id.bytes), evse_id.data());
|
||||
ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size();
|
||||
|
||||
// Setting up session_setup_res
|
||||
auto& session_setup_res = exi_out->V2G_Message.Body.SessionSetupRes;
|
||||
exi_out->V2G_Message.Body.SessionSetupRes_isUsed = 1u;
|
||||
init_din_SessionSetupResType(&session_setup_res);
|
||||
|
||||
EXPECT_EQ(states::handle_din_session_setup(conn.get()), V2G_EVENT_NO_EVENT);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 0);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 3);
|
||||
|
||||
EXPECT_EQ(session_setup_res.DateTimeNow_isUsed, false);
|
||||
// Checking if session id is generated
|
||||
EXPECT_GT(ctx->evse_v2g_data.session_id, 0);
|
||||
// Checking if evse id was set correctly
|
||||
EXPECT_EQ(evse_id, std::string(reinterpret_cast<char*>(session_setup_res.EVSEID.bytes)));
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, session_setup_old_session_id) {
|
||||
// Setting up session_setup_req
|
||||
auto& session_setup_req = exi_in->V2G_Message.Body.SessionSetupReq;
|
||||
exi_in->V2G_Message.Body.SessionSetupReq_isUsed = true;
|
||||
init_din_SessionSetupReqType(&session_setup_req);
|
||||
|
||||
const uint8_t evcc_id[8] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11};
|
||||
memcpy(session_setup_req.EVCCID.bytes, evcc_id, sizeof(evcc_id));
|
||||
session_setup_req.EVCCID.bytesLen = sizeof(evcc_id);
|
||||
|
||||
// Setting up conn
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG;
|
||||
|
||||
ctx->evse_v2g_data.session_id = 4158610156;
|
||||
ctx->evse_v2g_data.date_time_now_is_used = 0;
|
||||
|
||||
ctx->ev_v2g_data.received_session_id = 0;
|
||||
|
||||
std::string evse_id = std::string("DE*PNX*TET1*234");
|
||||
strcpy(reinterpret_cast<char*>(ctx->evse_v2g_data.evse_id.bytes), evse_id.data());
|
||||
ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size();
|
||||
|
||||
// Setting up session_setup_res
|
||||
auto& session_setup_res = exi_out->V2G_Message.Body.SessionSetupRes;
|
||||
exi_out->V2G_Message.Body.SessionSetupRes_isUsed = 1u;
|
||||
init_din_SessionSetupResType(&session_setup_res);
|
||||
|
||||
EXPECT_EQ(states::handle_din_session_setup(conn.get()), V2G_EVENT_NO_EVENT);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 0);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 2);
|
||||
|
||||
EXPECT_EQ(session_setup_res.DateTimeNow_isUsed, false);
|
||||
// Checking if session id is generated
|
||||
EXPECT_EQ(ctx->evse_v2g_data.session_id, 4158610156);
|
||||
// Checking if evse id was set correctly
|
||||
EXPECT_EQ(evse_id, std::string(reinterpret_cast<char*>(session_setup_res.EVSEID.bytes)));
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, session_setup_datetime_is_used) {
|
||||
// Setting up session_setup_req
|
||||
auto& session_setup_req = exi_in->V2G_Message.Body.SessionSetupReq;
|
||||
exi_in->V2G_Message.Body.SessionSetupReq_isUsed = true;
|
||||
init_din_SessionSetupReqType(&session_setup_req);
|
||||
|
||||
const uint8_t evcc_id[8] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11};
|
||||
memcpy(session_setup_req.EVCCID.bytes, evcc_id, sizeof(evcc_id));
|
||||
session_setup_req.EVCCID.bytesLen = sizeof(evcc_id);
|
||||
|
||||
// Setting up conn
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG;
|
||||
|
||||
ctx->evse_v2g_data.session_id = 0;
|
||||
ctx->evse_v2g_data.date_time_now_is_used = true;
|
||||
|
||||
ctx->ev_v2g_data.received_session_id = 0;
|
||||
|
||||
std::string evse_id = std::string("DE*PNX*TET1*234");
|
||||
strcpy(reinterpret_cast<char*>(ctx->evse_v2g_data.evse_id.bytes), evse_id.data());
|
||||
ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size();
|
||||
|
||||
// Setting up session_setup_res
|
||||
auto& session_setup_res = exi_out->V2G_Message.Body.SessionSetupRes;
|
||||
exi_out->V2G_Message.Body.SessionSetupRes_isUsed = 1u;
|
||||
init_din_SessionSetupResType(&session_setup_res);
|
||||
|
||||
EXPECT_EQ(states::handle_din_session_setup(conn.get()), V2G_EVENT_NO_EVENT);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 0);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 3);
|
||||
|
||||
EXPECT_EQ(session_setup_res.DateTimeNow_isUsed, true);
|
||||
EXPECT_GT(session_setup_res.DateTimeNow, 0);
|
||||
// Checking if session id is generated
|
||||
EXPECT_GT(ctx->evse_v2g_data.session_id, 0);
|
||||
// Checking if evse id was set correctly
|
||||
EXPECT_EQ(evse_id, std::string(reinterpret_cast<char*>(session_setup_res.EVSEID.bytes)));
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_service_discovery_good_case) {
|
||||
|
||||
// TODO(sl): Maybe add this to check exi_out proberly
|
||||
exi_out->V2G_Message.Body.ServiceDiscoveryRes_isUsed = true;
|
||||
init_din_ServiceDiscoveryResType(&exi_out->V2G_Message.Body.ServiceDiscoveryRes);
|
||||
|
||||
// TODO: Setting the correct session_id + received_session_id via functions
|
||||
|
||||
EXPECT_EQ(states::handle_din_service_discovery(conn.get()), V2G_EVENT_NO_EVENT);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 1);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, handle_din_contract_authentication_check_evse_processing_finished) {
|
||||
|
||||
// TODO: set a prober session id
|
||||
ctx->evse_v2g_data.session_id = 0;
|
||||
ctx->evse_v2g_data.date_time_now_is_used = 0;
|
||||
|
||||
ctx->current_v2g_msg = V2G_AUTHORIZATION_MSG;
|
||||
ctx->ev_v2g_data.received_session_id = 0;
|
||||
|
||||
ctx->evse_v2g_data.evse_processing[PHASE_AUTH] = 0;
|
||||
EXPECT_EQ(states::handle_din_contract_authentication(conn.get()), V2G_EVENT_NO_EVENT); // TODO
|
||||
|
||||
auto& res = exi_out->V2G_Message.Body.ContractAuthenticationRes;
|
||||
|
||||
// EXPECT_EQ(res.ResponseCode, din_responseCodeType_OK);
|
||||
EXPECT_EQ(res.EVSEProcessing, din_EVSEProcessingType_Finished);
|
||||
EXPECT_EQ(ctx->state, WAIT_FOR_CHARGEPARAMETERDISCOVERY);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 1);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, handle_din_contract_authentication_check_evse_processing_ongoing) {
|
||||
|
||||
// TODO: set a prober session id
|
||||
ctx->evse_v2g_data.session_id = 0;
|
||||
ctx->evse_v2g_data.date_time_now_is_used = 0;
|
||||
|
||||
ctx->current_v2g_msg = V2G_AUTHORIZATION_MSG;
|
||||
ctx->ev_v2g_data.received_session_id = 0;
|
||||
|
||||
ctx->evse_v2g_data.evse_processing[PHASE_AUTH] = 1;
|
||||
EXPECT_EQ(states::handle_din_contract_authentication(conn.get()), V2G_EVENT_NO_EVENT); // TODO
|
||||
|
||||
auto& res = exi_out->V2G_Message.Body.ContractAuthenticationRes;
|
||||
|
||||
// EXPECT_EQ(res.ResponseCode, din_responseCodeType_OK);
|
||||
EXPECT_EQ(res.EVSEProcessing, din_EVSEProcessingType_Ongoing);
|
||||
EXPECT_EQ(ctx->state, WAIT_FOR_AUTHORIZATION);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 1);
|
||||
EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 0);
|
||||
}
|
||||
|
||||
// if not otherwise specified, the following testcases are happy paths
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_TERMINATE_CONNECTION) {
|
||||
|
||||
// which response code is actually irrelevant here and was picked at random
|
||||
auto tmp = din_responseCodeType_FAILED_TariffSelectionInvalid;
|
||||
|
||||
// only this bool determines the outcome
|
||||
ctx->is_connection_terminated = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_TERMINATE_CONNECTION);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_failed_response_FAILED) {
|
||||
|
||||
// which response code is actually irrelevant here and was picked at random
|
||||
// FAILED code
|
||||
auto tmp = din_responseCodeType_FAILED_ChallengeInvalid;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = false;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_failed_response_OK) {
|
||||
|
||||
// which response code is actually irrelevant here and was picked at random
|
||||
// OK code
|
||||
auto tmp = din_responseCodeType_OK;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = false;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK) {
|
||||
|
||||
// which response code is actually irrelevant here and was picked at random
|
||||
// OK code
|
||||
auto tmp = din_responseCodeType_OK;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 2;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_1) {
|
||||
|
||||
// OK code
|
||||
auto tmp = din_responseCodeType_OK;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = true; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 2;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_2) {
|
||||
|
||||
// OK code
|
||||
auto tmp = din_responseCodeType_OK;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = true;
|
||||
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 2;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_3) {
|
||||
|
||||
// OK code
|
||||
auto tmp = din_responseCodeType_OK;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_CERTIFICATE_INSTALLATION_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 2;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_4) {
|
||||
|
||||
// OK code
|
||||
auto tmp = din_responseCodeType_OK;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = true;
|
||||
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 6;
|
||||
ctx->ev_v2g_data.received_session_id = 6;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_1) {
|
||||
|
||||
auto tmp = din_responseCodeType_FAILED_WrongEnergyTransferType;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = true; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_METERING_RECEIPT_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 6;
|
||||
ctx->ev_v2g_data.received_session_id = 6;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_2) {
|
||||
|
||||
auto tmp = din_responseCodeType_FAILED_MeteringSignatureNotValid;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = true;
|
||||
|
||||
ctx->current_v2g_msg = V2G_CHARGING_STATUS_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 6;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_3) {
|
||||
|
||||
auto tmp = din_responseCodeType_OK_CertificateExpiresSoon;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_UNKNOWN_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 1;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_4) {
|
||||
|
||||
auto tmp = din_responseCodeType_OK_CertificateExpiresSoon;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_UNKNOWN_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 1;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_5) {
|
||||
|
||||
auto tmp = din_responseCodeType_OK_CertificateExpiresSoon;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_CABLE_CHECK_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 2;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_6) {
|
||||
|
||||
auto tmp = din_responseCodeType_FAILED_SequenceError;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->stop_hlc = false; //||
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // &&
|
||||
ctx->evse_v2g_data.session_id = 1;
|
||||
ctx->ev_v2g_data.received_session_id = 2;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE);
|
||||
}
|
||||
TEST_F(DinServerTest, din_validate_response_code_V2G_DC_390) {
|
||||
// The response message shall contain the ResponseCode “FAILED_SequenceError” if the
|
||||
// SECC has received an unexpected request message.
|
||||
|
||||
auto given_response_code = din_responseCodeType_OK;
|
||||
constexpr auto expected_response_code = din_responseCodeType_FAILED_SequenceError;
|
||||
constexpr auto expected_response = V2G_EVENT_NO_EVENT;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = false;
|
||||
|
||||
ctx->current_v2g_msg = V2G_UNKNOWN_MSG;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&given_response_code, conn.get()), expected_response);
|
||||
// given response code should change in the function call
|
||||
EXPECT_EQ(given_response_code, expected_response_code);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_V2G_DC_391) {
|
||||
// The response message shall contain the ResponseCode “FAILED_UnknownSession” if the
|
||||
// SessionID in a request message does not match the SessionID provided by the SECC in the SessionSetupRes
|
||||
// message.
|
||||
|
||||
auto given_response_code = din_responseCodeType_OK;
|
||||
constexpr auto expected_response_code = din_responseCodeType_FAILED_UnknownSession;
|
||||
constexpr auto expected_response = V2G_EVENT_NO_EVENT;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = false;
|
||||
ctx->evse_v2g_data.session_id = 1234;
|
||||
ctx->ev_v2g_data.received_session_id = 5678;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&given_response_code, conn.get()), expected_response);
|
||||
// given response code should change in the function call
|
||||
EXPECT_EQ(given_response_code, expected_response_code);
|
||||
}
|
||||
|
||||
TEST_F(DinServerTest, din_validate_response_code_V2G_DC_665) {
|
||||
// If the SECC receives a request message that it expects according to the message sequence
|
||||
// specified in this chapter, and if the SECC cannot process this request message, e. g. due to
|
||||
// errors in the message parameters or due to impeding conditions in the EVSE, the SECC
|
||||
// shall:
|
||||
// [1.] without any delay, carry out an “EVSE-initiated emergency shutdown” as specified in
|
||||
// IEC 61851-23, which includes turning off the CP oscillator, if it is turned on,
|
||||
// [2.] respond with the corresponding response message with parameter ResponseCode
|
||||
// equal to “FAILED”, if possible, and
|
||||
// [3.] close the TCP connection according to [V2G-DC-116].
|
||||
|
||||
auto given_response_code = din_responseCodeType_FAILED;
|
||||
constexpr auto min_expected_response_code = din_responseCodeType_FAILED;
|
||||
constexpr auto expected_response = V2G_EVENT_SEND_AND_TERMINATE;
|
||||
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
ctx->terminate_connection_on_failed_response = true;
|
||||
|
||||
EXPECT_EQ(utils::din_validate_response_code(&given_response_code, conn.get()), expected_response);
|
||||
EXPECT_GE(given_response_code, min_expected_response_code);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef EVSE_SECURITYINTFSTUB_H_
|
||||
#define EVSE_SECURITYINTFSTUB_H_
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "ModuleAdapterStub.hpp"
|
||||
#include "generated/types/evse_security.hpp"
|
||||
#include "utils/types.hpp"
|
||||
#include <functional>
|
||||
#include <generated/interfaces/evse_security/Interface.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
namespace module::stub {
|
||||
|
||||
class evse_securityIntfStub : public evse_securityIntf {
|
||||
private:
|
||||
std::map<const std::string, Result (evse_securityIntfStub::*)(const Requirement& req, const Parameters& args)>
|
||||
functions;
|
||||
|
||||
public:
|
||||
evse_securityIntfStub(ModuleAdapterStub* adapter) :
|
||||
evse_securityIntf(adapter, Requirement{"", 0}, "EvseSecurity", std::nullopt) {
|
||||
functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file;
|
||||
functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info;
|
||||
}
|
||||
evse_securityIntfStub(ModuleAdapterStub& adapter) :
|
||||
evse_securityIntf(&adapter, Requirement{"", 0}, "EvseSecurity", std::nullopt) {
|
||||
functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file;
|
||||
functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info;
|
||||
}
|
||||
|
||||
virtual Result call_fn(const Requirement& req, const std::string& str, Parameters args) {
|
||||
if (auto it = functions.find(str); it != functions.end()) {
|
||||
return std::invoke(it->second, this, req, args);
|
||||
}
|
||||
std::printf("call_fn (%s)\n", str.c_str());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
virtual Result get_verify_file(const Requirement& req, const Parameters& args) {
|
||||
std::cout << "evse_securityIntf::get_verify_file called" << std::endl;
|
||||
return "";
|
||||
}
|
||||
|
||||
virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) {
|
||||
std::cout << "evse_securityIntf::get_leaf_certificate_info called" << std::endl;
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace module::stub
|
||||
|
||||
#endif // EVSE_SECURITYINTFSTUB_H_
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef ISO15118_EXTENTSIONSIMPLSTUB_H
|
||||
#define ISO15118_EXTENTSIONSIMPLSTUB_H
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <generated/interfaces/iso15118_extensions/Implementation.hpp>
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
namespace module::stub {
|
||||
|
||||
class iso15118_extensionsImplStub : public iso15118_extensionsImplBase {
|
||||
public:
|
||||
iso15118_extensionsImplStub() : iso15118_extensionsImplBase(nullptr, "EvseV2G"){};
|
||||
|
||||
virtual void init() {
|
||||
}
|
||||
virtual void ready() {
|
||||
}
|
||||
|
||||
virtual void handle_set_get_certificate_response(types::iso15118::ResponseExiStreamStatus& certificate_response) {
|
||||
std::cout << "iso15118_extensionsImplBase::handle_set_get_certificate_response called" << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace module::stub
|
||||
|
||||
#endif // ISO15118_EXTENTSIONSIMPLSTUB_H
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef ISO15118_VASINTFSTUB_H_
|
||||
#define ISO15118_VASINTFSTUB_H_
|
||||
|
||||
#include "ModuleAdapterStub.hpp"
|
||||
#include <generated/interfaces/ISO15118_vas/Interface.hpp>
|
||||
|
||||
namespace module::stub {
|
||||
|
||||
class iso15118_vasIntfStub : public ISO15118_vasIntf {
|
||||
public:
|
||||
explicit iso15118_vasIntfStub(ModuleAdapterStub& adapter) :
|
||||
ISO15118_vasIntf(&adapter, Requirement{"", 0}, "EvseV2G", std::nullopt) {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace module::stub
|
||||
|
||||
#endif // ISO15118_VASINTFSTUB_H_
|
||||
45
tools/EVerest-main/modules/EVSE/EvseV2G/tests/log.cpp
Normal file
45
tools/EVerest-main/modules/EVSE/EvseV2G/tests/log.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "utest_log.hpp"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <map>
|
||||
|
||||
namespace {
|
||||
std::map<dloglevel_t, std::vector<std::string>> logged_events;
|
||||
|
||||
void add_log(dloglevel_t loglevel, const std::string& event) {
|
||||
logged_events[loglevel].push_back(event);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace module::stub {
|
||||
std::vector<std::string>& get_logs(dloglevel_t loglevel) {
|
||||
return logged_events[loglevel];
|
||||
}
|
||||
|
||||
void clear_logs() {
|
||||
logged_events.clear();
|
||||
}
|
||||
|
||||
} // namespace module::stub
|
||||
|
||||
void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname,
|
||||
const char* format, ...) {
|
||||
va_list ap;
|
||||
std::array<char, 256> buffer;
|
||||
va_start(ap, format);
|
||||
std::size_t len = std::vsnprintf(buffer.data(), buffer.size(), format, ap);
|
||||
va_end(ap);
|
||||
if (len > 0) {
|
||||
auto s_len = std::min(len, buffer.size());
|
||||
std::string event{buffer.data(), s_len};
|
||||
(void)std::fprintf(stderr, "log: %s\n", event.c_str());
|
||||
add_log(loglevel, event);
|
||||
}
|
||||
}
|
||||
165
tools/EVerest-main/modules/EVSE/EvseV2G/tests/openssl_test.cpp
Normal file
165
tools/EVerest-main/modules/EVSE/EvseV2G/tests/openssl_test.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "crypto_common.hpp"
|
||||
#include "gtest/gtest.h"
|
||||
#include <crypto_openssl.hpp>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <everest/tls/openssl_util.hpp>
|
||||
#include <iso_server.hpp>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/pem.h>
|
||||
|
||||
#include <cbv2g/common/exi_bitstream.h>
|
||||
#include <cbv2g/exi_v2gtp.h> //for V2GTP_HEADER_LENGTHs
|
||||
#include <cbv2g/iso_2/iso2_msgDefDatatypes.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefDecoder.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefEncoder.h>
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T> constexpr void setCharacters(T& dest, const std::string& s) {
|
||||
dest.charactersLen = s.size();
|
||||
std::memcpy(&dest.characters[0], s.c_str(), s.size());
|
||||
}
|
||||
|
||||
template <typename T> constexpr void setBytes(T& dest, const std::uint8_t* b, std::size_t len) {
|
||||
dest.bytesLen = len;
|
||||
std::memcpy(&dest.bytes[0], b, len);
|
||||
}
|
||||
|
||||
struct test_vectors_t {
|
||||
const char* input;
|
||||
const std::uint8_t digest[32];
|
||||
};
|
||||
|
||||
#if 0
|
||||
// not used, useful to keep all the exi message test values together
|
||||
constexpr std::uint8_t sign_test[] = {0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
||||
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55};
|
||||
|
||||
constexpr test_vectors_t sha_256_test[] = {
|
||||
{"", {0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
||||
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}},
|
||||
{"abc", {0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
|
||||
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad}}};
|
||||
|
||||
// Test vectors from ISO 15118-2 Section J.2
|
||||
// checked okay (see iso_priv.pem)
|
||||
constexpr std::uint8_t iso_private_key[] = {0xb9, 0x13, 0x49, 0x63, 0xf5, 0x1c, 0x44, 0x14, 0x73, 0x84, 0x35,
|
||||
0x05, 0x7f, 0x97, 0xbb, 0xf1, 0x01, 0x0c, 0xab, 0xcb, 0x8d, 0xbd,
|
||||
0xe9, 0xc5, 0xd4, 0x81, 0x38, 0x39, 0x6a, 0xa9, 0x4b, 0x9d};
|
||||
// checked okay (see iso_priv.pem)
|
||||
constexpr std::uint8_t iso_public_key[] = {0x43, 0xe4, 0xfc, 0x4c, 0xcb, 0x64, 0x39, 0x04, 0x27, 0x9c, 0x7a, 0x5e, 0x65,
|
||||
0x76, 0xb3, 0x23, 0xe5, 0x5e, 0xc7, 0x9f, 0xf0, 0xe5, 0xa4, 0x05, 0x6e, 0x33,
|
||||
0x40, 0x84, 0xcb, 0xc3, 0x36, 0xff, 0x46, 0xe4, 0x4c, 0x1a, 0xdd, 0xf6, 0x91,
|
||||
0x62, 0xe5, 0x19, 0x2c, 0x2a, 0x83, 0xfc, 0x2b, 0xca, 0x9d, 0x8f, 0x46, 0xec,
|
||||
0xf4, 0xb7, 0x80, 0x67, 0xc2, 0x47, 0x6f, 0x6b, 0x3f, 0x34, 0x60, 0x0e};
|
||||
#endif
|
||||
|
||||
// EXI AuthorizationReq: checked okay (hash computes correctly)
|
||||
constexpr std::uint8_t iso_exi_a[] = {0x80, 0x04, 0x01, 0x52, 0x51, 0x0c, 0x40, 0x82, 0x9b, 0x7b, 0x6b, 0x29, 0x02,
|
||||
0x93, 0x0b, 0x73, 0x23, 0x7b, 0x69, 0x02, 0x23, 0x0b, 0xa3, 0x09, 0xe8};
|
||||
|
||||
// checked okay
|
||||
constexpr std::uint8_t iso_exi_a_hash[] = {0xd1, 0xb5, 0xe0, 0x3d, 0x00, 0x65, 0xbe, 0xe5, 0x6b, 0x31, 0x79,
|
||||
0x84, 0x45, 0x30, 0x51, 0xeb, 0x54, 0xca, 0x18, 0xfc, 0x0e, 0x09,
|
||||
0x16, 0x17, 0x4f, 0x8b, 0x3c, 0x77, 0xa9, 0x8f, 0x4a, 0xa9};
|
||||
|
||||
#if 0
|
||||
// not used, useful to keep all the exi message test values together
|
||||
// EXI AuthorizationReq signature block: checked okay (hash computes correctly)
|
||||
constexpr std::uint8_t iso_exi_b[] = {
|
||||
0x80, 0x81, 0x12, 0xb4, 0x3a, 0x3a, 0x38, 0x1d, 0x17, 0x97, 0xbb, 0xbb, 0xbb, 0x97, 0x3b, 0x99, 0x97, 0x37, 0xb9,
|
||||
0x33, 0x97, 0xaa, 0x29, 0x17, 0xb1, 0xb0, 0xb7, 0x37, 0xb7, 0x34, 0xb1, 0xb0, 0xb6, 0x16, 0xb2, 0xbc, 0x34, 0x97,
|
||||
0xa1, 0xab, 0x43, 0xa3, 0xa3, 0x81, 0xd1, 0x79, 0x7b, 0xbb, 0xbb, 0xb9, 0x73, 0xb9, 0x99, 0x73, 0x7b, 0x93, 0x39,
|
||||
0x79, 0x91, 0x81, 0x81, 0x89, 0x79, 0x81, 0xa1, 0x7b, 0xc3, 0x6b, 0x63, 0x23, 0x9b, 0x4b, 0x39, 0x6b, 0x6b, 0x7b,
|
||||
0x93, 0x29, 0x1b, 0x2b, 0x1b, 0x23, 0x9b, 0x09, 0x6b, 0x9b, 0x43, 0x09, 0x91, 0xa9, 0xb2, 0x20, 0x62, 0x34, 0x94,
|
||||
0x43, 0x10, 0x25, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72,
|
||||
0x67, 0x2f, 0x54, 0x52, 0x2f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x2d, 0x65, 0x78, 0x69, 0x2f,
|
||||
0x48, 0x52, 0xd0, 0xe8, 0xe8, 0xe0, 0x74, 0x5e, 0x5e, 0xee, 0xee, 0xee, 0x5c, 0xee, 0x66, 0x5c, 0xde, 0xe4, 0xce,
|
||||
0x5e, 0x64, 0x60, 0x60, 0x62, 0x5e, 0x60, 0x68, 0x5e, 0xf0, 0xda, 0xd8, 0xca, 0xdc, 0xc6, 0x46, 0xe6, 0xd0, 0xc2,
|
||||
0x64, 0x6a, 0x6c, 0x84, 0x1a, 0x36, 0xbc, 0x07, 0xa0, 0x0c, 0xb7, 0xdc, 0xad, 0x66, 0x2f, 0x30, 0x88, 0xa6, 0x0a,
|
||||
0x3d, 0x6a, 0x99, 0x43, 0x1f, 0x81, 0xc1, 0x22, 0xc2, 0xe9, 0xf1, 0x67, 0x8e, 0xf5, 0x31, 0xe9, 0x55, 0x23, 0x70};
|
||||
#endif
|
||||
|
||||
// checked okay
|
||||
constexpr std::uint8_t iso_exi_b_hash[] = {0xa4, 0xe9, 0x03, 0xe1, 0x82, 0x43, 0x04, 0x1b, 0x55, 0x4e, 0x11,
|
||||
0x64, 0x7e, 0x10, 0x1e, 0xd2, 0x5f, 0xc9, 0xf2, 0x15, 0x2a, 0xf4,
|
||||
0x67, 0x40, 0x14, 0xfe, 0x2a, 0xde, 0xac, 0x1e, 0x1c, 0xf7};
|
||||
|
||||
// checked okay (verifies iso_exi_b_hash with iso_priv.pem)
|
||||
constexpr std::uint8_t iso_exi_sig[] = {0x4c, 0x8f, 0x20, 0xc1, 0x40, 0x0b, 0xa6, 0x76, 0x06, 0xaa, 0x48, 0x11, 0x57,
|
||||
0x2a, 0x2f, 0x1a, 0xd3, 0xc1, 0x50, 0x89, 0xd9, 0x54, 0x20, 0x36, 0x34, 0x30,
|
||||
0xbb, 0x26, 0xb4, 0x9d, 0xb1, 0x04, 0xf0, 0x8d, 0xfa, 0x8b, 0xf8, 0x05, 0x5e,
|
||||
0x63, 0xa4, 0xb7, 0x5a, 0x8d, 0x31, 0x69, 0x20, 0x6f, 0xa8, 0xd5, 0x43, 0x08,
|
||||
0xba, 0x58, 0xf0, 0x56, 0x6b, 0x96, 0xba, 0xf6, 0x92, 0xce, 0x59, 0x50};
|
||||
|
||||
#if 0
|
||||
// not used, useful to keep all the exi message test values together
|
||||
const char iso_exi_a_hash_b64[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=";
|
||||
const char iso_exi_a_hash_b64_nl[] = "0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk=\n";
|
||||
|
||||
const char iso_exi_sig_b64[] =
|
||||
"TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBvqNVDCLpY8FZrlrr2ks5ZUA==";
|
||||
const char iso_exi_sig_b64_nl[] =
|
||||
"TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n";
|
||||
#endif
|
||||
|
||||
TEST(openssl, verifyIso) {
|
||||
auto* bio = BIO_new_file("iso_priv.pem", "r");
|
||||
ASSERT_NE(bio, nullptr);
|
||||
auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
ASSERT_NE(pkey, nullptr);
|
||||
BIO_free(bio);
|
||||
|
||||
auto sig = openssl::bn_to_signature(&iso_exi_sig[0], &iso_exi_sig[32]);
|
||||
EXPECT_TRUE(openssl::verify(pkey, sig.get(), sig.size(), &iso_exi_b_hash[0], sizeof(iso_exi_b_hash)));
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
TEST(isoExi, signature) {
|
||||
// The message is:
|
||||
// header { SessionID, Signature}
|
||||
// body { AuthorizationReq }
|
||||
// the test vector doesn't include the entire encoded message
|
||||
|
||||
auto* bio = BIO_new_file("iso_priv.pem", "r");
|
||||
ASSERT_NE(bio, nullptr);
|
||||
auto* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
ASSERT_NE(pkey, nullptr);
|
||||
BIO_free(bio);
|
||||
|
||||
// decode the test vector AuthorizationReq
|
||||
struct iso2_exiFragment exi_a {};
|
||||
init_iso2_exiFragment(&exi_a);
|
||||
init_iso2_AuthorizationReqType(&exi_a.AuthorizationReq);
|
||||
|
||||
exi_bitstream_t stream;
|
||||
exi_bitstream_init(&stream, const_cast<std::uint8_t*>(&iso_exi_a[0]), sizeof(iso_exi_a), 0, nullptr);
|
||||
EXPECT_EQ(decode_iso2_exiFragment(&stream, &exi_a), 0);
|
||||
|
||||
// manually populate the Signature structure
|
||||
struct iso2_SignatureType sig {};
|
||||
init_iso2_SignatureType(&sig);
|
||||
|
||||
// SignedInfo
|
||||
setCharacters(sig.SignedInfo.CanonicalizationMethod.Algorithm, "http://www.w3.org/TR/canonical-exi/");
|
||||
setCharacters(sig.SignedInfo.SignatureMethod.Algorithm, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256");
|
||||
sig.SignedInfo.Reference.arrayLen = 1;
|
||||
sig.SignedInfo.Reference.array[0].URI_isUsed = 1;
|
||||
setCharacters(sig.SignedInfo.Reference.array[0].URI, "#ID1");
|
||||
sig.SignedInfo.Reference.array[0].Transforms_isUsed = 1;
|
||||
setCharacters(sig.SignedInfo.Reference.array[0].Transforms.Transform.Algorithm,
|
||||
"http://www.w3.org/TR/canonical-exi/");
|
||||
setCharacters(sig.SignedInfo.Reference.array[0].DigestMethod.Algorithm, "http://www.w3.org/2001/04/xmlenc#sha256");
|
||||
setBytes(sig.SignedInfo.Reference.array[0].DigestValue, &iso_exi_a_hash[0], ::openssl::sha_256_digest_size);
|
||||
// SignatureValue
|
||||
setBytes(sig.SignatureValue.CONTENT, &iso_exi_sig[0], ::openssl::signature_size);
|
||||
EXPECT_TRUE(crypto::openssl::check_iso2_signature(&sig, pkey, &exi_a));
|
||||
|
||||
EVP_PKEY_free(pkey);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "utils/types.hpp"
|
||||
|
||||
bool operator<(const Requirement& lhs, const Requirement& rhs) {
|
||||
return true;
|
||||
}
|
||||
104
tools/EVerest-main/modules/EVSE/EvseV2G/tests/sdp_test.cpp
Normal file
104
tools/EVerest-main/modules/EVSE/EvseV2G/tests/sdp_test.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
#include <sdp.hpp>
|
||||
#include <tools.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
class SdpTest : public testing::Test {
|
||||
protected:
|
||||
SdpTest() {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SdpTest, sdp_set_dlink_ready_sets_flag) {
|
||||
v2g_context ctx{};
|
||||
EXPECT_FALSE(ctx.sdp_dlink_ready);
|
||||
|
||||
sdp_set_dlink_ready(&ctx, true);
|
||||
EXPECT_TRUE(ctx.sdp_dlink_ready);
|
||||
|
||||
sdp_set_dlink_ready(&ctx, false);
|
||||
EXPECT_FALSE(ctx.sdp_dlink_ready);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, sdp_set_dlink_ready_captures_timestamp) {
|
||||
v2g_context ctx{};
|
||||
EXPECT_EQ(ctx.sdp_dlink_ready_time.load(), 0);
|
||||
|
||||
sdp_set_dlink_ready(&ctx, true);
|
||||
EXPECT_GT(ctx.sdp_dlink_ready_time.load(), 0);
|
||||
|
||||
sdp_set_dlink_ready(&ctx, false);
|
||||
EXPECT_EQ(ctx.sdp_dlink_ready_time.load(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, sdp_set_dlink_ready_timestamp_is_monotonic) {
|
||||
v2g_context ctx{};
|
||||
|
||||
sdp_set_dlink_ready(&ctx, true);
|
||||
long long int first = ctx.sdp_dlink_ready_time.load();
|
||||
|
||||
sdp_set_dlink_ready(&ctx, false);
|
||||
sdp_set_dlink_ready(&ctx, true);
|
||||
long long int second = ctx.sdp_dlink_ready_time.load();
|
||||
|
||||
EXPECT_GE(second, first);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, sdp_write_header) {
|
||||
uint8_t buffer[20];
|
||||
uint16_t payload_type = 0x9001;
|
||||
uint32_t length = 367;
|
||||
|
||||
EXPECT_EQ(sdp_write_header(buffer, payload_type, length), 8);
|
||||
|
||||
EXPECT_EQ(buffer[0], 0x01);
|
||||
EXPECT_EQ(buffer[1], 0xFE);
|
||||
EXPECT_EQ(buffer[2], 0x90);
|
||||
EXPECT_EQ(buffer[3], 0x01);
|
||||
EXPECT_EQ(buffer[4], 0x00);
|
||||
EXPECT_EQ(buffer[5], 0x00);
|
||||
EXPECT_EQ(buffer[6], 0x01);
|
||||
EXPECT_EQ(buffer[7], 0x6F);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, timeout_detected_when_elapsed_exceeds_limit) {
|
||||
v2g_context ctx{};
|
||||
// Simulate dlink becoming ready 18001ms ago
|
||||
long long int now = getmonotonictime();
|
||||
ctx.sdp_dlink_ready_time = now - (V2G_COMMUNICATION_SETUP_TIMEOUT + 1);
|
||||
|
||||
long long int elapsed = now - ctx.sdp_dlink_ready_time.load();
|
||||
EXPECT_GE(elapsed, V2G_COMMUNICATION_SETUP_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, timeout_not_detected_when_elapsed_below_limit) {
|
||||
v2g_context ctx{};
|
||||
// Simulate dlink becoming ready just now
|
||||
ctx.sdp_dlink_ready_time = getmonotonictime();
|
||||
|
||||
long long int elapsed = getmonotonictime() - ctx.sdp_dlink_ready_time.load();
|
||||
EXPECT_LT(elapsed, V2G_COMMUNICATION_SETUP_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, timeout_cancelled_when_connection_initiated) {
|
||||
v2g_context ctx{};
|
||||
sdp_set_dlink_ready(&ctx, true);
|
||||
EXPECT_NE(ctx.sdp_dlink_ready_time.load(), 0);
|
||||
|
||||
// Simulate connection established — timeout should be cancelled
|
||||
ctx.connection_initiated = true;
|
||||
long long int dlink_ready_time = ctx.sdp_dlink_ready_time.load();
|
||||
if (ctx.connection_initiated && dlink_ready_time != 0) {
|
||||
ctx.sdp_dlink_ready_time = 0;
|
||||
}
|
||||
EXPECT_EQ(ctx.sdp_dlink_ready_time.load(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SdpTest, v2g_communication_setup_timeout_is_18000) {
|
||||
EXPECT_EQ(V2G_COMMUNICATION_SETUP_TIMEOUT, 18000);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
36
tools/EVerest-main/modules/EVSE/EvseV2G/tests/tools_test.cpp
Normal file
36
tools/EVerest-main/modules/EVSE/EvseV2G/tests/tools_test.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <gtest/gtest.h>
|
||||
#include <tools.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T, std::size_t S> constexpr auto macStr(const T (&arg)[S]) {
|
||||
return to_mac_address_str(reinterpret_cast<const uint8_t*>(arg), S - 1);
|
||||
}
|
||||
|
||||
TEST(to_mac_address_str, various) {
|
||||
auto result = to_mac_address_str(nullptr, 0);
|
||||
EXPECT_TRUE(result.empty());
|
||||
const char* txt = "123";
|
||||
result = to_mac_address_str(reinterpret_cast<const uint8_t*>(txt), 0);
|
||||
EXPECT_TRUE(result.empty());
|
||||
|
||||
result = macStr("");
|
||||
EXPECT_TRUE(result.empty());
|
||||
result = macStr("12345678901234567"); // too long
|
||||
EXPECT_TRUE(result.empty());
|
||||
|
||||
result = macStr("A");
|
||||
EXPECT_EQ(result, "41");
|
||||
result = macStr("AB");
|
||||
EXPECT_EQ(result, "41:42");
|
||||
result = macStr("ABM");
|
||||
EXPECT_EQ(result, "41:42:4D");
|
||||
result = macStr("\xac\x91\xa1\x56\x5f\x46");
|
||||
EXPECT_EQ(result, "AC:91:A1:56:5F:46");
|
||||
result = macStr("1234567890123456"); // max length
|
||||
EXPECT_EQ(result, "31:32:33:34:35:36:37:38:39:30:31:32:33:34:35:36");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
15
tools/EVerest-main/modules/EVSE/EvseV2G/tests/utest_log.hpp
Normal file
15
tools/EVerest-main/modules/EVSE/EvseV2G/tests/utest_log.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <log.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace module::stub {
|
||||
std::vector<std::string>& get_logs(dloglevel_t loglevel);
|
||||
void clear_logs();
|
||||
|
||||
} // namespace module::stub
|
||||
162
tools/EVerest-main/modules/EVSE/EvseV2G/tests/v2g_ctx_test.cpp
Normal file
162
tools/EVerest-main/modules/EVSE/EvseV2G/tests/v2g_ctx_test.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include <memory>
|
||||
#include <v2g_ctx.hpp>
|
||||
|
||||
#include "ISO15118_chargerImplStub.hpp"
|
||||
#include "ModuleAdapterStub.hpp"
|
||||
#include "evse_securityIntfStub.hpp"
|
||||
#include "iso15118_extensionsImplStub.hpp"
|
||||
#include "iso15118_vasIntfStub.hpp"
|
||||
#include "utest_log.hpp"
|
||||
#include "v2g.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
|
||||
struct v2g_contextDeleter {
|
||||
void operator()(v2g_context* ptr) const {
|
||||
v2g_ctx_free(ptr);
|
||||
};
|
||||
};
|
||||
|
||||
class V2gCtxTest : public testing::Test {
|
||||
protected:
|
||||
std::unique_ptr<v2g_context, v2g_contextDeleter> ctx;
|
||||
module::stub::QuietModuleAdapterStub adapter;
|
||||
module::stub::ISO15118_chargerImplStub charger;
|
||||
module::stub::evse_securityIntfStub security;
|
||||
module::stub::iso15118_extensionsImplStub extensions;
|
||||
module::stub::iso15118_vasIntfStub vas_item;
|
||||
|
||||
V2gCtxTest() : charger(adapter), security(adapter), vas_item(adapter) {
|
||||
}
|
||||
|
||||
void v2g_ctx_init_charging_state_cleared() {
|
||||
// checks try to match the order is v2g.hpp
|
||||
|
||||
EXPECT_EQ(ctx->last_v2g_msg, V2G_UNKNOWN_MSG);
|
||||
EXPECT_EQ(ctx->current_v2g_msg, V2G_UNKNOWN_MSG);
|
||||
EXPECT_EQ(ctx->state, 0);
|
||||
|
||||
// not changed
|
||||
// is_dc_charger
|
||||
// debugMode
|
||||
// supported_protocols
|
||||
|
||||
EXPECT_EQ(ctx->selected_protocol, V2G_UNKNOWN_PROTOCOL);
|
||||
EXPECT_FALSE(ctx->intl_emergency_shutdown);
|
||||
EXPECT_FALSE(ctx->stop_hlc);
|
||||
|
||||
// ctx->is_connection_terminated is updated rather than cleared
|
||||
|
||||
// not changed
|
||||
// terminate_connection_on_failed_response
|
||||
// contactor_is_closed
|
||||
|
||||
// many items in session not reset
|
||||
EXPECT_FALSE(ctx->session.renegotiation_required);
|
||||
EXPECT_FALSE(ctx->session.is_charging);
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
auto ptr = v2g_ctx_create(&charger, &extensions, &security, {&vas_item});
|
||||
ctx = std::unique_ptr<v2g_context, v2g_contextDeleter>(ptr, v2g_contextDeleter());
|
||||
module::stub::clear_logs();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
}
|
||||
};
|
||||
|
||||
TEST(RunFirst, v2g_ctx_init_charging_values) {
|
||||
// must not be part of V2gCtxTest
|
||||
// V2gCtxTest::SetUp() creates the v2g_context which would be the 1st
|
||||
// call to v2g_ctx_init_charging_values()
|
||||
|
||||
// only called from v2g_ctx_init_charging_session()
|
||||
// which is called from v2g_ctx_create()
|
||||
|
||||
// note v2g_ctx_init_charging_values() has a static bool so it
|
||||
// performs different tidyup after the first time it is called
|
||||
|
||||
v2g_context ctx;
|
||||
ctx.evse_v2g_data.charge_service.FreeService = 9;
|
||||
v2g_ctx_init_charging_values(&ctx);
|
||||
EXPECT_EQ(ctx.evse_v2g_data.charge_service.FreeService, 0);
|
||||
ctx.evse_v2g_data.charge_service.FreeService = 10;
|
||||
v2g_ctx_init_charging_values(&ctx);
|
||||
EXPECT_EQ(ctx.evse_v2g_data.charge_service.FreeService, 10);
|
||||
|
||||
// reset back to a valid value as it will never be reset
|
||||
ctx.evse_v2g_data.charge_service.FreeService = 0;
|
||||
}
|
||||
|
||||
TEST_F(V2gCtxTest, v2g_ctx_init_charging_stateTrue) {
|
||||
// called on session start in v2g_handle_connection()
|
||||
|
||||
ctx->last_v2g_msg = V2G_CABLE_CHECK_MSG;
|
||||
ctx->current_v2g_msg = V2G_CHARGE_PARAMETER_DISCOVERY_MSG;
|
||||
ctx->state = 10;
|
||||
ctx->selected_protocol = V2G_PROTO_DIN70121;
|
||||
ctx->intl_emergency_shutdown = true;
|
||||
ctx->stop_hlc = true;
|
||||
ctx->session.renegotiation_required = true;
|
||||
ctx->session.is_charging = true;
|
||||
|
||||
v2g_ctx_init_charging_state(ctx.get(), true);
|
||||
|
||||
v2g_ctx_init_charging_state_cleared();
|
||||
EXPECT_TRUE(ctx->is_connection_terminated);
|
||||
}
|
||||
|
||||
TEST_F(V2gCtxTest, v2g_ctx_init_charging_stateFalse) {
|
||||
// called on session end in v2g_handle_connection()
|
||||
|
||||
ctx->last_v2g_msg = V2G_CABLE_CHECK_MSG;
|
||||
ctx->current_v2g_msg = V2G_CHARGE_PARAMETER_DISCOVERY_MSG;
|
||||
ctx->state = 10;
|
||||
ctx->selected_protocol = V2G_PROTO_DIN70121;
|
||||
ctx->intl_emergency_shutdown = true;
|
||||
ctx->stop_hlc = true;
|
||||
ctx->session.renegotiation_required = true;
|
||||
ctx->session.is_charging = true;
|
||||
|
||||
v2g_ctx_init_charging_state(ctx.get(), false);
|
||||
|
||||
v2g_ctx_init_charging_state_cleared();
|
||||
EXPECT_FALSE(ctx->is_connection_terminated);
|
||||
}
|
||||
|
||||
#if 0
|
||||
// v2g_ctx_init_charging_session() is a trivial implementation
|
||||
TEST_F(V2gCtxTest, v2g_ctx_init_charging_sessionTrue) {
|
||||
// called in connection_teardown()
|
||||
// calls v2g_ctx_init_charging_state
|
||||
// calls v2g_ctx_init_charging_values
|
||||
}
|
||||
|
||||
TEST_F(V2gCtxTest, v2g_ctx_init_charging_sessionFalse) {
|
||||
// called in connection_teardown()
|
||||
// calls v2g_ctx_init_charging_state
|
||||
// calls v2g_ctx_init_charging_values
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(valgrind, memcheck) {
|
||||
// v2g_ctx_free() now sets shutdown and joins the event thread,
|
||||
// so there is no use-after-free during cleanup.
|
||||
|
||||
// run via valgrind to ensure that malloc/free are working
|
||||
module::stub::QuietModuleAdapterStub adapter;
|
||||
module::stub::ISO15118_chargerImplStub charger(adapter);
|
||||
module::stub::evse_securityIntfStub security(adapter);
|
||||
module::stub::iso15118_extensionsImplStub extensions;
|
||||
module::stub::iso15118_vasIntfStub vas_item(adapter);
|
||||
|
||||
auto ptr = v2g_ctx_create(&charger, &extensions, &security, {&vas_item});
|
||||
v2g_ctx_free(ptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
175
tools/EVerest-main/modules/EVSE/EvseV2G/tests/v2g_main.cpp
Normal file
175
tools/EVerest-main/modules/EVSE/EvseV2G/tests/v2g_main.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/*
|
||||
* testing options
|
||||
* openssl s_client -connect [fe80::ae91:a1ff:fec9:a947%3]:64109 -verify 2 -CAfile server_root_cert.pem -cert
|
||||
* client_cert.pem -cert_chain client_chain.pem -key client_priv.pem -verify_return_error -verify_hostname
|
||||
* evse.pionix.de -status
|
||||
*/
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ISO15118_chargerImplStub.hpp"
|
||||
#include "ModuleAdapterStub.hpp"
|
||||
#include "evse_securityIntfStub.hpp"
|
||||
#include "iso15118_extensionsImplStub.hpp"
|
||||
#include "iso15118_vasIntfStub.hpp"
|
||||
|
||||
#include <connection.hpp>
|
||||
#include <everest/tls/tls.hpp>
|
||||
#include <v2g_ctx.hpp>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// needs to be in the global namespace
|
||||
int v2g_handle_connection(struct v2g_connection* conn) {
|
||||
assert(conn != nullptr);
|
||||
assert(conn->read != nullptr);
|
||||
assert(conn->write != nullptr);
|
||||
|
||||
std::array<unsigned char, 1024> buffer{};
|
||||
bool bExit = false;
|
||||
while (!bExit) {
|
||||
const ssize_t readbytes = conn->read(conn, buffer.data(), buffer.size());
|
||||
if (readbytes > 0) {
|
||||
const ssize_t writebytes = conn->write(conn, buffer.data(), readbytes);
|
||||
if (writebytes <= 0) {
|
||||
bExit = true;
|
||||
}
|
||||
} else if (readbytes < 0) {
|
||||
bExit = true;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const char* interface;
|
||||
|
||||
void parse_options(int argc, char** argv) {
|
||||
interface = nullptr;
|
||||
int c;
|
||||
|
||||
while ((c = getopt(argc, argv, "hi:")) != -1) {
|
||||
switch (c) {
|
||||
case 'i':
|
||||
interface = optarg;
|
||||
break;
|
||||
case 'h':
|
||||
case '?':
|
||||
std::cout << "Usage: " << argv[0] << " -i <interface name>" << std::endl;
|
||||
exit(1);
|
||||
break;
|
||||
default:
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
if (interface == nullptr) {
|
||||
std::cerr << "Error: " << argv[0] << " requires -i <interface name>" << std::endl;
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
// EvseSecurity "implementation"
|
||||
struct EvseSecurity : public module::stub::evse_securityIntfStub {
|
||||
EvseSecurity(module::stub::ModuleAdapterStub& adapter) : module::stub::evse_securityIntfStub(&adapter) {
|
||||
}
|
||||
|
||||
Result get_verify_file(const Requirement& req, const Parameters& args) override {
|
||||
return "client_root_cert.pem";
|
||||
}
|
||||
|
||||
virtual Result get_leaf_certificate_info(const Requirement& req, const Parameters& args) override {
|
||||
// using types::evse_security::CertificateHashDataType;
|
||||
using types::evse_security::CertificateInfo;
|
||||
using types::evse_security::CertificateOCSP;
|
||||
using types::evse_security::GetCertificateInfoResult;
|
||||
using types::evse_security::GetCertificateInfoStatus;
|
||||
using types::evse_security::HashAlgorithm;
|
||||
|
||||
CertificateInfo cert_info;
|
||||
cert_info.key = "server_priv.pem";
|
||||
cert_info.certificate = "server_chain.pem";
|
||||
cert_info.certificate_count = 2;
|
||||
cert_info.ocsp = {{
|
||||
{HashAlgorithm::SHA256},
|
||||
{"ocsp_response.der"},
|
||||
},
|
||||
{
|
||||
{HashAlgorithm::SHA256},
|
||||
{"ocsp_response.der"},
|
||||
}};
|
||||
|
||||
const GetCertificateInfoResult res = {
|
||||
GetCertificateInfoStatus::Accepted,
|
||||
cert_info,
|
||||
};
|
||||
json jres = res;
|
||||
return jres;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
parse_options(argc, argv);
|
||||
|
||||
tls::Server tls_server;
|
||||
module::stub::ModuleAdapterStub adapter;
|
||||
module::stub::ISO15118_chargerImplStub charger(adapter);
|
||||
EvseSecurity security(adapter);
|
||||
module::stub::iso15118_extensionsImplStub extensions;
|
||||
module::stub::iso15118_vasIntfStub vas_item(adapter);
|
||||
|
||||
auto* ctx = v2g_ctx_create(&charger, &extensions, &security, {&vas_item});
|
||||
|
||||
if (ctx == nullptr) {
|
||||
std::cerr << "failed to create context" << std::endl;
|
||||
} else {
|
||||
ctx->tls_server = &tls_server;
|
||||
ctx->if_name = interface;
|
||||
ctx->tls_security = TLS_SECURITY_FORCE;
|
||||
ctx->is_connection_terminated = false;
|
||||
|
||||
std::thread stop([ctx]() {
|
||||
// there is a 60 second read timeout in connection.cpp
|
||||
std::this_thread::sleep_for(75s);
|
||||
std::cout << "shutdown" << std::endl;
|
||||
ctx->is_connection_terminated = true;
|
||||
ctx->shutdown = true;
|
||||
});
|
||||
|
||||
std::cout << "connection_init" << std::endl;
|
||||
if (::connection_init(ctx) != 0) {
|
||||
std::cerr << "connection_init failed" << std::endl;
|
||||
} else {
|
||||
std::cout << "connection_init started" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "connection_start_servers " << std::endl;
|
||||
if (::connection_start_servers(ctx) != 0) {
|
||||
std::cerr << "connection_start_servers failed" << std::endl;
|
||||
} else {
|
||||
std::cout << "connection_start_servers started" << std::endl;
|
||||
}
|
||||
|
||||
stop.join();
|
||||
tls::ServerConnection::wait_all_closed();
|
||||
|
||||
// wait for v2g_ctx_start_events thread to stop
|
||||
std::this_thread::sleep_for(2s);
|
||||
v2g_ctx_free(ctx);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
244
tools/EVerest-main/modules/EVSE/EvseV2G/tools.cpp
Normal file
244
tools/EVerest-main/modules/EVSE/EvseV2G/tools.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#include "tools.hpp"
|
||||
#include "log.hpp"
|
||||
#include <algorithm>
|
||||
#include <arpa/inet.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <ifaddrs.h>
|
||||
#include <iomanip>
|
||||
#include <math.h>
|
||||
#include <sstream>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
ssize_t safe_read(int fd, void* buf, size_t count) {
|
||||
for (;;) {
|
||||
ssize_t result = read(fd, buf, count);
|
||||
|
||||
if (result >= 0)
|
||||
return result;
|
||||
else if (errno == EINTR)
|
||||
continue;
|
||||
else
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
int generate_random_data(void* dest, size_t dest_len) {
|
||||
size_t len = 0;
|
||||
int fd;
|
||||
|
||||
fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd == -1)
|
||||
return -1;
|
||||
|
||||
while (len < dest_len) {
|
||||
ssize_t rv = safe_read(fd, dest, dest_len);
|
||||
|
||||
if (rv < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
len += rv;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* choose_first_ipv6_interface() {
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
char buffer[INET6_ADDRSTRLEN];
|
||||
|
||||
if (getifaddrs(&ifaddr) == -1)
|
||||
return NULL;
|
||||
|
||||
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
|
||||
if (!ifa->ifa_addr)
|
||||
continue;
|
||||
|
||||
if (ifa->ifa_addr->sa_family == AF_INET6) {
|
||||
inet_ntop(AF_INET6, &ifa->ifa_addr->sa_data, buffer, sizeof(buffer));
|
||||
if (strstr(buffer, "fe80") != NULL) {
|
||||
return ifa->ifa_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
dlog(DLOG_LEVEL_ERROR, "No necessary IPv6 link-local address was found!");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int get_interface_ipv6_address(const char* if_name, enum Addr6Type type, struct sockaddr_in6* addr) {
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
int rv = -1;
|
||||
|
||||
// If using loopback device, accept any address
|
||||
// (lo usually does not have a link local address)
|
||||
if (strcmp(if_name, "lo") == 0) {
|
||||
type = ADDR6_TYPE_UNPSEC;
|
||||
}
|
||||
|
||||
if (getifaddrs(&ifaddr) == -1)
|
||||
return -1;
|
||||
|
||||
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
|
||||
if (!ifa->ifa_addr)
|
||||
continue;
|
||||
|
||||
if (ifa->ifa_addr->sa_family != AF_INET6)
|
||||
continue;
|
||||
|
||||
if (strcmp(ifa->ifa_name, if_name) != 0)
|
||||
continue;
|
||||
|
||||
/* on Linux the scope_id is interface index for link-local addresses */
|
||||
switch (type) {
|
||||
case ADDR6_TYPE_GLOBAL: /* no link-local address requested */
|
||||
if ((reinterpret_cast<struct sockaddr_in6*>(ifa->ifa_addr))->sin6_scope_id != 0)
|
||||
continue;
|
||||
break;
|
||||
|
||||
case ADDR6_TYPE_LINKLOCAL: /* link-local address requested */
|
||||
if ((reinterpret_cast<struct sockaddr_in6*>(ifa->ifa_addr))->sin6_scope_id == 0)
|
||||
continue;
|
||||
break;
|
||||
|
||||
default: /* any address of the interface requested */
|
||||
/* use first found */
|
||||
break;
|
||||
}
|
||||
|
||||
memcpy(addr, ifa->ifa_addr, sizeof(*addr));
|
||||
|
||||
rv = 0;
|
||||
goto out;
|
||||
}
|
||||
|
||||
out:
|
||||
freeifaddrs(ifaddr);
|
||||
return rv;
|
||||
}
|
||||
|
||||
#define NSEC_PER_SEC 1000000000L
|
||||
|
||||
void set_normalized_timespec(struct timespec* ts, time_t sec, int64_t nsec) {
|
||||
while (nsec >= NSEC_PER_SEC) {
|
||||
nsec -= NSEC_PER_SEC;
|
||||
++sec;
|
||||
}
|
||||
while (nsec < 0) {
|
||||
nsec += NSEC_PER_SEC;
|
||||
--sec;
|
||||
}
|
||||
ts->tv_sec = sec;
|
||||
ts->tv_nsec = nsec;
|
||||
}
|
||||
|
||||
struct timespec timespec_add(struct timespec lhs, struct timespec rhs) {
|
||||
struct timespec ts_delta;
|
||||
|
||||
set_normalized_timespec(&ts_delta, lhs.tv_sec + rhs.tv_sec, lhs.tv_nsec + rhs.tv_nsec);
|
||||
|
||||
return ts_delta;
|
||||
}
|
||||
|
||||
struct timespec timespec_sub(struct timespec lhs, struct timespec rhs) {
|
||||
struct timespec ts_delta;
|
||||
|
||||
set_normalized_timespec(&ts_delta, lhs.tv_sec - rhs.tv_sec, lhs.tv_nsec - rhs.tv_nsec);
|
||||
|
||||
return ts_delta;
|
||||
}
|
||||
|
||||
void timespec_add_ms(struct timespec* ts, long long msec) {
|
||||
long long sec = msec / 1000;
|
||||
|
||||
set_normalized_timespec(ts, ts->tv_sec + sec, ts->tv_nsec + (msec - sec * 1000) * 1000 * 1000);
|
||||
}
|
||||
|
||||
long long timespec_to_ms(struct timespec ts) {
|
||||
return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000);
|
||||
}
|
||||
|
||||
long long int getmonotonictime() {
|
||||
struct timespec time;
|
||||
clock_gettime(CLOCK_MONOTONIC, &time);
|
||||
return time.tv_sec * 1000 + time.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
double calc_physical_value(const int16_t& value, const int8_t& multiplier) {
|
||||
return static_cast<double>(value * pow(10.0, multiplier));
|
||||
}
|
||||
|
||||
types::iso15118::HashAlgorithm convert_to_hash_algorithm(const types::evse_security::HashAlgorithm hash_algorithm) {
|
||||
switch (hash_algorithm) {
|
||||
case types::evse_security::HashAlgorithm::SHA256:
|
||||
return types::iso15118::HashAlgorithm::SHA256;
|
||||
case types::evse_security::HashAlgorithm::SHA384:
|
||||
return types::iso15118::HashAlgorithm::SHA384;
|
||||
case types::evse_security::HashAlgorithm::SHA512:
|
||||
return types::iso15118::HashAlgorithm::SHA512;
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"Could not convert types::evse_security::HashAlgorithm to types::iso15118::HashAlgorithm");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<types::iso15118::CertificateHashDataInfo>
|
||||
convert_to_certificate_hash_data_info_vector(const types::evse_security::OCSPRequestDataList& ocsp_request_data_list) {
|
||||
std::vector<types::iso15118::CertificateHashDataInfo> certificate_hash_data_info_vec;
|
||||
for (const auto& ocsp_request_data : ocsp_request_data_list.ocsp_request_data_list) {
|
||||
if (ocsp_request_data.responder_url.has_value() and ocsp_request_data.certificate_hash_data.has_value()) {
|
||||
types::iso15118::CertificateHashDataInfo certificate_hash_data;
|
||||
certificate_hash_data.hashAlgorithm =
|
||||
convert_to_hash_algorithm(ocsp_request_data.certificate_hash_data.value().hash_algorithm);
|
||||
certificate_hash_data.issuerNameHash = ocsp_request_data.certificate_hash_data.value().issuer_name_hash;
|
||||
certificate_hash_data.issuerKeyHash = ocsp_request_data.certificate_hash_data.value().issuer_key_hash;
|
||||
certificate_hash_data.serialNumber = ocsp_request_data.certificate_hash_data.value().serial_number;
|
||||
certificate_hash_data.responderURL = ocsp_request_data.responder_url.value();
|
||||
certificate_hash_data_info_vec.push_back(certificate_hash_data);
|
||||
}
|
||||
}
|
||||
return certificate_hash_data_info_vec;
|
||||
}
|
||||
|
||||
std::string to_mac_address_str(const uint8_t* const ptr, size_t len) {
|
||||
std::string result;
|
||||
if ((ptr != nullptr) && (len > 0) && (len <= 16)) {
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
std::array<char, 4> buffer{};
|
||||
const auto res = snprintf(&buffer[0], sizeof(buffer), "%02X:", ptr[i]);
|
||||
if (res == 3) {
|
||||
result += std::string{buffer.data(), 3};
|
||||
}
|
||||
}
|
||||
if (!result.empty()) {
|
||||
result.pop_back();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void strncpy_to_v2g(char* characters, size_t size_of_characters, uint16_t* characters_len, const std::string& src) {
|
||||
// copy at max the size of the destination array but reserve one byte for trailing NUL byte;
|
||||
// this should not be needed, but just in case someone wants to access the field again
|
||||
// using standard string functions, then we are safe
|
||||
auto len = std::min(src.length(), size_of_characters - 1);
|
||||
|
||||
memcpy(characters, src.c_str(), len);
|
||||
characters[len] = '\0';
|
||||
|
||||
*characters_len = len;
|
||||
}
|
||||
93
tools/EVerest-main/modules/EVSE/EvseV2G/tools.hpp
Normal file
93
tools/EVerest-main/modules/EVSE/EvseV2G/tools.hpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#ifndef TOOLS_H
|
||||
#define TOOLS_H
|
||||
|
||||
#include <generated/types/evse_security.hpp>
|
||||
#include <generated/types/iso15118.hpp>
|
||||
#include <netinet/in.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <vector>
|
||||
|
||||
#define MAX_FILE_NAME_LENGTH 100
|
||||
#define MAX_PKI_CA_LENGTH 4 /* leaf up to root certificate */
|
||||
|
||||
#ifndef ARRAY_SIZE
|
||||
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
|
||||
#endif
|
||||
|
||||
#ifndef ROUND_UP
|
||||
#define ROUND_UP(N, S) ((((N) + (S)-1) / (S)) * (S))
|
||||
#endif
|
||||
|
||||
#ifndef ROUND_UP_ELEMENTS
|
||||
#define ROUND_UP_ELEMENTS(N, S) (((N) + (S)-1) / (S))
|
||||
#endif
|
||||
|
||||
int generate_random_data(void* dest, size_t dest_len);
|
||||
|
||||
enum Addr6Type {
|
||||
ADDR6_TYPE_UNPSEC = -1,
|
||||
ADDR6_TYPE_GLOBAL = 0,
|
||||
ADDR6_TYPE_LINKLOCAL = 1,
|
||||
};
|
||||
|
||||
const char* choose_first_ipv6_interface();
|
||||
int get_interface_ipv6_address(const char* if_name, enum Addr6Type type, struct sockaddr_in6* addr);
|
||||
|
||||
void set_normalized_timespec(struct timespec* ts, time_t sec, int64_t nsec);
|
||||
struct timespec timespec_sub(struct timespec lhs, struct timespec rhs);
|
||||
struct timespec timespec_add(struct timespec lhs, struct timespec rhs);
|
||||
void timespec_add_ms(struct timespec* ts, long long msec);
|
||||
long long timespec_to_ms(struct timespec ts);
|
||||
long long int getmonotonictime(void);
|
||||
|
||||
/*!
|
||||
* \brief calc_physical_value This function calculates the physical value consists on a value and multiplier.
|
||||
* \param value is the value of the physical value
|
||||
* \param multiplier is the multiplier of the physical value
|
||||
* \return Returns the physical value
|
||||
*/
|
||||
double calc_physical_value(const int16_t& value, const int8_t& multiplier);
|
||||
|
||||
/**
|
||||
* \brief convert the given \p hash_algorithm to type types::iso15118::HashAlgorithm
|
||||
* \param hash_algorithm
|
||||
* \return types::iso15118::HashAlgorithm
|
||||
*/
|
||||
types::iso15118::HashAlgorithm convert_to_hash_algorithm(const types::evse_security::HashAlgorithm hash_algorithm);
|
||||
|
||||
/**
|
||||
* \brief convert the given \p ocsp_request_data_list to std::vector<types::iso15118::CertificateHashDataInfo>
|
||||
* \param ocsp_request_data_list
|
||||
* \return std::vector<types::iso15118::CertificateHashDataInfo>
|
||||
*/
|
||||
std::vector<types::iso15118::CertificateHashDataInfo>
|
||||
convert_to_certificate_hash_data_info_vector(const types::evse_security::OCSPRequestDataList& ocsp_request_data_list);
|
||||
|
||||
/**
|
||||
* \brief convert bytes into a colon delimited string
|
||||
* \param[in] ptr - a pointer to the byte array
|
||||
* \param[in] len - the length of the byte array
|
||||
* \returns a string of hex digits separated by : or empty on error
|
||||
*/
|
||||
|
||||
std::string to_mac_address_str(const uint8_t* ptr, size_t len);
|
||||
|
||||
/**
|
||||
* \brief Copy the content of \p src to the character array pointed by \p characters.
|
||||
* \param characters The destination buffer (array)
|
||||
* \param size_of_characters The size of the destination (must not be zero)
|
||||
* \param characters_len Pointer to the corresponding string length field in the destination struct.
|
||||
* \param src The source string to use.
|
||||
*/
|
||||
void strncpy_to_v2g(char* characters, size_t size_of_characters, uint16_t* characters_len, const std::string& src);
|
||||
|
||||
#endif /* TOOLS_H */
|
||||
425
tools/EVerest-main/modules/EVSE/EvseV2G/v2g.hpp
Normal file
425
tools/EVerest-main/modules/EVSE/EvseV2G/v2g.hpp
Normal file
@@ -0,0 +1,425 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#ifndef V2G_H
|
||||
#define V2G_H
|
||||
|
||||
#include <generated/interfaces/ISO15118_charger/Implementation.hpp>
|
||||
#include <generated/interfaces/ISO15118_vas/Interface.hpp>
|
||||
#include <generated/interfaces/evse_security/Interface.hpp>
|
||||
#include <generated/interfaces/iso15118_extensions/Implementation.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <vector>
|
||||
|
||||
#include <everest/tls/openssl_util.hpp>
|
||||
#include <everest/tls/tls.hpp>
|
||||
|
||||
#include <cbv2g/app_handshake/appHand_Datatypes.h>
|
||||
#include <cbv2g/common/exi_basetypes.h>
|
||||
#include <cbv2g/common/exi_bitstream.h>
|
||||
#include <cbv2g/din/din_msgDefDatatypes.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefDatatypes.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/thread.h>
|
||||
|
||||
/* timeouts in milliseconds */
|
||||
#define V2G_SEQUENCE_TIMEOUT_60S 60000 /* [V2G2-443] et.al. */
|
||||
#define V2G_SEQUENCE_TIMEOUT_10S 10000
|
||||
#define V2G_CP_STATE_B_TO_C_D_TIMEOUT 250 /* [V2G2-847] */
|
||||
#define V2G_CP_STATE_B_TO_C_D_TIMEOUT_RELAXED 500 /* [V2G2-847] */
|
||||
#define V2G_CP_STATE_C_D_TO_B_TIMEOUT 250 /* [V2G2-848] */
|
||||
#define V2G_CONTACTOR_CLOSE_TIMEOUT 3000 /* [V2G2-862] [V2G2-865] 4.5 s for PowerDeliveryRes */
|
||||
#define V2G_COMMUNICATION_SETUP_TIMEOUT \
|
||||
18000 /* [V2G2-723] [V2G2-029] [V2G2-032] [V2G2-714] [V2G2-716] V2G_SECC_CommunicationSetup_Performance_Time */
|
||||
#define V2G_CPSTATE_DETECTION_TIMEOUT \
|
||||
1500 /* [V2G-DC-547] not (yet) defined for ISO and not implemented, but may be implemented */
|
||||
#define V2G_CPSTATE_DETECTION_TIMEOUT_RELAXED \
|
||||
3000 /* [V2G-DC-547] not (yet) defined for ISO and not implemented, but may be implemented */
|
||||
|
||||
#define SA_SCHEDULE_DURATION 86400
|
||||
|
||||
#define ISO_15118_2013_MSG_DEF "urn:iso:15118:2:2013:MsgDef"
|
||||
#define ISO_15118_2013_MAJOR 2
|
||||
#define ISO_15118_2013_MINOR 0
|
||||
|
||||
#define ISO_15118_2010_MSG_DEF "urn:iso:15118:2:2010:MsgDef"
|
||||
#define ISO_15118_2010_MAJOR 1
|
||||
|
||||
#define DIN_70121_MSG_DEF "urn:din:70121:2012:MsgDef"
|
||||
#define DIN_70121_MAJOR 2
|
||||
#define DIN_70121_MINOR 0
|
||||
|
||||
#define EVSE_LEAF_KEY_FILE_NAME "CPO_EVSE_LEAF.key"
|
||||
#define EVSE_PROV_KEY_FILE_NAME "PROV_LEAF.key"
|
||||
#define MO_ROOT_CRT_NAME "MO_ROOT_CRT"
|
||||
#define V2G_ROOT_CRT_NAME "V2G_ROOT_CRT"
|
||||
#define MAX_V2G_ROOT_CERTS 10
|
||||
#define MAX_KEY_PW_LEN 32
|
||||
#define FORCE_PUB_MSG 25 // max msg cycles when topics values must be udpated
|
||||
#define MAX_PCID_LEN 17
|
||||
|
||||
#define GEN_CHALLENGE_SIZE 16
|
||||
|
||||
#define DEFAULT_BUFFER_SIZE 8192
|
||||
|
||||
#define DEBUG 1
|
||||
|
||||
enum tls_security_level {
|
||||
TLS_SECURITY_ALLOW = 0,
|
||||
TLS_SECURITY_PROHIBIT,
|
||||
TLS_SECURITY_FORCE
|
||||
};
|
||||
|
||||
enum v2g_event {
|
||||
V2G_EVENT_NO_EVENT = 0,
|
||||
V2G_EVENT_TERMINATE_CONNECTION, // Terminate the connection immediately
|
||||
V2G_EVENT_SEND_AND_TERMINATE, // Send next msg and terminate the connection
|
||||
V2G_EVENT_SEND_RECV_EXI_MSG, // If msg must not be exi-encoded and can be sent directly
|
||||
V2G_EVENT_IGNORE_MSG // Received message can't be handled
|
||||
};
|
||||
|
||||
enum v2g_protocol {
|
||||
V2G_PROTO_DIN70121 = 0,
|
||||
V2G_PROTO_ISO15118_2010,
|
||||
V2G_PROTO_ISO15118_2013,
|
||||
V2G_PROTO_ISO15118_2015,
|
||||
V2G_UNKNOWN_PROTOCOL
|
||||
};
|
||||
|
||||
/* ISO 15118 table 105 */
|
||||
enum v2g_service {
|
||||
V2G_SERVICE_ID_CHARGING = 1,
|
||||
V2G_SERVICE_ID_CERTIFICATE = 2,
|
||||
V2G_SERVICE_ID_INTERNET = 3,
|
||||
V2G_SERVICE_ID_USECASEINFORMATION = 4,
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief The charging_phase enum to identify the actual charing phase.
|
||||
*/
|
||||
enum charging_phase {
|
||||
PHASE_INIT = 0,
|
||||
PHASE_AUTH,
|
||||
PHASE_PARAMETER,
|
||||
PHASE_ISOLATION,
|
||||
PHASE_PRECHARGE,
|
||||
PHASE_CHARGE,
|
||||
PHASE_WELDING,
|
||||
PHASE_STOP,
|
||||
PHASE_LENGTH
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief The res_msg_ids enum is a list of response msg ids
|
||||
*/
|
||||
enum V2gMsgTypeId {
|
||||
V2G_SUPPORTED_APP_PROTOCOL_MSG = 0,
|
||||
V2G_SESSION_SETUP_MSG,
|
||||
V2G_SERVICE_DISCOVERY_MSG,
|
||||
V2G_SERVICE_DETAIL_MSG,
|
||||
V2G_PAYMENT_SERVICE_SELECTION_MSG,
|
||||
V2G_PAYMENT_DETAILS_MSG,
|
||||
V2G_AUTHORIZATION_MSG,
|
||||
V2G_CHARGE_PARAMETER_DISCOVERY_MSG,
|
||||
V2G_METERING_RECEIPT_MSG,
|
||||
V2G_CERTIFICATE_UPDATE_MSG,
|
||||
V2G_CERTIFICATE_INSTALLATION_MSG,
|
||||
V2G_CHARGING_STATUS_MSG,
|
||||
V2G_CABLE_CHECK_MSG,
|
||||
V2G_PRE_CHARGE_MSG,
|
||||
V2G_POWER_DELIVERY_MSG,
|
||||
V2G_CURRENT_DEMAND_MSG,
|
||||
V2G_WELDING_DETECTION_MSG,
|
||||
V2G_SESSION_STOP_MSG,
|
||||
V2G_UNKNOWN_MSG
|
||||
};
|
||||
|
||||
/* EVSE ID */
|
||||
struct v2g_evse_id {
|
||||
uint8_t bytes[iso2_EVSEID_CHARACTER_SIZE];
|
||||
uint16_t bytesLen;
|
||||
};
|
||||
|
||||
/* Meter ID */
|
||||
struct v2g_meter_id {
|
||||
uint8_t bytes[iso2_MeterID_CHARACTER_SIZE];
|
||||
uint16_t bytesLen;
|
||||
};
|
||||
|
||||
/* Struct for tls-session-log-key tracing */
|
||||
typedef struct keylogDebugCtx {
|
||||
FILE* file;
|
||||
bool inClientRandom;
|
||||
bool inMasterSecret;
|
||||
uint8_t hexdumpLinesToProcess;
|
||||
int udp_socket;
|
||||
std::string udp_buffer;
|
||||
} keylogDebugCtx;
|
||||
|
||||
struct SAE_Bidi_Data {
|
||||
bool enabled_sae_v2h;
|
||||
bool enabled_sae_v2g;
|
||||
int8_t sae_v2h_minimal_soc;
|
||||
bool discharging;
|
||||
};
|
||||
|
||||
enum NoEnergyPauseStatus {
|
||||
None,
|
||||
AllowEvToIgnorePause,
|
||||
AfterCableCheckPreCharge,
|
||||
BeforeCableCheck,
|
||||
};
|
||||
|
||||
struct PowerCapabilities {
|
||||
iso2_PhysicalValueType max_current;
|
||||
iso2_PhysicalValueType min_current;
|
||||
iso2_PhysicalValueType max_power;
|
||||
iso2_PhysicalValueType max_voltage;
|
||||
iso2_PhysicalValueType min_voltage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstracts a charging port, i.e. a power outlet in this daemon.
|
||||
*
|
||||
* **** NOTE ****
|
||||
* Be very careful about adding C++ objects since constructors and
|
||||
* destructors are not called. (see v2g_ctx_create() and calloc)
|
||||
*/
|
||||
struct v2g_context {
|
||||
std::atomic_bool shutdown;
|
||||
|
||||
evse_securityIntf* r_security;
|
||||
std::vector<ISO15118_vasIntf*> r_vas;
|
||||
ISO15118_chargerImplBase* p_charger;
|
||||
iso15118_extensionsImplBase* p_extensions;
|
||||
|
||||
struct event_base* event_base;
|
||||
pthread_t event_thread;
|
||||
|
||||
const char* if_name;
|
||||
struct sockaddr_in6* local_tcp_addr;
|
||||
struct sockaddr_in6* local_tls_addr;
|
||||
|
||||
std::string tls_key_logging_path;
|
||||
|
||||
uint32_t network_read_timeout; /* in milli seconds */
|
||||
uint32_t network_read_timeout_tls; /* in milli seconds */
|
||||
|
||||
enum tls_security_level tls_security;
|
||||
|
||||
int sdp_socket;
|
||||
int tcp_socket;
|
||||
|
||||
int udp_port;
|
||||
int udp_socket;
|
||||
|
||||
pthread_t tcp_thread;
|
||||
|
||||
struct {
|
||||
int fd;
|
||||
} tls_socket;
|
||||
tls::Server* tls_server;
|
||||
|
||||
bool tls_key_logging;
|
||||
|
||||
pthread_mutex_t mqtt_lock;
|
||||
pthread_cond_t mqtt_cond;
|
||||
pthread_condattr_t mqtt_attr;
|
||||
|
||||
struct {
|
||||
float evse_ac_current_limit; // default is 0
|
||||
float evse_ac_nominal_current; // default is 0
|
||||
float evse_ac_nominal_voltage; // default is 230
|
||||
} basic_config; // This config will not reseted after beginning of a new charging session
|
||||
|
||||
/* actual charging state */
|
||||
enum V2gMsgTypeId last_v2g_msg; /* holds the current v2g msg type */
|
||||
enum V2gMsgTypeId current_v2g_msg; /* holds the last v2g msg type */
|
||||
int state; /* holds the current state id */
|
||||
bool is_dc_charger; /* Is set to true if it is a DC charger. Value is configured after configuration of the
|
||||
supported energy type */
|
||||
bool debugMode; /* To activate/deactivate the debug mode */
|
||||
std::atomic<int8_t>
|
||||
supported_protocols; /* Is an bit mask and holds the supported app protocols. See v2g_protocol enum */
|
||||
enum v2g_protocol selected_protocol; /* Holds the selected protocole after supported app protocol */
|
||||
std::atomic<bool>
|
||||
intl_emergency_shutdown; /* Is set to true if an internal emergency_shutdown has occurred (send failed response,
|
||||
configure emergency shutdown in EVSEStatus and close tcp connection) */
|
||||
std::atomic_bool stop_hlc; /* is set to true if a shutdown of the charging session should be initiated (send failed
|
||||
response and close tcp connection) */
|
||||
std::atomic_bool is_connection_terminated; /* Is set to true if the connection is terminated (CP State A/F, shutdown
|
||||
immediately without response message) */
|
||||
std::atomic<bool> terminate_connection_on_failed_response;
|
||||
std::atomic<bool> contactor_is_closed; /* Actual contactor state */
|
||||
|
||||
struct {
|
||||
bool meter_info_is_used;
|
||||
uint64_t meter_reading;
|
||||
struct v2g_meter_id meter_id;
|
||||
} meter_info;
|
||||
|
||||
struct {
|
||||
/* EVSE V2G values */
|
||||
uint64_t session_id; // Is the evse session id, generated by the evse. This id shall not change during a V2G
|
||||
// Communication Session.
|
||||
uint32_t notification_max_delay;
|
||||
uint8_t evse_isolation_status;
|
||||
unsigned int evse_isolation_status_is_used;
|
||||
uint8_t evse_notification;
|
||||
uint8_t evse_status_code[PHASE_LENGTH];
|
||||
uint8_t evse_processing[PHASE_LENGTH];
|
||||
struct v2g_evse_id evse_id;
|
||||
unsigned int date_time_now_is_used;
|
||||
struct iso2_ChargeServiceType charge_service;
|
||||
std::vector<iso2_ServiceType> evse_service_list;
|
||||
std::map<uint16_t, iso2_ServiceParameterListType> service_parameter_list;
|
||||
|
||||
struct iso2_SAScheduleListType evse_sa_schedule_list;
|
||||
bool evse_sa_schedule_list_is_used;
|
||||
|
||||
std::vector<iso2_paymentOptionType> payment_option_list;
|
||||
bool central_contract_validation_allowed;
|
||||
|
||||
bool cert_install_status;
|
||||
std::string cert_install_res_b64_buffer;
|
||||
|
||||
// AC parameter
|
||||
int rcd;
|
||||
int receipt_required;
|
||||
|
||||
// evse power electronic values
|
||||
struct iso2_PhysicalValueType evse_current_regulation_tolerance;
|
||||
unsigned int evse_current_regulation_tolerance_is_used;
|
||||
struct iso2_PhysicalValueType evse_energy_to_be_delivered;
|
||||
unsigned int evse_energy_to_be_delivered_is_used;
|
||||
struct iso2_PhysicalValueType evse_maximum_current_limit; // DC charging
|
||||
unsigned int evse_maximum_current_limit_is_used;
|
||||
int evse_current_limit_achieved;
|
||||
struct iso2_PhysicalValueType evse_maximum_power_limit;
|
||||
unsigned int evse_maximum_power_limit_is_used;
|
||||
int evse_power_limit_achieved;
|
||||
struct iso2_PhysicalValueType evse_maximum_voltage_limit;
|
||||
unsigned int evse_maximum_voltage_limit_is_used;
|
||||
int evse_voltage_limit_achieved;
|
||||
struct iso2_PhysicalValueType evse_minimum_current_limit;
|
||||
struct iso2_PhysicalValueType evse_minimum_voltage_limit;
|
||||
struct iso2_PhysicalValueType evse_peak_current_ripple;
|
||||
struct iso2_PhysicalValueType evse_present_voltage;
|
||||
struct iso2_PhysicalValueType evse_present_current;
|
||||
|
||||
/* AC only power electronic values */
|
||||
struct iso2_PhysicalValueType evse_nominal_voltage;
|
||||
|
||||
// Specific SAE J2847 bidi values
|
||||
struct SAE_Bidi_Data sae_bidi_data;
|
||||
|
||||
// No energy pause IEC61851-23:2023
|
||||
NoEnergyPauseStatus no_energy_pause{NoEnergyPauseStatus::None};
|
||||
|
||||
// Min and max limits from the dc powersupply
|
||||
PowerCapabilities power_capabilities{};
|
||||
|
||||
} evse_v2g_data;
|
||||
|
||||
struct {
|
||||
/* V2G session values */
|
||||
iso2_paymentOptionType iso_selected_payment_option;
|
||||
long long int auth_start_timeout;
|
||||
int auth_timeout_eim;
|
||||
int auth_timeout_pnc; // for PnC
|
||||
uint8_t gen_challenge[GEN_CHALLENGE_SIZE]; // for PnC
|
||||
bool verify_contract_cert_chain; // for PnC
|
||||
types::authorization::CertificateStatus certificate_status; // for PnC
|
||||
bool authorization_rejected; // for PnC
|
||||
std::optional<types::authorization::ProvidedIdToken> provided_id_token; // for PnC
|
||||
|
||||
bool renegotiation_required; /* Is set to true if renegotiation is required. Only relevant for ISO */
|
||||
bool is_charging; /* set to true if ChargeProgress is set to Start */
|
||||
uint8_t sa_schedule_tuple_id; /* selected SA schedule tuple ID*/
|
||||
} session;
|
||||
|
||||
struct {
|
||||
/* EV V2G values */
|
||||
int bulk_charging_complete;
|
||||
int charging_complete;
|
||||
uint64_t received_session_id; // Is the received ev session id transmitted over the v2g header. This id shall
|
||||
// not change during a V2G Communication Session.
|
||||
|
||||
union {
|
||||
struct din_DC_EVStatusType din_dc_ev_status;
|
||||
struct iso2_DC_EVStatusType iso2_dc_ev_status;
|
||||
};
|
||||
float ev_maximum_current_limit;
|
||||
float ev_maximum_power_limit;
|
||||
float ev_maximum_voltage_limit;
|
||||
float v2g_target_current;
|
||||
float v2g_target_voltage;
|
||||
float remaining_time_to_bulk_soc;
|
||||
float remaining_time_to_full_soc;
|
||||
} ev_v2g_data;
|
||||
|
||||
bool hlc_pause_active;
|
||||
|
||||
std::vector<std::vector<uint16_t>> supported_vas_services_per_provider;
|
||||
|
||||
bool connection_initiated;
|
||||
|
||||
bool sdp_dlink_ready{false};
|
||||
std::atomic<long long int> sdp_dlink_ready_time{0};
|
||||
};
|
||||
|
||||
enum class dLinkAction {
|
||||
D_LINK_ACTION_ERROR,
|
||||
D_LINK_ACTION_TERMINATE,
|
||||
D_LINK_ACTION_PAUSE
|
||||
};
|
||||
|
||||
/**
|
||||
* High-level abstraction of an incoming TCP/TLS connection on a certain charging port.
|
||||
*/
|
||||
struct v2g_connection {
|
||||
pthread_t thread_id;
|
||||
struct v2g_context* ctx;
|
||||
|
||||
bool is_tls_connection;
|
||||
bool tls_handshake_failed; /* true when accept() never returned success */
|
||||
V2gMsgTypeId last_v2g_msg_at_disconnect; /* last V2G message before connection teardown */
|
||||
|
||||
// used for non-TLS connections
|
||||
struct {
|
||||
int socket_fd;
|
||||
} conn;
|
||||
|
||||
tls::Connection* tls_connection;
|
||||
openssl::pkey_ptr* pubkey;
|
||||
|
||||
ssize_t (*read)(struct v2g_connection* conn, unsigned char* buf, std::size_t count);
|
||||
ssize_t (*write)(struct v2g_connection* conn, unsigned char* buf, std::size_t count);
|
||||
|
||||
/* V2GTP EXI encoding/decoding stuff */
|
||||
uint8_t* buffer;
|
||||
uint32_t payload_len;
|
||||
exi_bitstream_t stream;
|
||||
|
||||
struct appHand_exiDocument handshake_req;
|
||||
struct appHand_exiDocument handshake_resp;
|
||||
|
||||
union {
|
||||
struct din_exiDocument* dinEXIDocument;
|
||||
struct iso2_exiDocument* iso2EXIDocument;
|
||||
} exi_in;
|
||||
|
||||
union {
|
||||
struct din_exiDocument* dinEXIDocument;
|
||||
struct iso2_exiDocument* iso2EXIDocument;
|
||||
} exi_out;
|
||||
|
||||
dLinkAction d_link_action; /* signaled data-link action after connection is closed */
|
||||
};
|
||||
|
||||
#endif /* V2G_H */
|
||||
577
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_ctx.cpp
Normal file
577
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_ctx.cpp
Normal file
@@ -0,0 +1,577 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <unistd.h> // sleep
|
||||
|
||||
#include "log.hpp"
|
||||
#include "tools.hpp"
|
||||
#include "v2g_ctx.hpp"
|
||||
|
||||
#include <cbv2g/iso_2/iso2_msgDefDatatypes.h>
|
||||
|
||||
void init_physical_value(struct iso2_PhysicalValueType* const physicalValue, iso2_unitSymbolType unit) {
|
||||
physicalValue->Multiplier = 0;
|
||||
physicalValue->Unit = unit;
|
||||
physicalValue->Value = 0;
|
||||
}
|
||||
|
||||
// Only for AC
|
||||
bool populate_physical_value(struct iso2_PhysicalValueType* pv, long long int value, iso2_unitSymbolType unit) {
|
||||
struct iso2_PhysicalValueType physic_tmp = {pv->Multiplier, pv->Unit, pv->Value}; // To restore
|
||||
pv->Unit = unit;
|
||||
pv->Multiplier = 0; // with integers, we don't need negative multipliers for precision, so start at 0
|
||||
|
||||
// if the value is too large to be represented in 16 signed bits, increase the multiplier
|
||||
while ((value > INT16_MAX) || (value < INT16_MIN)) {
|
||||
pv->Multiplier++;
|
||||
value /= 10;
|
||||
}
|
||||
|
||||
if ((pv->Multiplier < PHY_VALUE_MULT_MIN) || (pv->Multiplier > PHY_VALUE_MULT_MAX)) {
|
||||
memcpy(pv, &physic_tmp, sizeof(struct iso2_PhysicalValueType));
|
||||
dlog(DLOG_LEVEL_WARNING, "Physical value out of scope. Ignore value");
|
||||
return false;
|
||||
}
|
||||
|
||||
pv->Value = value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void populate_physical_value_float(struct iso2_PhysicalValueType* pv, float value, uint8_t decimal_places,
|
||||
iso2_unitSymbolType unit) {
|
||||
if (false == populate_physical_value(pv, (long long int)value, unit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pv->Multiplier == 0) {
|
||||
for (uint8_t idx = 0; idx < decimal_places; idx++) {
|
||||
if (((long int)(value * 10) < INT16_MAX) && ((long int)(value * 10) > INT16_MIN)) {
|
||||
pv->Multiplier--;
|
||||
value *= 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pv->Multiplier != -decimal_places) {
|
||||
dlog(DLOG_LEVEL_WARNING,
|
||||
"Possible precision loss while converting to physical value type, requested %i, actual %i (value %f)",
|
||||
decimal_places, -pv->Multiplier, value);
|
||||
}
|
||||
|
||||
pv->Value = value;
|
||||
}
|
||||
|
||||
static void* v2g_ctx_eventloop(void* data) {
|
||||
struct v2g_context* ctx = static_cast<struct v2g_context*>(data);
|
||||
|
||||
while (!ctx->shutdown) {
|
||||
int rv;
|
||||
|
||||
rv = event_base_loop(ctx->event_base, 0);
|
||||
if (rv == -1)
|
||||
break;
|
||||
|
||||
/* if no events are registered, restart looping */
|
||||
if (rv == 1)
|
||||
sleep(1); /* FIXME this is bad since we actually do busy-waiting here */
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int v2g_ctx_start_events(struct v2g_context* ctx) {
|
||||
int rv = pthread_create(&ctx->event_thread, NULL, v2g_ctx_eventloop, ctx);
|
||||
return rv ? -1 : 0;
|
||||
}
|
||||
|
||||
void v2g_ctx_init_charging_session(struct v2g_context* const ctx, bool is_connection_terminated) {
|
||||
v2g_ctx_init_charging_state(ctx, is_connection_terminated); // Init charging state
|
||||
v2g_ctx_init_charging_values(ctx); // Loads the internal default config
|
||||
}
|
||||
|
||||
void v2g_ctx_init_charging_state(struct v2g_context* const ctx, bool is_connection_terminated) {
|
||||
ctx->stop_hlc = false;
|
||||
ctx->intl_emergency_shutdown = false;
|
||||
ctx->is_connection_terminated = is_connection_terminated;
|
||||
ctx->last_v2g_msg = V2G_UNKNOWN_MSG;
|
||||
ctx->current_v2g_msg = V2G_UNKNOWN_MSG;
|
||||
ctx->state = 0; // WAIT_FOR_SESSIONSETUP
|
||||
ctx->selected_protocol = V2G_UNKNOWN_PROTOCOL;
|
||||
ctx->session.renegotiation_required = false;
|
||||
ctx->session.is_charging = false;
|
||||
ctx->evse_v2g_data.evse_notification = (uint8_t)0;
|
||||
}
|
||||
|
||||
void v2g_ctx_init_charging_values(struct v2g_context* const ctx) {
|
||||
static bool initialize_once = false;
|
||||
|
||||
if (ctx->hlc_pause_active != true) {
|
||||
ctx->evse_v2g_data.session_id =
|
||||
(uint64_t)0; /* store associated session id, this is zero until SessionSetupRes is sent */
|
||||
}
|
||||
ctx->evse_v2g_data.notification_max_delay = (uint32_t)0;
|
||||
ctx->evse_v2g_data.evse_isolation_status = (uint8_t)iso2_isolationLevelType_Invalid;
|
||||
ctx->evse_v2g_data.evse_isolation_status_is_used = (unsigned int)1; // Shall be used in DIN
|
||||
ctx->evse_v2g_data.evse_notification = (uint8_t)0;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_INIT] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_AUTH] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_PARAMETER] = iso2_DC_EVSEStatusCodeType_EVSE_Ready; // [V2G-DC-453]
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_ISOLATION] = iso2_DC_EVSEStatusCodeType_EVSE_IsolationMonitoringActive;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_PRECHARGE] = iso2_DC_EVSEStatusCodeType_EVSE_Ready;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_CHARGE] = iso2_DC_EVSEStatusCodeType_EVSE_Ready;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_WELDING] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
ctx->evse_v2g_data.evse_status_code[PHASE_STOP] = iso2_DC_EVSEStatusCodeType_EVSE_NotReady;
|
||||
memset(ctx->evse_v2g_data.evse_processing, iso2_EVSEProcessingType_Ongoing, PHASE_LENGTH);
|
||||
ctx->evse_v2g_data.evse_processing[PHASE_PARAMETER] = iso2_EVSEProcessingType_Finished; // Skip parameter phase
|
||||
|
||||
if (ctx->hlc_pause_active != true) {
|
||||
ctx->evse_v2g_data.charge_service.ServiceCategory = iso2_serviceCategoryType_EVCharging;
|
||||
ctx->evse_v2g_data.charge_service.ServiceID = (uint16_t)1;
|
||||
strncpy_to_v2g(ctx->evse_v2g_data.charge_service.ServiceName.characters,
|
||||
sizeof(ctx->evse_v2g_data.charge_service.ServiceName.characters),
|
||||
&ctx->evse_v2g_data.charge_service.ServiceName.charactersLen,
|
||||
// Conform to table 105 [V2G2-417]
|
||||
"AC_DC_Charging");
|
||||
ctx->evse_v2g_data.charge_service.ServiceName_isUsed = 0;
|
||||
// ctx->evse_v2g_data.chargeService.ServiceScope.characters
|
||||
// ctx->evse_v2g_data.chargeService.ServiceScope.charactersLen
|
||||
ctx->evse_v2g_data.charge_service.ServiceScope_isUsed = (unsigned int)0;
|
||||
}
|
||||
ctx->meter_info.meter_info_is_used = false;
|
||||
|
||||
ctx->evse_v2g_data.service_parameter_list.clear();
|
||||
if (initialize_once == false) {
|
||||
ctx->evse_v2g_data.charge_service.FreeService = 0;
|
||||
std::string evse_id = std::string("DE*CBY*ETE1*234");
|
||||
strcpy(reinterpret_cast<char*>(ctx->evse_v2g_data.evse_id.bytes), evse_id.data());
|
||||
ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size();
|
||||
ctx->evse_v2g_data.charge_service.SupportedEnergyTransferMode.EnergyTransferMode.array[0] =
|
||||
iso2_EnergyTransferModeType_AC_single_phase_core;
|
||||
ctx->evse_v2g_data.charge_service.SupportedEnergyTransferMode.EnergyTransferMode.arrayLen = 1;
|
||||
ctx->evse_v2g_data.date_time_now_is_used = (unsigned int)0;
|
||||
|
||||
// evse power values
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_current_regulation_tolerance, iso2_unitSymbolType_A);
|
||||
ctx->evse_v2g_data.evse_current_regulation_tolerance_is_used = (unsigned int)0; // optional in din
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_energy_to_be_delivered, iso2_unitSymbolType_Wh);
|
||||
ctx->evse_v2g_data.evse_energy_to_be_delivered_is_used = (unsigned int)0; // optional in din
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_maximum_current_limit, iso2_unitSymbolType_A);
|
||||
ctx->evse_v2g_data.evse_maximum_current_limit_is_used = (unsigned int)0;
|
||||
ctx->evse_v2g_data.evse_current_limit_achieved = (int)0;
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_maximum_power_limit, iso2_unitSymbolType_W);
|
||||
ctx->evse_v2g_data.evse_maximum_power_limit_is_used = (unsigned int)0;
|
||||
ctx->evse_v2g_data.evse_power_limit_achieved = (int)0;
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_maximum_voltage_limit, iso2_unitSymbolType_V);
|
||||
|
||||
ctx->evse_v2g_data.evse_maximum_voltage_limit_is_used = (unsigned int)0; // mandatory
|
||||
ctx->evse_v2g_data.evse_voltage_limit_achieved = (int)0;
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_minimum_current_limit, iso2_unitSymbolType_A);
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_minimum_voltage_limit, iso2_unitSymbolType_V);
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_peak_current_ripple, iso2_unitSymbolType_A);
|
||||
// AC evse power values
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_nominal_voltage, iso2_unitSymbolType_V);
|
||||
ctx->evse_v2g_data.rcd = (int)0; // 0 if RCD has not detected an error
|
||||
ctx->contactor_is_closed = false;
|
||||
|
||||
ctx->evse_v2g_data.payment_option_list.clear();
|
||||
ctx->evse_v2g_data.payment_option_list.reserve(iso2_paymentOptionType_2_ARRAY_SIZE);
|
||||
ctx->evse_v2g_data.payment_option_list.push_back(iso2_paymentOptionType_ExternalPayment);
|
||||
|
||||
ctx->evse_v2g_data.evse_service_list.clear();
|
||||
ctx->evse_v2g_data.evse_service_list.reserve(iso2_ServiceType_8_ARRAY_SIZE);
|
||||
}
|
||||
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_present_voltage, iso2_unitSymbolType_V);
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_present_current, iso2_unitSymbolType_A);
|
||||
|
||||
if (ctx->hlc_pause_active != true) {
|
||||
// SAScheduleTupleID#PMaxScheduleTupleID#Start#Duration#PMax#
|
||||
init_physical_value(&ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
|
||||
.PMaxSchedule.PMaxScheduleEntry.array[0]
|
||||
.PMax,
|
||||
iso2_unitSymbolType_W);
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
|
||||
.PMaxSchedule.PMaxScheduleEntry.array[0]
|
||||
.RelativeTimeInterval.duration = (uint32_t)0;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
|
||||
.PMaxSchedule.PMaxScheduleEntry.array[0]
|
||||
.RelativeTimeInterval.duration_isUsed = (unsigned int)1;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
|
||||
.PMaxSchedule.PMaxScheduleEntry.array[0]
|
||||
.RelativeTimeInterval.start = (uint32_t)0;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
|
||||
.PMaxSchedule.PMaxScheduleEntry.array[0]
|
||||
.RelativeTimeInterval_isUsed = (unsigned int)1; // Optional: In DIN/ISO it must be set to 1
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
|
||||
.PMaxSchedule.PMaxScheduleEntry.array[0]
|
||||
.TimeInterval_isUsed = (unsigned int)0;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0].PMaxSchedule.PMaxScheduleEntry.arrayLen = 1;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0].SalesTariff_isUsed = (unsigned int)0;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0].SAScheduleTupleID =
|
||||
(uint8_t)1; // [V2G2-773] 1 to 255
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.arrayLen = (uint16_t)1;
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list_is_used = false;
|
||||
|
||||
// ctx->evse_v2g_data.evseSAScheduleTuple.SalesTariff
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0].SalesTariff_isUsed =
|
||||
(unsigned int)0; // Not supported in DIN
|
||||
} else {
|
||||
ctx->evse_v2g_data.evse_sa_schedule_list_is_used = true;
|
||||
}
|
||||
if (ctx->evse_v2g_data.cert_install_res_b64_buffer.empty() == false) {
|
||||
ctx->evse_v2g_data.cert_install_res_b64_buffer.clear();
|
||||
}
|
||||
|
||||
// AC paramter
|
||||
ctx->evse_v2g_data.rcd = (int)0; // 0 if RCD has not detected an error
|
||||
ctx->contactor_is_closed = false;
|
||||
ctx->evse_v2g_data.receipt_required = (int)0;
|
||||
|
||||
// Specific SAE J2847 bidi values
|
||||
ctx->evse_v2g_data.sae_bidi_data.enabled_sae_v2g = false;
|
||||
ctx->evse_v2g_data.sae_bidi_data.enabled_sae_v2h = false;
|
||||
ctx->evse_v2g_data.sae_bidi_data.sae_v2h_minimal_soc = 20;
|
||||
ctx->evse_v2g_data.sae_bidi_data.discharging = false;
|
||||
|
||||
ctx->evse_v2g_data.no_energy_pause = NoEnergyPauseStatus::None;
|
||||
|
||||
// Init EV received v2g-data to an invalid state
|
||||
memset(&ctx->ev_v2g_data, 0xff, sizeof(ctx->ev_v2g_data));
|
||||
|
||||
/* Init session values */
|
||||
if (ctx->hlc_pause_active != true) {
|
||||
ctx->session.iso_selected_payment_option = iso2_paymentOptionType_ExternalPayment;
|
||||
} else {
|
||||
ctx->evse_v2g_data.payment_option_list.clear();
|
||||
ctx->evse_v2g_data.payment_option_list.push_back(ctx->session.iso_selected_payment_option);
|
||||
}
|
||||
memset(ctx->session.gen_challenge, 0, sizeof(ctx->session.gen_challenge));
|
||||
|
||||
ctx->session.authorization_rejected = false;
|
||||
|
||||
remove_service_from_service_list_if_exists(ctx, V2G_SERVICE_ID_CERTIFICATE);
|
||||
|
||||
initialize_once = true;
|
||||
}
|
||||
|
||||
struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase,
|
||||
iso15118_extensionsImplBase* p_extensions, evse_securityIntf* r_security,
|
||||
std::vector<ISO15118_vasIntf*> r_vas) {
|
||||
struct v2g_context* ctx;
|
||||
|
||||
// TODO There are c++ objects within v2g_context and calloc doesn't call initialisers.
|
||||
// free() will not call destructors
|
||||
ctx = static_cast<v2g_context*>(calloc(1, sizeof(*ctx)));
|
||||
if (!ctx)
|
||||
return NULL;
|
||||
|
||||
ctx->r_security = r_security;
|
||||
ctx->r_vas = r_vas;
|
||||
ctx->p_charger = p_chargerImplBase;
|
||||
ctx->p_extensions = p_extensions;
|
||||
|
||||
ctx->tls_security = TLS_SECURITY_PROHIBIT; // default
|
||||
|
||||
/* This evse parameter will be initialized once */
|
||||
ctx->basic_config.evse_ac_current_limit = 0.0f;
|
||||
ctx->basic_config.evse_ac_nominal_current = 0.0f;
|
||||
ctx->basic_config.evse_ac_nominal_voltage = 230.0f;
|
||||
|
||||
ctx->local_tcp_addr = NULL;
|
||||
ctx->local_tls_addr = NULL;
|
||||
|
||||
ctx->is_dc_charger = true;
|
||||
|
||||
v2g_ctx_init_charging_session(ctx, true);
|
||||
|
||||
/* interface from config file or options */
|
||||
ctx->if_name = "eth1";
|
||||
|
||||
ctx->network_read_timeout = 1000;
|
||||
ctx->network_read_timeout_tls = 5000;
|
||||
|
||||
ctx->sdp_socket = -1;
|
||||
ctx->tcp_socket = -1;
|
||||
ctx->tls_socket.fd = -1;
|
||||
ctx->tls_key_logging = false;
|
||||
ctx->debugMode = false;
|
||||
|
||||
/* according to man page, both functions never return an error */
|
||||
evthread_use_pthreads();
|
||||
pthread_mutex_init(&ctx->mqtt_lock, NULL);
|
||||
pthread_condattr_init(&ctx->mqtt_attr);
|
||||
pthread_condattr_setclock(&ctx->mqtt_attr, CLOCK_MONOTONIC);
|
||||
pthread_cond_init(&ctx->mqtt_cond, &ctx->mqtt_attr);
|
||||
|
||||
ctx->event_base = event_base_new();
|
||||
if (!ctx->event_base) {
|
||||
dlog(DLOG_LEVEL_ERROR, "event_base_new failed");
|
||||
goto free_out;
|
||||
}
|
||||
|
||||
ctx->event_thread = 0;
|
||||
if (v2g_ctx_start_events(ctx) != 0)
|
||||
goto free_out;
|
||||
|
||||
ctx->hlc_pause_active = false;
|
||||
|
||||
ctx->connection_initiated = false;
|
||||
|
||||
return ctx;
|
||||
|
||||
free_out:
|
||||
ctx->shutdown = true;
|
||||
if (ctx->event_base) {
|
||||
event_base_loopbreak(ctx->event_base);
|
||||
}
|
||||
if (ctx->event_thread) {
|
||||
pthread_join(ctx->event_thread, NULL);
|
||||
}
|
||||
if (ctx->event_base) {
|
||||
event_base_free(ctx->event_base);
|
||||
}
|
||||
free(ctx->local_tls_addr);
|
||||
free(ctx->local_tcp_addr);
|
||||
free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void v2g_ctx_free(struct v2g_context* ctx) {
|
||||
if (ctx == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx->shutdown = true;
|
||||
|
||||
if (ctx->event_base) {
|
||||
event_base_loopbreak(ctx->event_base);
|
||||
}
|
||||
|
||||
if (ctx->event_thread) {
|
||||
pthread_join(ctx->event_thread, NULL);
|
||||
}
|
||||
|
||||
if (ctx->event_base) {
|
||||
event_base_free(ctx->event_base);
|
||||
}
|
||||
|
||||
pthread_cond_destroy(&ctx->mqtt_cond);
|
||||
pthread_mutex_destroy(&ctx->mqtt_lock);
|
||||
|
||||
free(ctx->local_tls_addr);
|
||||
ctx->local_tls_addr = NULL;
|
||||
free(ctx->local_tcp_addr);
|
||||
ctx->local_tcp_addr = NULL;
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
void publish_dc_ev_maximum_limits(struct v2g_context* ctx, const float& v2g_dc_ev_max_current_limit,
|
||||
const unsigned int& v2g_dc_ev_max_current_limit_is_used,
|
||||
const float& v2g_dc_ev_max_power_limit,
|
||||
const unsigned int& v2g_dc_ev_max_power_limit_is_used,
|
||||
const float& v2g_dc_ev_max_voltage_limit,
|
||||
const unsigned int& v2g_dc_ev_max_voltage_limit_is_used) {
|
||||
types::iso15118::DcEvMaximumLimits dc_ev_maximum_limits;
|
||||
bool publish_message = false;
|
||||
|
||||
if (v2g_dc_ev_max_current_limit_is_used == (unsigned int)1) {
|
||||
dc_ev_maximum_limits.dc_ev_maximum_current_limit = v2g_dc_ev_max_current_limit;
|
||||
if (ctx->ev_v2g_data.ev_maximum_current_limit != dc_ev_maximum_limits.dc_ev_maximum_current_limit.value()) {
|
||||
ctx->ev_v2g_data.ev_maximum_current_limit = v2g_dc_ev_max_current_limit;
|
||||
publish_message = true;
|
||||
}
|
||||
}
|
||||
if (v2g_dc_ev_max_power_limit_is_used == (unsigned int)1) {
|
||||
dc_ev_maximum_limits.dc_ev_maximum_power_limit = v2g_dc_ev_max_power_limit;
|
||||
if (ctx->ev_v2g_data.ev_maximum_power_limit != v2g_dc_ev_max_power_limit) {
|
||||
ctx->ev_v2g_data.ev_maximum_power_limit = v2g_dc_ev_max_power_limit;
|
||||
publish_message = true;
|
||||
}
|
||||
}
|
||||
if (v2g_dc_ev_max_voltage_limit_is_used == (unsigned int)1) {
|
||||
dc_ev_maximum_limits.dc_ev_maximum_voltage_limit = v2g_dc_ev_max_voltage_limit;
|
||||
if (ctx->ev_v2g_data.ev_maximum_voltage_limit != dc_ev_maximum_limits.dc_ev_maximum_voltage_limit.value()) {
|
||||
ctx->ev_v2g_data.ev_maximum_voltage_limit = v2g_dc_ev_max_voltage_limit;
|
||||
publish_message = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (publish_message == true) {
|
||||
ctx->p_charger->publish_dc_ev_maximum_limits(dc_ev_maximum_limits);
|
||||
}
|
||||
}
|
||||
|
||||
void publish_dc_ev_target_voltage_current(struct v2g_context* ctx, const float& v2g_dc_ev_target_voltage,
|
||||
const float& v2g_dc_ev_target_current) {
|
||||
if ((ctx->ev_v2g_data.v2g_target_voltage != v2g_dc_ev_target_voltage) ||
|
||||
(ctx->ev_v2g_data.v2g_target_current != v2g_dc_ev_target_current)) {
|
||||
types::iso15118::DcEvTargetValues dc_ev_target_values;
|
||||
dc_ev_target_values.dc_ev_target_voltage = v2g_dc_ev_target_voltage;
|
||||
dc_ev_target_values.dc_ev_target_current = v2g_dc_ev_target_current;
|
||||
|
||||
ctx->ev_v2g_data.v2g_target_voltage = v2g_dc_ev_target_voltage;
|
||||
ctx->ev_v2g_data.v2g_target_current = v2g_dc_ev_target_current;
|
||||
|
||||
ctx->p_charger->publish_dc_ev_target_voltage_current(dc_ev_target_values);
|
||||
}
|
||||
}
|
||||
|
||||
void publish_dc_ev_remaining_time(struct v2g_context* ctx, const float& v2g_dc_ev_remaining_time_to_full_soc,
|
||||
const unsigned int& v2g_dc_ev_remaining_time_to_full_soc_is_used,
|
||||
const float& v2g_dc_ev_remaining_time_to_bulk_soc,
|
||||
const unsigned int& v2g_dc_ev_remaining_time_to_bulk_soc_is_used) {
|
||||
types::iso15118::DcEvRemainingTime dc_ev_remaining_time;
|
||||
const char* format = "%Y-%m-%dT%H:%M:%SZ";
|
||||
char buffer[100];
|
||||
std::time_t time_now_in_sec = time(NULL);
|
||||
bool publish_message = false;
|
||||
|
||||
if (v2g_dc_ev_remaining_time_to_full_soc_is_used == (unsigned int)1) {
|
||||
if (ctx->ev_v2g_data.remaining_time_to_full_soc != v2g_dc_ev_remaining_time_to_full_soc) {
|
||||
std::time_t time_to_full_soc = time_now_in_sec + v2g_dc_ev_remaining_time_to_full_soc;
|
||||
std::strftime(buffer, sizeof(buffer), format, std::gmtime(&time_to_full_soc));
|
||||
dc_ev_remaining_time.ev_remaining_time_to_full_soc = std::string(buffer);
|
||||
ctx->ev_v2g_data.remaining_time_to_full_soc = v2g_dc_ev_remaining_time_to_full_soc;
|
||||
publish_message = true;
|
||||
}
|
||||
}
|
||||
if (v2g_dc_ev_remaining_time_to_bulk_soc_is_used == (unsigned int)1) {
|
||||
if (ctx->ev_v2g_data.remaining_time_to_bulk_soc != v2g_dc_ev_remaining_time_to_bulk_soc) {
|
||||
std::time_t time_to_bulk_soc = time_now_in_sec + v2g_dc_ev_remaining_time_to_bulk_soc;
|
||||
std::strftime(buffer, sizeof(buffer), format, std::gmtime(&time_to_bulk_soc));
|
||||
dc_ev_remaining_time.ev_remaining_time_to_full_bulk_soc = std::string(buffer);
|
||||
ctx->ev_v2g_data.remaining_time_to_bulk_soc = v2g_dc_ev_remaining_time_to_bulk_soc;
|
||||
publish_message = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (publish_message == true) {
|
||||
ctx->p_charger->publish_dc_ev_remaining_time(dc_ev_remaining_time);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief log_selected_energy_transfer_type This function prints the selected energy transfer mode.
|
||||
* \param selected_energy_transfer_mode is the selected energy transfer mode
|
||||
*/
|
||||
void log_selected_energy_transfer_type(int selected_energy_transfer_mode) {
|
||||
if (selected_energy_transfer_mode >= iso2_EnergyTransferModeType_AC_single_phase_core &&
|
||||
selected_energy_transfer_mode <= iso2_EnergyTransferModeType_DC_unique) {
|
||||
dlog(DLOG_LEVEL_INFO, "Selected energy transfer mode: %s",
|
||||
selected_energy_transfer_mode_string[selected_energy_transfer_mode]);
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_WARNING, "Selected energy transfer mode %d is invalid", selected_energy_transfer_mode);
|
||||
}
|
||||
}
|
||||
|
||||
bool add_service_to_service_list(struct v2g_context* v2g_ctx, const struct iso2_ServiceType& evse_service,
|
||||
const int16_t* parameter_set_id, uint8_t parameter_set_id_len) {
|
||||
|
||||
uint8_t write_idx = 0;
|
||||
bool service_found = false;
|
||||
|
||||
for (const auto& service : v2g_ctx->evse_v2g_data.evse_service_list) {
|
||||
if (service.ServiceID == evse_service.ServiceID) {
|
||||
service_found = true;
|
||||
break;
|
||||
}
|
||||
write_idx++;
|
||||
}
|
||||
|
||||
if (service_found == false and (v2g_ctx->evse_v2g_data.evse_service_list.size() < iso2_ServiceType_8_ARRAY_SIZE)) {
|
||||
v2g_ctx->evse_v2g_data.evse_service_list.push_back(evse_service);
|
||||
} else if (v2g_ctx->evse_v2g_data.evse_service_list.size() == iso2_ServiceType_8_ARRAY_SIZE) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Maximum service list size reached. Unable to add service ID %u",
|
||||
evse_service.ServiceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure parameter-set-id if requiered
|
||||
for (uint8_t idx = 0; idx < parameter_set_id_len; idx++) {
|
||||
iso2_ServiceParameterListType parameter_list{};
|
||||
init_iso2_ServiceParameterListType(¶meter_list);
|
||||
v2g_ctx->evse_v2g_data.service_parameter_list.try_emplace(evse_service.ServiceID, parameter_list);
|
||||
|
||||
configure_parameter_set(v2g_ctx->evse_v2g_data.service_parameter_list.at(evse_service.ServiceID),
|
||||
parameter_set_id[idx], evse_service.ServiceID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove_service_from_service_list_if_exists(struct v2g_context* v2g_ctx, uint16_t service_id) {
|
||||
auto& service_list = v2g_ctx->evse_v2g_data.evse_service_list;
|
||||
service_list.erase(std::remove_if(service_list.begin(), service_list.end(),
|
||||
[service_id](const auto service) { return service.ServiceID == service_id; }),
|
||||
service_list.end());
|
||||
}
|
||||
|
||||
void configure_parameter_set(iso2_ServiceParameterListType& parameterSetList, int16_t parameterSetId,
|
||||
uint16_t serviceId) {
|
||||
|
||||
bool parameter_set_id_found = false;
|
||||
uint8_t write_idx = 0;
|
||||
for (uint8_t idx = 0; idx < parameterSetList.ParameterSet.arrayLen; idx++) {
|
||||
if (parameterSetList.ParameterSet.array[idx].ParameterSetID == parameterSetId) {
|
||||
parameter_set_id_found = true;
|
||||
write_idx = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((parameter_set_id_found == false) &&
|
||||
(parameterSetList.ParameterSet.arrayLen < iso2_ParameterSetType_5_ARRAY_SIZE)) {
|
||||
write_idx = parameterSetList.ParameterSet.arrayLen;
|
||||
parameterSetList.ParameterSet.arrayLen++;
|
||||
} else if (parameterSetList.ParameterSet.arrayLen == iso2_ParameterSetType_5_ARRAY_SIZE) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Maximum parameter-set list size reached. Unable to add parameter-set-ID %d",
|
||||
parameterSetId);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Get an free parameter-set-entry */
|
||||
auto& parameterSet = parameterSetList.ParameterSet.array[write_idx];
|
||||
parameterSet.ParameterSetID = parameterSetId;
|
||||
if (serviceId == 2) {
|
||||
/* Configure parameter-set-ID of the certificate service */
|
||||
/* Service to install a Contract Certificate (Ref. Table 106 —
|
||||
* ServiceParameterList for certificate service) */
|
||||
if (parameterSet.ParameterSetID == 1) {
|
||||
/* Configure parameter name */
|
||||
strncpy_to_v2g(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].Name.characters,
|
||||
sizeof(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].Name.characters),
|
||||
¶meterSet.Parameter.array[parameterSet.Parameter.arrayLen].Name.charactersLen,
|
||||
"Service");
|
||||
/* Configure parameter value */
|
||||
strncpy_to_v2g(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue.characters,
|
||||
sizeof(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue.characters),
|
||||
¶meterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue.charactersLen,
|
||||
"Installation");
|
||||
parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue_isUsed = 1;
|
||||
parameterSet.Parameter.arrayLen = 1;
|
||||
}
|
||||
/* Service to update a Contract Certificate */
|
||||
else if (parameterSet.ParameterSetID == 2) {
|
||||
/* Configure parameter name */
|
||||
strncpy_to_v2g(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].Name.characters,
|
||||
sizeof(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].Name.characters),
|
||||
¶meterSet.Parameter.array[parameterSet.Parameter.arrayLen].Name.charactersLen,
|
||||
"Service");
|
||||
/* Configure parameter value */
|
||||
strncpy_to_v2g(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue.characters,
|
||||
sizeof(parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue.characters),
|
||||
¶meterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue.charactersLen,
|
||||
"Update");
|
||||
parameterSet.Parameter.array[parameterSet.Parameter.arrayLen].stringValue_isUsed = 1;
|
||||
parameterSet.Parameter.arrayLen = 1;
|
||||
}
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_WARNING, "Parameter-set-ID of service ID %u is not supported", serviceId);
|
||||
}
|
||||
}
|
||||
151
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_ctx.hpp
Normal file
151
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_ctx.hpp
Normal file
@@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2022-2023 chargebyte GmbH
|
||||
// Copyright (C) 2022-2023 Contributors to EVerest
|
||||
#ifndef V2G_CTX_H
|
||||
#define V2G_CTX_H
|
||||
|
||||
#include "v2g.hpp"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PHY_VALUE_MULT_MIN -3
|
||||
#define PHY_VALUE_MULT_MAX 3
|
||||
#define PHY_VALUE_VALUE_MIN SHRT_MIN
|
||||
#define PHY_VALUE_VALUE_MAX SHRT_MAX
|
||||
|
||||
static const char* selected_energy_transfer_mode_string[] = {
|
||||
"AC_single_phase_core", "AC_three_phase_core", "DC_core", "DC_extended", "DC_combo_core", "DC_unique",
|
||||
};
|
||||
|
||||
struct v2g_context* v2g_ctx_create(ISO15118_chargerImplBase* p_chargerImplBase,
|
||||
iso15118_extensionsImplBase* p_extensions, evse_securityIntf* r_security,
|
||||
std::vector<ISO15118_vasIntf*> r_vas);
|
||||
|
||||
/*!
|
||||
* \brief v2g_ctx_init_charging_session This funcion inits a charging session.
|
||||
* \param ctx is a pointer of type \c v2g_context. It holds the charging values.
|
||||
* \param is_connection_terminated must be set to \c true if the connection is terminated.
|
||||
*/
|
||||
void v2g_ctx_init_charging_session(struct v2g_context* const ctx, bool is_connection_terminated);
|
||||
|
||||
/*!
|
||||
* \brief init_physical_value This funcion inits a physicalValue struct.
|
||||
* \param physicalValue is the struct of the physical value.
|
||||
* \param unit is the unit of the physical value.
|
||||
*/
|
||||
void init_physical_value(struct iso2_PhysicalValueType* const physicalValue, iso2_unitSymbolType unit);
|
||||
|
||||
/*!
|
||||
* \brief populate_physical_value This function fills all elements of a \c iso2_PhysicalValueType struct regarding the
|
||||
* parameter value and unit.
|
||||
* \param pv is pointer to the physical value struct
|
||||
* \param value is the physical value
|
||||
* \param unit is the unit of the physical value
|
||||
* \return Returns \c true if the convertion was succesfull, otherwise \c false.
|
||||
*/
|
||||
bool populate_physical_value(struct iso2_PhysicalValueType* pv, long long int value, iso2_unitSymbolType unit);
|
||||
|
||||
/*!
|
||||
* \brief populate_physical_value_float This function fills all elements of a \c iso2_PhysicalValueType struct from a
|
||||
* json object.
|
||||
* \param pv is pointer to the physical value struct
|
||||
* \param value is the physical value
|
||||
* \param decimal_places is to determine the precision
|
||||
* \param unit is the unit of the physical value
|
||||
*/
|
||||
void populate_physical_value_float(struct iso2_PhysicalValueType* pv, float value, uint8_t decimal_places,
|
||||
iso2_unitSymbolType unit);
|
||||
|
||||
/*!
|
||||
* \brief v2g_ctx_init_charging_state This function inits the charging state. This should be called afer a terminated
|
||||
* charging session.
|
||||
* \param ctx is a pointer of type \c v2g_context. It holds the charging values.
|
||||
* \param is_connection_terminated is set to \c true if the connection is terminated
|
||||
*/
|
||||
void v2g_ctx_init_charging_state(struct v2g_context* const ctx, bool is_connection_terminated);
|
||||
|
||||
/*!
|
||||
* \brief init_charging_values This function inits all charge-values (din/iso). This should be called after starting the
|
||||
* charging session.
|
||||
* \param ctx is a pointer of type \c v2g_context. It holds the charging values.
|
||||
*/
|
||||
void v2g_ctx_init_charging_values(struct v2g_context* const ctx);
|
||||
|
||||
/*!
|
||||
* \brief v2g_ctx_free
|
||||
* \param ctx
|
||||
*/
|
||||
void v2g_ctx_free(struct v2g_context* ctx);
|
||||
|
||||
/*!
|
||||
* \brief publish_dc_ev_maximum_limits This function publishes the dc_ev_maximum_limits
|
||||
* \param ctx is a pointer of type \c v2g_context
|
||||
* \param v2g_dc_ev_max_current_limit is the EV max current limit
|
||||
* \param v2g_dc_ev_max_current_limit_is_used is set to \c true if used, otherwise \c false
|
||||
* \param v2g_dc_ev_max_power_limit is the EV max power limit
|
||||
* \param v2g_dc_ev_max_power_limit_is_used is set to \c true if used, otherwise \c false
|
||||
* \param v2g_dc_ev_max_voltage_limit is the EV max voltage limit
|
||||
* \param v2g_dc_ev_max_voltage_limit_is_used is set to \c true if used, otherwise \c false
|
||||
*/
|
||||
void publish_dc_ev_maximum_limits(struct v2g_context* ctx, const float& v2g_dc_ev_max_current_limit,
|
||||
const unsigned int& v2g_dc_ev_max_current_limit_is_used,
|
||||
const float& v2g_dc_ev_max_power_limit,
|
||||
const unsigned int& v2g_dc_ev_max_power_limit_is_used,
|
||||
const float& v2g_dc_ev_max_voltage_limit,
|
||||
const unsigned int& v2g_dc_ev_max_voltage_limit_is_used);
|
||||
|
||||
/*!
|
||||
* \brief publish_dc_ev_target_voltage_current This function publishes the DcEvTargetValues
|
||||
* \param ctx is a pointer of type \c v2g_context
|
||||
* \param v2g_dc_ev_target_voltage is the EV target voltage
|
||||
* \param v2g_dc_ev_target_current is the EV target current
|
||||
*/
|
||||
void publish_dc_ev_target_voltage_current(struct v2g_context* ctx, const float& v2g_dc_ev_target_voltage,
|
||||
const float& v2g_dc_ev_target_current);
|
||||
|
||||
/*!
|
||||
* \brief publish_dc_ev_remaining_time This function publishes the dc_ev_remaining_time
|
||||
* \param ctx is a pointer of type \c v2g_context
|
||||
* \param iso2_dc_ev_remaining_time_to_full_soc is the EV remaining time to full soc
|
||||
* \param iso2_dc_ev_remaining_time_to_full_soc_is_used is set to \c true if used, otherwise \c false
|
||||
* \param iso2_dc_ev_remaining_time_to_bulk_soc is the EV remaining time to bulk soc
|
||||
* \param iso2_dc_ev_remaining_time_to_bulk_soc_is_used is set to \c true if used, otherwise \c false
|
||||
*/
|
||||
void publish_dc_ev_remaining_time(struct v2g_context* ctx, const float& iso2_dc_ev_remaining_time_to_full_soc,
|
||||
const unsigned int& iso2_dc_ev_remaining_time_to_full_soc_is_used,
|
||||
const float& iso2_dc_ev_remaining_time_to_bulk_soc,
|
||||
const unsigned int& iso2_dc_ev_remaining_time_to_bulk_soc_is_used);
|
||||
|
||||
/*!
|
||||
* \brief log_selected_energy_transfer_type This function logs the selected_energy_transfer_mode
|
||||
*/
|
||||
void log_selected_energy_transfer_type(int selected_energy_transfer_mode);
|
||||
|
||||
/*!
|
||||
* \brief add_service_to_service_list This function adds a service list item to the service list.
|
||||
* \param v2g_ctx is a pointer of type \c v2g_context
|
||||
* \param evse_service is service which shall be provided by the EVSE in the service list.
|
||||
* \param parameter_set_id is an array of optional service parameter-set-IDs
|
||||
* \param parameter_set_id_len is the array length of parameter_set_id
|
||||
* \return Returns \c true if it was successful, otherwise \c false.
|
||||
*/
|
||||
bool add_service_to_service_list(struct v2g_context* v2g_ctx, const struct iso2_ServiceType& evse_service,
|
||||
const int16_t* parameter_set_id = NULL, uint8_t parameter_set_id_len = 0);
|
||||
|
||||
/*!
|
||||
* \brief remove_service_from_service_list_if_exists This function removes a service list item from the service list.
|
||||
* \param v2g_ctx is a pointer of type \c v2g_context
|
||||
* \param service_id is the service which shall be removed by the EVSE in the service list.
|
||||
*/
|
||||
void remove_service_from_service_list_if_exists(struct v2g_context* v2g_ctx, uint16_t service_id);
|
||||
|
||||
/*!
|
||||
* \brief configure_parameter_set This function configures the parameter-set structure of a specific service ID.
|
||||
* \param v2g_ctx is a pointer of type \c v2g_context
|
||||
* \param parameterSetId is the parameter-set-ID which belongs to the service ID.
|
||||
* \param serviceId is the service ID. Currently only service ID 2 ("Certificate") supported.
|
||||
*/
|
||||
void configure_parameter_set(iso2_ServiceParameterListType& parameterSetList, int16_t parameterSetId,
|
||||
uint16_t serviceId);
|
||||
|
||||
#endif /* V2G_CTX_H */
|
||||
649
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_server.cpp
Normal file
649
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_server.cpp
Normal file
@@ -0,0 +1,649 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 chargebyte GmbH
|
||||
// Copyright (C) 2023 Contributors to EVerest
|
||||
#include "v2g_server.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cbv2g/app_handshake/appHand_Decoder.h>
|
||||
#include <cbv2g/app_handshake/appHand_Encoder.h>
|
||||
#include <cbv2g/common/exi_basetypes.h>
|
||||
#include <cbv2g/din/din_msgDefDecoder.h>
|
||||
#include <cbv2g/din/din_msgDefEncoder.h>
|
||||
#include <cbv2g/exi_v2gtp.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefDecoder.h>
|
||||
#include <cbv2g/iso_2/iso2_msgDefEncoder.h>
|
||||
|
||||
#include "connection.hpp"
|
||||
#include "din_server.hpp"
|
||||
#include "iso_server.hpp"
|
||||
#include "log.hpp"
|
||||
#include "tools.hpp"
|
||||
|
||||
#define MAX_RES_TIME 98
|
||||
|
||||
static types::iso15118::V2gMessageId get_v2g_message_id(enum V2gMsgTypeId v2g_msg, enum v2g_protocol selected_protocol,
|
||||
bool is_req) {
|
||||
switch (v2g_msg) {
|
||||
case V2G_SUPPORTED_APP_PROTOCOL_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::SupportedAppProtocolReq
|
||||
: types::iso15118::V2gMessageId::SupportedAppProtocolRes;
|
||||
case V2G_SESSION_SETUP_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::SessionSetupReq
|
||||
: types::iso15118::V2gMessageId::SessionSetupRes;
|
||||
case V2G_SERVICE_DISCOVERY_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::ServiceDiscoveryReq
|
||||
: types::iso15118::V2gMessageId::ServiceDiscoveryRes;
|
||||
case V2G_SERVICE_DETAIL_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::ServiceDetailReq
|
||||
: types::iso15118::V2gMessageId::ServiceDetailRes;
|
||||
case V2G_PAYMENT_SERVICE_SELECTION_MSG:
|
||||
return is_req == true ? selected_protocol == V2G_PROTO_DIN70121
|
||||
? types::iso15118::V2gMessageId::ServicePaymentSelectionReq
|
||||
: types::iso15118::V2gMessageId::PaymentServiceSelectionReq
|
||||
: selected_protocol == V2G_PROTO_DIN70121 ? types::iso15118::V2gMessageId::ServicePaymentSelectionRes
|
||||
: types::iso15118::V2gMessageId::PaymentServiceSelectionRes;
|
||||
case V2G_PAYMENT_DETAILS_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::PaymentDetailsReq
|
||||
: types::iso15118::V2gMessageId::PaymentDetailsRes;
|
||||
case V2G_AUTHORIZATION_MSG:
|
||||
return is_req == true ? selected_protocol == V2G_PROTO_DIN70121
|
||||
? types::iso15118::V2gMessageId::ContractAuthenticationReq
|
||||
: types::iso15118::V2gMessageId::AuthorizationReq
|
||||
: selected_protocol == V2G_PROTO_DIN70121 ? types::iso15118::V2gMessageId::ContractAuthenticationRes
|
||||
: types::iso15118::V2gMessageId::AuthorizationRes;
|
||||
case V2G_CHARGE_PARAMETER_DISCOVERY_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::ChargeParameterDiscoveryReq
|
||||
: types::iso15118::V2gMessageId::ChargeParameterDiscoveryRes;
|
||||
case V2G_METERING_RECEIPT_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::MeteringReceiptReq
|
||||
: types::iso15118::V2gMessageId::MeteringReceiptRes;
|
||||
case V2G_CERTIFICATE_UPDATE_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::CertificateUpdateReq
|
||||
: types::iso15118::V2gMessageId::CertificateUpdateRes;
|
||||
case V2G_CERTIFICATE_INSTALLATION_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::CertificateInstallationReq
|
||||
: types::iso15118::V2gMessageId::CertificateInstallationRes;
|
||||
case V2G_CHARGING_STATUS_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::ChargingStatusReq
|
||||
: types::iso15118::V2gMessageId::ChargingStatusRes;
|
||||
case V2G_CABLE_CHECK_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::CableCheckReq
|
||||
: types::iso15118::V2gMessageId::CableCheckRes;
|
||||
case V2G_PRE_CHARGE_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::PreChargeReq
|
||||
: types::iso15118::V2gMessageId::PreChargeRes;
|
||||
case V2G_POWER_DELIVERY_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::PowerDeliveryReq
|
||||
: types::iso15118::V2gMessageId::PowerDeliveryRes;
|
||||
case V2G_CURRENT_DEMAND_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::CurrentDemandReq
|
||||
: types::iso15118::V2gMessageId::CurrentDemandRes;
|
||||
case V2G_WELDING_DETECTION_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::WeldingDetectionReq
|
||||
: types::iso15118::V2gMessageId::WeldingDetectionRes;
|
||||
case V2G_SESSION_STOP_MSG:
|
||||
return is_req == true ? types::iso15118::V2gMessageId::SessionStopReq
|
||||
: types::iso15118::V2gMessageId::SessionStopRes;
|
||||
case V2G_UNKNOWN_MSG:
|
||||
default:
|
||||
return types::iso15118::V2gMessageId::UnknownMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief publish_var_V2G_Message This function fills a V2gMessages type with the V2G EXI message as HEX and Base64
|
||||
* \param conn hold the context of the V2G-connection.
|
||||
* \param is_req if it is a V2G request or response: 'true' if a request, and 'false' if a response
|
||||
*/
|
||||
static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) {
|
||||
types::iso15118::V2gMessages v2g_message;
|
||||
|
||||
u_int8_t* tempbuff = conn->buffer;
|
||||
std::string msg_as_hex_string;
|
||||
for (int i = 0; ((tempbuff != NULL) && (i < conn->payload_len + V2GTP_HEADER_LENGTH)); i++) {
|
||||
char hex[4];
|
||||
snprintf(hex, 4, "%x", *tempbuff); // to hex
|
||||
if (std::string(hex).size() == 1)
|
||||
msg_as_hex_string += '0';
|
||||
msg_as_hex_string += hex;
|
||||
tempbuff++;
|
||||
}
|
||||
|
||||
std::string EXI_Base64;
|
||||
|
||||
EXI_Base64 = openssl::base64_encode(conn->buffer, conn->payload_len + V2GTP_HEADER_LENGTH);
|
||||
if (EXI_Base64.size() == 0) {
|
||||
dlog(DLOG_LEVEL_WARNING, "Unable to base64 encode EXI buffer");
|
||||
}
|
||||
|
||||
v2g_message.exi_base64 = EXI_Base64;
|
||||
v2g_message.id = get_v2g_message_id(conn->ctx->current_v2g_msg, conn->ctx->selected_protocol, is_req);
|
||||
v2g_message.exi = msg_as_hex_string;
|
||||
conn->ctx->p_charger->publish_v2g_messages(v2g_message);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief v2g_incoming_v2gtp This function reads the V2G transport header
|
||||
* \param conn hold the context of the V2G-connection.
|
||||
* \return Returns 0 if the V2G-session was successfully stopped, 1 if connection was closed unexpectedly, otherwise -1.
|
||||
*/
|
||||
static int v2g_incoming_v2gtp(struct v2g_connection* conn) {
|
||||
assert(conn != nullptr);
|
||||
assert(conn->read != nullptr);
|
||||
|
||||
int rv;
|
||||
|
||||
/* read and process header */
|
||||
rv = conn->read(conn, conn->buffer, V2GTP_HEADER_LENGTH);
|
||||
if (rv < 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "connection_read(header) failed: %s",
|
||||
(rv == -1) ? strerror(errno) : "connection terminated");
|
||||
return -1;
|
||||
}
|
||||
/* connection was closed unexpectedly (timeout or closed by peer) */
|
||||
if (rv == 0)
|
||||
return 1;
|
||||
|
||||
if (rv != V2GTP_HEADER_LENGTH) {
|
||||
dlog(DLOG_LEVEL_ERROR, "connection_read(header) too short: expected %d, got %d", V2GTP_HEADER_LENGTH, rv);
|
||||
return -1;
|
||||
}
|
||||
|
||||
rv = V2GTP_ReadHeader(conn->buffer, &conn->payload_len);
|
||||
if (rv == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Invalid v2gtp header");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (conn->payload_len >= UINT32_MAX - V2GTP_HEADER_LENGTH) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Prevent integer overflow - payload too long: have %d, would need %u",
|
||||
DEFAULT_BUFFER_SIZE, conn->payload_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (conn->payload_len + V2GTP_HEADER_LENGTH > DEFAULT_BUFFER_SIZE) {
|
||||
dlog(DLOG_LEVEL_ERROR, "payload too long: have %d, would need %u", DEFAULT_BUFFER_SIZE,
|
||||
conn->payload_len + V2GTP_HEADER_LENGTH);
|
||||
|
||||
/* we have no way to flush/discard remaining unread data from the socket without reading it in chunks,
|
||||
* but this opens the chance to bind us in a "endless" read loop; so to protect us, simply close the connection
|
||||
*/
|
||||
|
||||
return -1;
|
||||
}
|
||||
/* read request */
|
||||
rv = conn->read(conn, &conn->buffer[V2GTP_HEADER_LENGTH], conn->payload_len);
|
||||
if (rv < 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "connection_read(payload) failed: %s",
|
||||
(rv == -1) ? strerror(errno) : "connection terminated");
|
||||
return -1;
|
||||
}
|
||||
if (rv != conn->payload_len) {
|
||||
dlog(DLOG_LEVEL_ERROR, "connection_read(payload) too short: expected %d, got %d", conn->payload_len, rv);
|
||||
return -1;
|
||||
}
|
||||
/* adjust buffer pos to decode request */
|
||||
conn->stream.byte_pos = V2GTP_HEADER_LENGTH;
|
||||
conn->stream.data_size = conn->payload_len + V2GTP_HEADER_LENGTH;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief v2g_outgoing_v2gtp This function creates the v2g transport header
|
||||
* \param conn hold the context of the v2g-connection.
|
||||
* \return Returns 0 if the v2g-session was successfully stopped, otherwise -1.
|
||||
*/
|
||||
int v2g_outgoing_v2gtp(struct v2g_connection* conn) {
|
||||
assert(conn != nullptr);
|
||||
assert(conn->write != nullptr);
|
||||
|
||||
/* fixup/create header */
|
||||
const auto len = exi_bitstream_get_length(&conn->stream);
|
||||
|
||||
V2GTP_WriteHeader(conn->buffer, len - V2GTP_HEADER_LENGTH);
|
||||
|
||||
if (conn->write(conn, conn->buffer, len) == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "connection_write(header) failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief v2g_handle_apphandshake After receiving a supportedAppProtocolReq message,
|
||||
* the SECC shall process the received information. DIN [V2G-DC-436] ISO [V2G2-540]
|
||||
* \param conn hold the context of the v2g-connection.
|
||||
* \return Returns a v2g-event of type enum v2g_event.
|
||||
*/
|
||||
static enum v2g_event v2g_handle_apphandshake(struct v2g_connection* conn) {
|
||||
enum v2g_event next_event = V2G_EVENT_NO_EVENT;
|
||||
int i;
|
||||
uint8_t ev_app_priority = 20; // lowest priority
|
||||
|
||||
/* validate handshake request and create response */
|
||||
init_appHand_exiDocument(&conn->handshake_resp);
|
||||
conn->handshake_resp.supportedAppProtocolRes_isUsed = 1;
|
||||
conn->handshake_resp.supportedAppProtocolRes.ResponseCode =
|
||||
appHand_responseCodeType_Failed_NoNegotiation; // [V2G2-172]
|
||||
|
||||
dlog(DLOG_LEVEL_INFO, "Handling SupportedAppProtocolReq");
|
||||
conn->ctx->current_v2g_msg = V2G_SUPPORTED_APP_PROTOCOL_MSG;
|
||||
|
||||
if (decode_appHand_exiDocument(&conn->stream, &conn->handshake_req) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "decode_appHandExiDocument() failed");
|
||||
return V2G_EVENT_TERMINATE_CONNECTION; // If the mesage can't be decoded we have to terminate the tcp-connection
|
||||
// (e.g. after an unexpected message)
|
||||
}
|
||||
|
||||
types::iso15118::AppProtocols app_protocols; // to publish supported app protocol array
|
||||
|
||||
for (i = 0; i < conn->handshake_req.supportedAppProtocolReq.AppProtocol.arrayLen; i++) {
|
||||
struct appHand_AppProtocolType* app_proto = &conn->handshake_req.supportedAppProtocolReq.AppProtocol.array[i];
|
||||
char* proto_ns = strndup(static_cast<const char*>(app_proto->ProtocolNamespace.characters),
|
||||
app_proto->ProtocolNamespace.charactersLen);
|
||||
|
||||
if (!proto_ns) {
|
||||
dlog(DLOG_LEVEL_ERROR, "out-of-memory condition");
|
||||
return V2G_EVENT_TERMINATE_CONNECTION;
|
||||
}
|
||||
|
||||
dlog(DLOG_LEVEL_TRACE,
|
||||
"handshake_req: Namespace: %s, Version: %" PRIu32 ".%" PRIu32 ", SchemaID: %" PRIu8 ", Priority: %" PRIu8,
|
||||
proto_ns, app_proto->VersionNumberMajor, app_proto->VersionNumberMinor, app_proto->SchemaID,
|
||||
app_proto->Priority);
|
||||
|
||||
if ((conn->ctx->supported_protocols & (1 << V2G_PROTO_DIN70121)) &&
|
||||
(strcmp(proto_ns, DIN_70121_MSG_DEF) == 0) && (app_proto->VersionNumberMajor == DIN_70121_MAJOR) &&
|
||||
(ev_app_priority >= app_proto->Priority)) {
|
||||
conn->handshake_resp.supportedAppProtocolRes.ResponseCode =
|
||||
(app_proto->VersionNumberMinor == DIN_70121_MINOR)
|
||||
? appHand_responseCodeType_OK_SuccessfulNegotiation
|
||||
: appHand_responseCodeType_OK_SuccessfulNegotiationWithMinorDeviation;
|
||||
ev_app_priority = app_proto->Priority;
|
||||
conn->handshake_resp.supportedAppProtocolRes.SchemaID = app_proto->SchemaID;
|
||||
conn->ctx->selected_protocol = V2G_PROTO_DIN70121;
|
||||
} else if ((conn->ctx->supported_protocols & (1 << V2G_PROTO_ISO15118_2013)) &&
|
||||
(strcmp(proto_ns, ISO_15118_2013_MSG_DEF) == 0) &&
|
||||
(app_proto->VersionNumberMajor == ISO_15118_2013_MAJOR) &&
|
||||
(ev_app_priority >= app_proto->Priority)) {
|
||||
|
||||
conn->handshake_resp.supportedAppProtocolRes.ResponseCode =
|
||||
(app_proto->VersionNumberMinor == ISO_15118_2013_MINOR)
|
||||
? appHand_responseCodeType_OK_SuccessfulNegotiation
|
||||
: appHand_responseCodeType_OK_SuccessfulNegotiationWithMinorDeviation;
|
||||
ev_app_priority = app_proto->Priority;
|
||||
conn->handshake_resp.supportedAppProtocolRes.SchemaID = app_proto->SchemaID;
|
||||
conn->ctx->selected_protocol = V2G_PROTO_ISO15118_2013;
|
||||
}
|
||||
|
||||
if (conn->ctx->debugMode == true) {
|
||||
const types::iso15118::AppProtocol protocol = {
|
||||
std::string(proto_ns), static_cast<int32_t>(app_proto->VersionNumberMajor),
|
||||
static_cast<int32_t>(app_proto->VersionNumberMinor), static_cast<int32_t>(app_proto->SchemaID),
|
||||
static_cast<int32_t>(app_proto->Priority)};
|
||||
|
||||
app_protocols.Protocols.push_back(protocol);
|
||||
}
|
||||
|
||||
// TODO: ISO15118v2
|
||||
free(proto_ns);
|
||||
}
|
||||
|
||||
if (conn->ctx->debugMode == true) {
|
||||
conn->ctx->p_charger->publish_ev_app_protocol(app_protocols);
|
||||
/* form the content of V2G_Message type and publish the request*/
|
||||
publish_var_V2G_Message(conn, true);
|
||||
}
|
||||
|
||||
std::string selected_protocol_str;
|
||||
if (conn->handshake_resp.supportedAppProtocolRes.ResponseCode ==
|
||||
appHand_responseCodeType_OK_SuccessfulNegotiation ||
|
||||
conn->handshake_resp.supportedAppProtocolRes.ResponseCode ==
|
||||
appHand_responseCodeType_OK_SuccessfulNegotiationWithMinorDeviation) {
|
||||
conn->handshake_resp.supportedAppProtocolRes.SchemaID_isUsed = (unsigned int)1;
|
||||
if (V2G_PROTO_DIN70121 == conn->ctx->selected_protocol) {
|
||||
dlog(DLOG_LEVEL_INFO, "Protocol negotiation was successful. Selected protocol is DIN70121");
|
||||
selected_protocol_str = "DIN70121";
|
||||
} else if (V2G_PROTO_ISO15118_2013 == conn->ctx->selected_protocol) {
|
||||
dlog(DLOG_LEVEL_INFO, "Protocol negotiation was successful. Selected protocol is ISO15118");
|
||||
selected_protocol_str = "ISO15118-2-2013";
|
||||
} else if (V2G_PROTO_ISO15118_2010 == conn->ctx->selected_protocol) {
|
||||
dlog(DLOG_LEVEL_INFO, "Protocol negotiation was successful. Selected protocol is ISO15118-2010");
|
||||
selected_protocol_str = "ISO15118-2-2010";
|
||||
}
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_ERROR, "No compatible protocol found");
|
||||
selected_protocol_str = "None";
|
||||
next_event = V2G_EVENT_SEND_AND_TERMINATE; // Send response and terminate tcp-connection
|
||||
}
|
||||
|
||||
if (conn->ctx->debugMode == true) {
|
||||
conn->ctx->p_charger->publish_selected_protocol(selected_protocol_str);
|
||||
}
|
||||
|
||||
if (conn->ctx->is_connection_terminated == true) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Connection is terminated. Abort charging");
|
||||
return V2G_EVENT_TERMINATE_CONNECTION; // Abort charging without sending a response
|
||||
}
|
||||
|
||||
/* Validate response code */
|
||||
if ((conn->ctx->intl_emergency_shutdown == true) || (conn->ctx->stop_hlc == true) ||
|
||||
(V2G_EVENT_SEND_AND_TERMINATE == next_event)) {
|
||||
conn->handshake_resp.supportedAppProtocolRes.ResponseCode = appHand_responseCodeType_Failed_NoNegotiation;
|
||||
dlog(DLOG_LEVEL_ERROR, "Abort charging session");
|
||||
|
||||
if (conn->ctx->terminate_connection_on_failed_response == true) {
|
||||
next_event = V2G_EVENT_SEND_AND_TERMINATE; // send response and terminate the TCP-connection
|
||||
}
|
||||
}
|
||||
|
||||
/* encode response at the right buffer location */
|
||||
conn->stream.byte_pos = V2GTP_HEADER_LENGTH;
|
||||
conn->stream.bit_count = 0;
|
||||
|
||||
if (encode_appHand_exiDocument(&conn->stream, &conn->handshake_resp) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Encoding of the protocol handshake message failed");
|
||||
next_event = V2G_EVENT_SEND_AND_TERMINATE;
|
||||
}
|
||||
|
||||
return next_event;
|
||||
}
|
||||
|
||||
int v2g_handle_connection(struct v2g_connection* conn) {
|
||||
int rv = -1;
|
||||
enum v2g_event rvAppHandshake = V2G_EVENT_NO_EVENT;
|
||||
bool stop_receiving_loop = false;
|
||||
int64_t start_time = 0; // in ms
|
||||
|
||||
enum v2g_protocol selected_protocol = V2G_UNKNOWN_PROTOCOL;
|
||||
v2g_ctx_init_charging_state(conn->ctx, false);
|
||||
conn->buffer = static_cast<uint8_t*>(malloc(DEFAULT_BUFFER_SIZE));
|
||||
if (!conn->buffer)
|
||||
return -1;
|
||||
|
||||
/* static setup */
|
||||
conn->stream.data = conn->buffer;
|
||||
|
||||
/* Here is a good point to wait until the customer is ready for a resumed session,
|
||||
* because we are waiting for the incoming message of the ev */
|
||||
if (conn->d_link_action == dLinkAction::D_LINK_ACTION_PAUSE) {
|
||||
// TODO: D_LINK pause
|
||||
}
|
||||
|
||||
do {
|
||||
/* setup for receive */
|
||||
conn->stream.data[0] = 0;
|
||||
conn->payload_len = 0;
|
||||
exi_bitstream_init(&conn->stream, conn->buffer, 0, 0, nullptr);
|
||||
|
||||
/* next call return -1 on error, 1 if connection was closed unexpectedly, 0 on success */
|
||||
rv = v2g_incoming_v2gtp(conn);
|
||||
|
||||
if (rv != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "v2g_incoming_v2gtp() failed");
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if (conn->ctx->is_connection_terminated == true) {
|
||||
rv = -1;
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
/* next call return -1 on non-recoverable errors, 1 on recoverable errors, 0 on success */
|
||||
rvAppHandshake = v2g_handle_apphandshake(conn);
|
||||
|
||||
if (rvAppHandshake == V2G_EVENT_IGNORE_MSG) {
|
||||
dlog(DLOG_LEVEL_WARNING, "v2g_handle_apphandshake() failed, ignoring packet");
|
||||
}
|
||||
} while ((rv == 1) && (rvAppHandshake == V2G_EVENT_IGNORE_MSG));
|
||||
|
||||
/* stream setup for sending is done within v2g_handle_apphandshake */
|
||||
/* send supportedAppRes message */
|
||||
if ((rvAppHandshake == V2G_EVENT_SEND_AND_TERMINATE) || (rvAppHandshake == V2G_EVENT_NO_EVENT)) {
|
||||
/* form the content of V2G_Message type and publish the response for debugging*/
|
||||
if (conn->ctx->debugMode == true) {
|
||||
publish_var_V2G_Message(conn, false);
|
||||
}
|
||||
|
||||
rv = v2g_outgoing_v2gtp(conn);
|
||||
|
||||
if (rv == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "v2g_outgoing_v2gtp() failed");
|
||||
goto error_out;
|
||||
}
|
||||
}
|
||||
|
||||
/* terminate connection, if supportedApp handshake has failed */
|
||||
if ((rvAppHandshake == V2G_EVENT_SEND_AND_TERMINATE) || (rvAppHandshake == V2G_EVENT_TERMINATE_CONNECTION)) {
|
||||
rv = -1;
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
/* Backup the selected protocol, because this value is shared and can be reseted while unplugging. */
|
||||
selected_protocol = conn->ctx->selected_protocol;
|
||||
|
||||
/* allocate in/out documents dynamically */
|
||||
switch (selected_protocol) {
|
||||
case V2G_PROTO_DIN70121:
|
||||
case V2G_PROTO_ISO15118_2010:
|
||||
conn->exi_in.dinEXIDocument = static_cast<struct din_exiDocument*>(calloc(1, sizeof(struct din_exiDocument)));
|
||||
if (conn->exi_in.dinEXIDocument == NULL) {
|
||||
dlog(DLOG_LEVEL_ERROR, "out-of-memory");
|
||||
goto error_out;
|
||||
}
|
||||
conn->exi_out.dinEXIDocument = static_cast<struct din_exiDocument*>(calloc(1, sizeof(struct din_exiDocument)));
|
||||
if (conn->exi_out.dinEXIDocument == NULL) {
|
||||
dlog(DLOG_LEVEL_ERROR, "out-of-memory");
|
||||
goto error_out;
|
||||
}
|
||||
break;
|
||||
case V2G_PROTO_ISO15118_2013:
|
||||
conn->exi_in.iso2EXIDocument =
|
||||
static_cast<struct iso2_exiDocument*>(calloc(1, sizeof(struct iso2_exiDocument)));
|
||||
if (conn->exi_in.iso2EXIDocument == NULL) {
|
||||
dlog(DLOG_LEVEL_ERROR, "out-of-memory");
|
||||
goto error_out;
|
||||
}
|
||||
conn->exi_out.iso2EXIDocument =
|
||||
static_cast<struct iso2_exiDocument*>(calloc(1, sizeof(struct iso2_exiDocument)));
|
||||
if (conn->exi_out.iso2EXIDocument == NULL) {
|
||||
dlog(DLOG_LEVEL_ERROR, "out-of-memory");
|
||||
goto error_out;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
goto error_out; // if protocol is unknown
|
||||
}
|
||||
|
||||
do {
|
||||
/* setup for receive */
|
||||
conn->stream.data[0] = 0;
|
||||
conn->stream.bit_count = 0;
|
||||
conn->stream.byte_pos = 0;
|
||||
conn->payload_len = 0;
|
||||
|
||||
/* next call return -1 on error, 1 connection was closed unexpectedly, 0 on success */
|
||||
rv = v2g_incoming_v2gtp(conn);
|
||||
|
||||
if (rv == 1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "Timeout waiting for next request or peer closed connection");
|
||||
break;
|
||||
} else if (rv == -1) {
|
||||
dlog(DLOG_LEVEL_ERROR, "v2g_incoming_v2gtp() (previous message \"%s\") failed",
|
||||
v2g_msg_type[conn->ctx->last_v2g_msg]);
|
||||
break;
|
||||
}
|
||||
|
||||
start_time = getmonotonictime(); // To calc the duration of req msg configuration
|
||||
|
||||
/* according to agreed protocol decode the stream */
|
||||
enum v2g_event v2gEvent = V2G_EVENT_NO_EVENT;
|
||||
switch (selected_protocol) {
|
||||
case V2G_PROTO_DIN70121:
|
||||
case V2G_PROTO_ISO15118_2010:
|
||||
memset(conn->exi_in.dinEXIDocument, 0, sizeof(struct din_exiDocument));
|
||||
rv = decode_din_exiDocument(&conn->stream, conn->exi_in.dinEXIDocument);
|
||||
if (rv != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "decode_dinExiDocument() (previous message \"%s\") failed: %d",
|
||||
v2g_msg_type[conn->ctx->last_v2g_msg], rv);
|
||||
/* we must ignore packet which we cannot decode, so reset rv to zero to stay in loop */
|
||||
rv = 0;
|
||||
v2gEvent = V2G_EVENT_IGNORE_MSG;
|
||||
break;
|
||||
}
|
||||
|
||||
memset(conn->exi_out.dinEXIDocument, 0, sizeof(struct din_exiDocument));
|
||||
|
||||
v2gEvent = din_handle_request(conn);
|
||||
break;
|
||||
|
||||
case V2G_PROTO_ISO15118_2013:
|
||||
memset(conn->exi_in.iso2EXIDocument, 0, sizeof(struct iso2_exiDocument));
|
||||
rv = decode_iso2_exiDocument(&conn->stream, conn->exi_in.iso2EXIDocument);
|
||||
if (rv != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "decode_iso2_exiDocument() (previous message \"%s\") failed: %d",
|
||||
v2g_msg_type[conn->ctx->last_v2g_msg], rv);
|
||||
/* we must ignore packet which we cannot decode, so reset rv to zero to stay in loop */
|
||||
rv = 0;
|
||||
v2gEvent = V2G_EVENT_IGNORE_MSG;
|
||||
break;
|
||||
}
|
||||
conn->stream.byte_pos = 0; // Reset pos for the case if exi msg will be configured over mqtt
|
||||
memset(conn->exi_out.iso2EXIDocument, 0, sizeof(struct iso2_exiDocument));
|
||||
|
||||
v2gEvent = iso_handle_request(conn);
|
||||
|
||||
break;
|
||||
default:
|
||||
goto error_out; // if protocol is unknown
|
||||
}
|
||||
|
||||
/* form the content of V2G_Message type and publish the request*/
|
||||
if (conn->ctx->debugMode == true) {
|
||||
publish_var_V2G_Message(conn, true);
|
||||
}
|
||||
|
||||
switch (v2gEvent) {
|
||||
case V2G_EVENT_SEND_AND_TERMINATE:
|
||||
stop_receiving_loop = true;
|
||||
case V2G_EVENT_NO_EVENT: { // fall-through intended
|
||||
/* Reset v2g-buffer */
|
||||
conn->stream.data[0] = 0;
|
||||
conn->stream.bit_count = 0;
|
||||
conn->stream.byte_pos = V2GTP_HEADER_LENGTH;
|
||||
conn->stream.data_size = DEFAULT_BUFFER_SIZE;
|
||||
|
||||
/* Configure msg and send */
|
||||
switch (selected_protocol) {
|
||||
case V2G_PROTO_DIN70121:
|
||||
case V2G_PROTO_ISO15118_2010:
|
||||
if ((rv = encode_din_exiDocument(&conn->stream, conn->exi_out.dinEXIDocument)) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "encode_dinExiDocument() (message \"%s\") failed: %d",
|
||||
v2g_msg_type[conn->ctx->current_v2g_msg], rv);
|
||||
}
|
||||
break;
|
||||
case V2G_PROTO_ISO15118_2013:
|
||||
if ((rv = encode_iso2_exiDocument(&conn->stream, conn->exi_out.iso2EXIDocument)) != 0) {
|
||||
dlog(DLOG_LEVEL_ERROR, "encode_iso2_exiDocument() (message \"%s\") failed: %d",
|
||||
v2g_msg_type[conn->ctx->current_v2g_msg], rv);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
goto error_out; // if protocol is unknown
|
||||
}
|
||||
/* Wait max. res-time before sending the next response */
|
||||
int64_t time_to_conf_res = getmonotonictime() - start_time;
|
||||
|
||||
if (time_to_conf_res < MAX_RES_TIME) {
|
||||
// dlog(DLOG_LEVEL_ERROR,"time_to_conf_res %llu", time_to_conf_res);
|
||||
std::this_thread::sleep_for(std::chrono::microseconds((MAX_RES_TIME - time_to_conf_res) * 1000));
|
||||
} else {
|
||||
dlog(DLOG_LEVEL_WARNING, "Response message (type %d) not configured within %d ms (took %" PRIi64 " ms)",
|
||||
conn->ctx->current_v2g_msg, MAX_RES_TIME, time_to_conf_res);
|
||||
}
|
||||
}
|
||||
case V2G_EVENT_SEND_RECV_EXI_MSG: { // fall-through intended
|
||||
/* form the content of V2G_Message type and publish the response for debugging*/
|
||||
if (conn->ctx->debugMode == true) {
|
||||
publish_var_V2G_Message(conn, false);
|
||||
}
|
||||
|
||||
/* Write header and send next res-msg */
|
||||
if ((rv != 0) || ((rv = v2g_outgoing_v2gtp(conn)) == -1)) {
|
||||
dlog(DLOG_LEVEL_ERROR, "v2g_outgoing_v2gtp() \"%s\" failed: %d",
|
||||
v2g_msg_type[conn->ctx->current_v2g_msg], rv);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case V2G_EVENT_IGNORE_MSG:
|
||||
dlog(DLOG_LEVEL_ERROR, "Ignoring V2G request message \"%s\". Waiting for next request",
|
||||
v2g_msg_type[conn->ctx->current_v2g_msg]);
|
||||
break;
|
||||
case V2G_EVENT_TERMINATE_CONNECTION: // fall-through intended
|
||||
default:
|
||||
dlog(DLOG_LEVEL_ERROR, "Failed to handle V2G request message \"%s\"",
|
||||
v2g_msg_type[conn->ctx->current_v2g_msg]);
|
||||
stop_receiving_loop = true;
|
||||
break;
|
||||
}
|
||||
} while ((rv == 0) && (stop_receiving_loop == false));
|
||||
|
||||
error_out:
|
||||
switch (selected_protocol) {
|
||||
case V2G_PROTO_DIN70121:
|
||||
case V2G_PROTO_ISO15118_2010:
|
||||
if (conn->exi_in.dinEXIDocument != NULL)
|
||||
free(conn->exi_in.dinEXIDocument);
|
||||
if (conn->exi_out.dinEXIDocument != NULL)
|
||||
free(conn->exi_out.dinEXIDocument);
|
||||
break;
|
||||
case V2G_PROTO_ISO15118_2013:
|
||||
if (conn->exi_in.iso2EXIDocument != NULL)
|
||||
free(conn->exi_in.iso2EXIDocument);
|
||||
if (conn->exi_out.iso2EXIDocument != NULL)
|
||||
free(conn->exi_out.iso2EXIDocument);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (conn->buffer != NULL) {
|
||||
free(conn->buffer);
|
||||
}
|
||||
|
||||
conn->last_v2g_msg_at_disconnect = conn->ctx->current_v2g_msg;
|
||||
v2g_ctx_init_charging_state(conn->ctx, true);
|
||||
|
||||
return rv ? -1 : 0;
|
||||
}
|
||||
|
||||
uint64_t v2g_session_id_from_exi(bool is_iso, void* exi_in) {
|
||||
uint64_t session_id = 0;
|
||||
|
||||
if (is_iso) {
|
||||
struct iso2_exiDocument* req = static_cast<struct iso2_exiDocument*>(exi_in);
|
||||
struct iso2_MessageHeaderType* hdr = &req->V2G_Message.Header;
|
||||
|
||||
/* the provided session id could be smaller (error) in case that the peer did not
|
||||
* send our full session id back to us; this is why we init the id with 0 above
|
||||
* and only copy the provided byte len
|
||||
*/
|
||||
memcpy(&session_id, &hdr->SessionID.bytes, std::min((int)sizeof(session_id), (int)hdr->SessionID.bytesLen));
|
||||
} else {
|
||||
struct din_exiDocument* req = static_cast<struct din_exiDocument*>(exi_in);
|
||||
struct din_MessageHeaderType* hdr = &req->V2G_Message.Header;
|
||||
|
||||
/* see comment above */
|
||||
memcpy(&session_id, &hdr->SessionID.bytes, std::min((int)sizeof(session_id), (int)hdr->SessionID.bytesLen));
|
||||
}
|
||||
|
||||
return session_id;
|
||||
}
|
||||
46
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_server.hpp
Normal file
46
tools/EVerest-main/modules/EVSE/EvseV2G/v2g_server.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 chargebyte GmbH
|
||||
// Copyright (C) 2023 Contributors to EVerest
|
||||
#ifndef V2G_SERVER_H
|
||||
#define V2G_SERVER_H
|
||||
|
||||
#include "v2g.hpp"
|
||||
|
||||
static const char* v2g_msg_type[] = {
|
||||
"Supported App Protocol",
|
||||
"Session Setup",
|
||||
"Service Discovery",
|
||||
"Service Detail",
|
||||
"Payment Service Selection",
|
||||
"Payment Details",
|
||||
"Authorization",
|
||||
"Charge Parameter Discovery",
|
||||
"Metering Receipt",
|
||||
"Certificate Update",
|
||||
"Certificate Installation",
|
||||
"Charging Status",
|
||||
"Cable Check",
|
||||
"Pre Charge",
|
||||
"Power Delivery",
|
||||
"Current Demand",
|
||||
"Welding Detection",
|
||||
"Session Stop",
|
||||
"Unknown",
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief v2g_handle_connection This function handles a v2g-charging-session.
|
||||
* \param conn hold the context of the v2g-connection.
|
||||
* \return Returns 0 if the v2g-session was successfully stopped, otherwise -1.
|
||||
*/
|
||||
int v2g_handle_connection(struct v2g_connection* conn);
|
||||
|
||||
/*!
|
||||
* \brief v2g_session_id_from_exi This function extracts session ID from an EXI stream.
|
||||
* \param is_iso determines if ISO or DIN should be handled.
|
||||
* \param exi_in holds the input EXI stream.
|
||||
* \return Returns the extracted session ID.
|
||||
*/
|
||||
uint64_t v2g_session_id_from_exi(bool is_iso, void* exi_in);
|
||||
|
||||
#endif /* V2G_SERVER_H */
|
||||
Reference in New Issue
Block a user