Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,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",
],
)

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 */

View File

@@ -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

View File

@@ -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_

View File

@@ -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_

View File

@@ -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

View File

@@ -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_

File diff suppressed because it is too large Load Diff

View 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 */

View 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
======================= ==================

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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 */

View 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];
}

View 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 */

View 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

View 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");
}

View 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 */

View 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})

View File

@@ -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_

View 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
```

View File

@@ -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

View File

@@ -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_

View File

@@ -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

View File

@@ -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_

View 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);
}
}

View 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

View File

@@ -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;
}

View 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

View 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

View 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

View 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

View 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;
}

View 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;
}

View 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 */

View 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 */

View 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(&parameter_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),
&parameterSet.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),
&parameterSet.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),
&parameterSet.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),
&parameterSet.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);
}
}

View 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 */

View 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;
}

View 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 */