- 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
363 lines
17 KiB
C++
363 lines
17 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright chargebyte GmbH and Contributors to EVerest
|
|
|
|
#include "RpcHandler.hpp"
|
|
|
|
#include <jsonrpccxx/client.hpp>
|
|
#include <jsonrpccxx/common.hpp>
|
|
#include <jsonrpccxx/server.hpp>
|
|
#include <jsonrpccxx/typemapper.hpp>
|
|
|
|
#include <everest/logging.hpp>
|
|
|
|
#include "../helpers/Conversions.hpp" // For to_json() for nlohmann::json
|
|
#include "../helpers/LimitDecimalPlaces.hpp"
|
|
|
|
namespace rpc {
|
|
|
|
template <typename T> struct is_optional : std::false_type {};
|
|
|
|
static const std::chrono::milliseconds REQ_COLLECTION_TIMEOUT(
|
|
10); // Timeout for collecting client requests. After this timeout, the requests will be processed.
|
|
static const std::chrono::milliseconds
|
|
REQ_PROCESSING_TIMEOUT(50); // Timeout for processing requests. After this timeout, the request will be processed.
|
|
|
|
// Helper functions
|
|
template <typename T> struct is_optional<std::optional<T>> : std::true_type {};
|
|
|
|
template <typename T> auto extract_param(const nlohmann::json& j) {
|
|
if constexpr (is_optional<T>::value) {
|
|
using InnerT = typename T::value_type;
|
|
if (j.is_null()) {
|
|
return std::optional<InnerT>{};
|
|
} else {
|
|
return std::optional<InnerT>{j.get<InnerT>()};
|
|
}
|
|
} else {
|
|
return j.get<T>();
|
|
}
|
|
}
|
|
|
|
// json-rpc-cpp does not support optional parameters in method signatures
|
|
// so we need to create our own get_handle function to handle methods with optional parameters correctly
|
|
template <typename...> using void_t = void;
|
|
|
|
template <typename Default, template <typename...> class Op, typename... Args> struct detector {
|
|
using value_t = std::false_type;
|
|
using type = Default;
|
|
};
|
|
|
|
template <template <typename...> class Op, typename... Args> struct detector<void_t<Op<Args...>>, Op, Args...> {
|
|
using value_t = std::true_type;
|
|
using type = Op<Args...>;
|
|
};
|
|
|
|
template <template <typename...> class Op, typename... Args>
|
|
using is_detected = typename detector<void, Op, Args...>::value_t;
|
|
|
|
template <typename T>
|
|
using is_to_json_serializable = decltype(to_json(std::declval<nlohmann::json&>(), std::declval<T>()));
|
|
|
|
template <typename T> constexpr bool is_to_json_serializable_v = is_detected<is_to_json_serializable, T>::value;
|
|
|
|
template <typename T, typename MethodT, typename... ParamTypes, std::size_t... I>
|
|
auto invoke_with_params_impl(T& instance, MethodT method, const json& params, std::index_sequence<I...>) {
|
|
return (instance.*method)((extract_param<std::remove_reference_t<ParamTypes>>(params.at(I)))...);
|
|
}
|
|
|
|
template <typename T, typename MethodT, typename... ParamTypes>
|
|
auto invoke_with_params(T& instance, MethodT method, const json& params) {
|
|
return invoke_with_params_impl<T, MethodT, ParamTypes...>(instance, method, params,
|
|
std::index_sequence_for<ParamTypes...>{});
|
|
}
|
|
|
|
template <typename T, typename ReturnType, typename... ParamTypes>
|
|
MethodHandle get_handle(ReturnType (T::*method)(ParamTypes...), T& instance, int precision = 3) {
|
|
return [&instance, method, precision](const json& params) -> json {
|
|
if (!params.is_array()) {
|
|
throw std::runtime_error("params must be array");
|
|
}
|
|
|
|
constexpr size_t expected = sizeof...(ParamTypes);
|
|
if (params.size() != expected) {
|
|
throw std::runtime_error("invalid number of parameters");
|
|
}
|
|
|
|
auto result = invoke_with_params<T, decltype(method), ParamTypes...>(instance, method, params);
|
|
|
|
if constexpr (std::is_same_v<ReturnType, void>) {
|
|
return json(); // no return value
|
|
} else if constexpr (std::is_same_v<ReturnType, nlohmann::json>) {
|
|
return result; // return json directly
|
|
} else if constexpr (is_to_json_serializable_v<ReturnType>) {
|
|
nlohmann::json j;
|
|
to_json(j, result);
|
|
helpers::round_floats_in_json(j, precision);
|
|
return j; // convert to json and round floats
|
|
} else {
|
|
return result; // fallback: no conversion to json possible, return as is
|
|
}
|
|
};
|
|
}
|
|
|
|
RpcHandler::RpcHandler(std::vector<std::shared_ptr<server::TransportInterface>> transport_interfaces,
|
|
DataStoreCharger& dataobj,
|
|
std::unique_ptr<request_interface::RequestHandlerInterface> request_handler, int precision) :
|
|
m_transport_interfaces(std::move(transport_interfaces)),
|
|
m_data_store(dataobj),
|
|
m_methods_api(dataobj),
|
|
m_methods_chargepoint(dataobj),
|
|
m_methods_evse(dataobj, std::move(request_handler)),
|
|
m_conn(m_transport_interfaces, m_api_hello_received, m_mtx),
|
|
m_precision(precision) {
|
|
init_rpc_api();
|
|
init_transport_interfaces();
|
|
m_notifications_evse = std::make_unique<notifications::Evse>(m_rpc_server, dataobj, m_precision);
|
|
m_notifications_chargepoint = std::make_unique<notifications::ChargePoint>(m_rpc_server, dataobj, m_precision);
|
|
}
|
|
|
|
void RpcHandler::init_rpc_api() {
|
|
// Initialize the RPC API here
|
|
m_methods_api.set_authentication_required(false);
|
|
m_methods_api.set_api_version(API_VERSION);
|
|
m_rpc_server = std::make_shared<JsonRpc2ServerWithClient>(m_conn);
|
|
m_rpc_server->Add(methods::METHOD_API_HELLO, get_handle(&methods::Api::hello, m_methods_api, m_precision), {});
|
|
m_rpc_server->Add(methods::METHOD_CHARGEPOINT_GET_EVSE_INFOS,
|
|
get_handle(&methods::ChargePoint::getEVSEInfos, m_methods_chargepoint, m_precision), {});
|
|
m_rpc_server->Add(methods::METHOD_CHARGEPOINT_GET_ACTIVE_ERRORS,
|
|
get_handle(&methods::ChargePoint::getActiveErrors, m_methods_chargepoint, m_precision), {});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_GET_INFO, get_handle(&methods::Evse::get_info, m_methods_evse, m_precision),
|
|
{"evse_index"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_GET_STATUS,
|
|
get_handle(&methods::Evse::get_status, m_methods_evse, m_precision), {"evse_index"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_GET_HARDWARE_CAPABILITIES,
|
|
get_handle(&methods::Evse::get_hardware_capabilities, m_methods_evse, m_precision),
|
|
{"evse_index"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_SET_CHARGING_ALLOWED,
|
|
get_handle(&methods::Evse::set_charging_allowed, m_methods_evse, m_precision),
|
|
{"evse_index", "charging_allowed"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_GET_METER_DATA,
|
|
get_handle(&methods::Evse::get_meter_data, m_methods_evse, m_precision), {"evse_index"});
|
|
// TODO: m_rpc_server->Add(methods::METHOD_EVSE_SET_AC_CHARGING, (get_handle(&methods::Evse::set_ac_charging,
|
|
// m_methods_evse, m_precision)),
|
|
// {"evse_index", "charging_allowed", "max_current", "phase_count"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_SET_AC_CHARGING_CURRENT,
|
|
get_handle(&methods::Evse::set_ac_charging_current, m_methods_evse, m_precision),
|
|
{"evse_index", "max_current"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_SET_AC_CHARGING_PHASE_COUNT,
|
|
get_handle(&methods::Evse::set_ac_charging_phase_count, m_methods_evse, m_precision),
|
|
{"evse_index", "phase_count"});
|
|
// TODO: m_rpc_server->Add(methods::METHOD_EVSE_SET_DC_CHARGING, (get_handle(&methods::Evse::set_dc_charging,
|
|
// m_methods_evse, m_precision)),
|
|
// {"evse_index", "charging_allowed", "max_power"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_SET_DC_CHARGING_POWER,
|
|
get_handle(&methods::Evse::set_dc_charging_power, m_methods_evse, m_precision),
|
|
{"evse_index", "max_power"});
|
|
m_rpc_server->Add(methods::METHOD_EVSE_ENABLE_CONNECTOR,
|
|
get_handle(&methods::Evse::enable_connector, m_methods_evse, m_precision),
|
|
{"evse_index", "connector_index", "enable", "priority"});
|
|
}
|
|
|
|
void RpcHandler::init_transport_interfaces() {
|
|
for (const auto& transport_interface : m_transport_interfaces) {
|
|
if (!transport_interface) {
|
|
throw std::runtime_error("Transport interface is null");
|
|
}
|
|
m_last_req_notification = std::chrono::steady_clock::now();
|
|
|
|
transport_interface->on_client_connected =
|
|
[this, transport_interface](const server::TransportInterface::ClientId& client_id,
|
|
const server::TransportInterface::Address& address) {
|
|
this->client_connected(transport_interface, client_id, address);
|
|
};
|
|
|
|
transport_interface->on_client_disconnected =
|
|
[this, transport_interface](const server::TransportInterface::ClientId& client_id) {
|
|
this->client_disconnected(transport_interface, client_id);
|
|
};
|
|
|
|
transport_interface->on_data_available =
|
|
[this, transport_interface](const server::TransportInterface::ClientId& client_id,
|
|
const server::TransportInterface::Data& data) {
|
|
this->data_available(transport_interface, client_id, data);
|
|
};
|
|
}
|
|
}
|
|
|
|
void RpcHandler::client_connected(const std::shared_ptr<server::TransportInterface>& transport_interface,
|
|
const server::TransportInterface::ClientId& client_id,
|
|
[[maybe_unused]] const server::TransportInterface::Address& address) {
|
|
// In case of a new client, we expect that the client will send an API.Hello request within 5 seconds.
|
|
// The API.Hello request is a handshake message sent by the client to establish a connection and verify
|
|
// compatibility. If the API.Hello request is not received within the timeout period, the connection will be
|
|
// terminated.
|
|
|
|
// Launch a detached thread to wait for the client hello message
|
|
std::thread([this, client_id, transport_interface]() {
|
|
std::unique_lock<std::mutex> lock(m_mtx);
|
|
if (m_cv_api_hello.wait_for(lock, CLIENT_HELLO_TIMEOUT, [this, client_id] {
|
|
return m_api_hello_received.find(client_id) != m_api_hello_received.end();
|
|
}) == false) {
|
|
// Client did not send hello, close connection
|
|
if (transport_interface) {
|
|
transport_interface->kill_client_connection(client_id, "Disconnected due to timeout");
|
|
} else {
|
|
// Log the error instead of throwing an exception in a detached thread
|
|
// to avoid undefined behavior.
|
|
EVLOG_error << "Transport interface is null during client connection timeout handling";
|
|
}
|
|
}
|
|
}).detach();
|
|
}
|
|
|
|
void RpcHandler::client_disconnected(const std::shared_ptr<server::TransportInterface>& transport_interface,
|
|
const server::TransportInterface::ClientId& client_id) {
|
|
if (transport_interface) {
|
|
std::lock_guard<std::mutex> lock(m_mtx);
|
|
m_api_hello_received.erase(client_id);
|
|
} else {
|
|
// Log the error instead of throwing an exception in a detached thread
|
|
// to avoid undefined behavior.
|
|
EVLOG_error << "Transport interface is null during client disconnection handling";
|
|
}
|
|
// Clean up the client data
|
|
std::lock_guard<std::mutex> lock(m_mtx);
|
|
auto it = messages.find(client_id);
|
|
if (it != messages.end()) {
|
|
messages.erase(it);
|
|
}
|
|
}
|
|
|
|
void RpcHandler::data_available(const std::shared_ptr<server::TransportInterface>& transport_interface,
|
|
const server::TransportInterface::ClientId& client_id,
|
|
const server::TransportInterface::Data& data) {
|
|
// Handle request
|
|
using namespace std::chrono;
|
|
|
|
try {
|
|
auto now = steady_clock::now();
|
|
|
|
nlohmann::json request = nlohmann::json::parse(data);
|
|
if (request.is_null()) {
|
|
EVLOG_error << "Received null request from client " << client_id;
|
|
return;
|
|
}
|
|
// Store message in a map with client_id as key
|
|
std::lock_guard<std::mutex> lock(m_mtx);
|
|
messages[client_id].data.push_back(request);
|
|
messages[client_id].transport_interface = transport_interface;
|
|
EVLOG_debug << "Received message from client " << client_id << ": " << request.dump();
|
|
|
|
auto elapsed = duration_cast<milliseconds>(now - m_last_req_notification);
|
|
if (elapsed >= REQ_COLLECTION_TIMEOUT) {
|
|
m_last_req_notification = now; // restart timer
|
|
m_cv_data_available.notify_all();
|
|
}
|
|
} catch (const nlohmann::json::parse_error& e) {
|
|
EVLOG_error << "Failed to parse JSON request from client " << client_id << ": " << e.what();
|
|
} catch (const std::exception& e) {
|
|
EVLOG_error << "Exception occurred while handling data available: " << e.what();
|
|
}
|
|
}
|
|
|
|
void RpcHandler::process_client_requests() {
|
|
while (m_is_running) {
|
|
std::unique_lock<std::mutex> lock(m_mtx);
|
|
// Wait for data to be available or timeout
|
|
m_cv_data_available.wait_for(lock, REQ_PROCESSING_TIMEOUT, [this]() {
|
|
// Iterate over all clients and check if data is available
|
|
for (const auto& [client_id, client_req] : messages) {
|
|
if (!client_req.data.empty()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Process requests for each client
|
|
bool all_requests_processed; // Flag to check if all requests are processed
|
|
do {
|
|
all_requests_processed = true;
|
|
for (auto& [client_id, client_req] : messages) {
|
|
if (client_req.data.empty()) {
|
|
continue; // Skip if no data available
|
|
}
|
|
// Process the data for this client
|
|
auto transport_interface = client_req.transport_interface;
|
|
|
|
if (!transport_interface) {
|
|
EVLOG_error << "Skip data. Transport interface is null for client " << client_id;
|
|
continue; // Skip if transport interface is null
|
|
}
|
|
|
|
// Get the first request from the client
|
|
nlohmann::json request = client_req.data.front();
|
|
client_req.data.pop_front(); // Remove the processed request
|
|
|
|
// Check if next request is available
|
|
if (client_req.data.empty()) {
|
|
all_requests_processed = false;
|
|
}
|
|
|
|
// Check if the request is an API.Hello request
|
|
if (is_api_hello_req(client_id, request)) {
|
|
// Notify condition variable to unblock the waiting thread
|
|
m_cv_api_hello.notify_all();
|
|
EVLOG_info << "API.Hello request received from client " << client_id;
|
|
} else {
|
|
// If it is not an API.Hello request, we need to check if the client has already sent an API.Hello
|
|
// request, if not, close the connection
|
|
if (m_api_hello_received.find(client_id) == m_api_hello_received.end()) {
|
|
EVLOG_debug << "Client " << client_id << " did not send API.Hello request. Closing connection.";
|
|
transport_interface->kill_client_connection(client_id,
|
|
"Disconnected due to missing API.Hello request");
|
|
continue; // Skip processing this request
|
|
}
|
|
}
|
|
|
|
// Process the request in a detached thread, because HandleRequest is blocking until the response is
|
|
// received
|
|
std::thread([this, transport_interface, client_id, request]() {
|
|
// Call the RPC server with the request
|
|
std::string res = m_rpc_server->HandleRequest(request.dump());
|
|
// Send the response back to the client
|
|
transport_interface->send_data(client_id, res);
|
|
EVLOG_debug << "Sent response to client " << client_id << ": " << res;
|
|
}).detach();
|
|
}
|
|
} while (!all_requests_processed && m_is_running);
|
|
}
|
|
}
|
|
|
|
void RpcHandler::start_server() {
|
|
m_is_running = true;
|
|
// Start all transport interfaces
|
|
for (const auto& transport_interface : m_transport_interfaces) {
|
|
if (!transport_interface->start_server()) {
|
|
throw std::runtime_error("Failed to start transport interface server");
|
|
}
|
|
}
|
|
|
|
// Start RPC receiver thread
|
|
m_rpc_recv_thread = std::thread([this]() { this->process_client_requests(); });
|
|
}
|
|
|
|
void RpcHandler::stop_server() {
|
|
m_is_running = false;
|
|
// Notify all threads to stop
|
|
m_cv_data_available.notify_all();
|
|
m_cv_api_hello.notify_all();
|
|
|
|
// Wait for the RPC receiver thread to finish
|
|
if (m_rpc_recv_thread.joinable()) {
|
|
m_rpc_recv_thread.join();
|
|
}
|
|
|
|
for (const auto& transport_interface : m_transport_interfaces) {
|
|
if (!transport_interface->stop_server()) {
|
|
throw std::runtime_error("Failed to stop transport interface server");
|
|
}
|
|
}
|
|
}
|
|
} // namespace rpc
|