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:
362
tools/EVerest-main/modules/API/RpcApi/rpc/RpcHandler.cpp
Normal file
362
tools/EVerest-main/modules/API/RpcApi/rpc/RpcHandler.cpp
Normal file
@@ -0,0 +1,362 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user