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,31 @@
add_library(slac)
add_library(slac::slac ALIAS slac)
target_include_directories(slac PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_sources(slac
PRIVATE
src/channel.cpp
src/slac.cpp
src/packet_socket.cpp
)
target_link_libraries(slac
PRIVATE
everest::tls
)
add_subdirectory(io)
add_subdirectory(fsm/ev)
add_subdirectory(fsm/evse)
if(BUILD_DEV_TESTS)
add_subdirectory(test)
endif()
if(BUILD_TESTING)
add_subdirectory(tests)
endif()

View File

@@ -0,0 +1,21 @@
add_library(slac_fsm_ev)
add_library(slac::fsm::ev ALIAS slac_fsm_ev)
ev_register_library_target(slac_fsm_ev)
target_include_directories(slac_fsm_ev
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_sources(slac_fsm_ev
PRIVATE
src/context.cpp
src/states/others.cpp
src/states/sounding.cpp
)
target_link_libraries(slac_fsm_ev
PUBLIC
slac::slac
fsm::fsm
)

View File

@@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EV_SLAC_CONTEXT_HPP
#define EV_SLAC_CONTEXT_HPP
#include <functional>
#include <stdexcept>
#include <string>
#include <slac/slac.hpp>
namespace slac::fsm::ev {
namespace _context_detail {
template <typename SlacMessageType> struct MMTYPE;
template <> struct MMTYPE<slac::messages::cm_slac_parm_req> {
static const uint16_t value = slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::cm_start_atten_char_ind> {
static const uint16_t value = slac::defs::MMTYPE_CM_START_ATTEN_CHAR | slac::defs::MMTYPE_MODE_IND;
};
template <> struct MMTYPE<slac::messages::cm_mnbc_sound_ind> {
static const uint16_t value = slac::defs::MMTYPE_CM_MNBC_SOUND | slac::defs::MMTYPE_MODE_IND;
};
template <> struct MMTYPE<slac::messages::cm_atten_char_rsp> {
static const uint16_t value = slac::defs::MMTYPE_CM_ATTEN_CHAR | slac::defs::MMTYPE_MODE_RSP;
};
template <> struct MMTYPE<slac::messages::cm_slac_match_req> {
static const uint16_t value = slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::cm_set_key_req> {
static const uint16_t value = slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_REQ;
};
template <typename SlacMessageType> struct MMV {
// this is the default value for homeplug av 2.0 messages, which are
// backward compatible with homeplug av 1.1 messages
// non-backward (to 1.1) compatible message are CM_CHAN_EST,
// CM_AMP_MAP and CM_NW_STATS, these need to use AV_2_0
// older av 1.0 message need to use AV_1_0
static constexpr auto value = slac::defs::MMV::AV_1_1;
};
} // namespace _context_detail
struct ContextCallbacks {
std::function<void(slac::messages::HomeplugMessage&)> send_raw_slac{nullptr};
std::function<void(const std::string&)> signal_state{nullptr};
std::function<void(const std::string&)> log_debug{nullptr};
std::function<void(const std::string&)> log_info{nullptr};
std::function<void(const std::string&)> log_warn{nullptr};
std::function<void(const std::string&)> log_error{nullptr};
};
struct Context {
static constexpr std::array<uint8_t, ETH_ALEN> BROADCAST_MAC = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static constexpr std::array<uint8_t, ETH_ALEN> EV_PLC_MAC = {0x00, 0xB0, 0x52, 0x00, 0x00, 0x01};
Context(const ContextCallbacks& callbacks_, const std::array<uint8_t, ETH_ALEN>& mac) :
callbacks(callbacks_), ev_host_mac(mac) {
}
const std::array<uint8_t, ETH_ALEN> ev_host_mac{};
// event specific payloads
// FIXME (aw): due to the synchroneous nature of the fsm, this could be even a ptr/ref
slac::messages::HomeplugMessage slac_message;
// FIXME (aw): message should be const, but libslac doesn't allow for const ptr - needs changes in libslac
template <typename SlacMessageType>
void send_slac_message(const uint8_t* dest_mac, SlacMessageType const& message) {
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(dest_mac);
try {
hp_message.setup_payload(&message, sizeof(message), _context_detail::MMTYPE<SlacMessageType>::value,
_context_detail::MMV<SlacMessageType>::value);
} catch (const std::runtime_error& e) {
const auto error_message = std::string("Could not setup SLAC payload: ") + std::string(e.what());
log_error(error_message);
}
callbacks.send_raw_slac(hp_message);
}
// signal handlers
void signal_state(const std::string& state);
// logging util
void log_debug(const std::string& text);
void log_info(const std::string& text);
void log_warn(const std::string& text);
void log_error(const std::string& text);
private:
const ContextCallbacks& callbacks;
};
struct SessionParamaters {
SessionParamaters(const uint8_t* run_id, const uint8_t* evse_mac);
uint8_t run_id[slac::defs::RUN_ID_LEN];
uint8_t evse_mac[ETH_ALEN];
};
} // namespace slac::fsm::ev
#endif // EV_SLAC_CONTEXT_HPP

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EV_SLAC_FSM_HPP
#define EV_SLAC_FSM_HPP
#include <fsm/fsm.hpp>
#include "context.hpp"
namespace slac::fsm::ev {
enum class Event {
RESET,
TRIGGER_MATCHING,
SLAC_MESSAGE,
// internal events
FAILED,
};
using FSMReturnType = int;
using FSM = ::fsm::FSM<Event, FSMReturnType>;
using FSMSimpleState = ::fsm::states::StateWithContext<FSM::SimpleStateType, Context>;
using FSMCompoundState = ::fsm::states::StateWithContext<FSM::CompoundStateType, Context>;
} // namespace slac::fsm::ev
#endif // EV_SLAC_FSM_HPP

View File

@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EV_SLAC_STATES_OTHERS_HPP
#define EV_SLAC_STATES_OTHERS_HPP
#include <chrono>
#include <everest/slac/fsm/ev/fsm.hpp>
namespace slac::fsm::ev {
struct ResetState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
};
struct InitSlacState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// only returns true, if valid
bool check_for_valid_parm_conf();
// sends out CM_SLAC_PARM.REQ, increases num of tries and returns the timeout until a response is expected
int send_parm_req();
std::chrono::time_point<std::chrono::steady_clock> next_timeout;
int num_of_tries{0};
uint8_t run_id[8];
};
struct MatchRequestState : public FSMSimpleState {
MatchRequestState(Context& ctx, SessionParamaters session_parameters);
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// return the pointer to the NMK, if valid
const uint8_t* check_for_valid_match_req_conf();
// sends out CM_SLAC_PARM.REQ, increases num of tries and returns the timeout until a response is expected
int send_match_req();
std::chrono::time_point<std::chrono::steady_clock> next_timeout;
int num_of_tries{0};
SessionParamaters session_parameters;
};
struct JoinNetworkState : public FSMSimpleState {
static constexpr auto SET_KEY_TIMEOUT_MS = 500;
JoinNetworkState(Context& ctx, const uint8_t* nmk);
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// only returns true, if valid
bool check_for_valid_set_key_conf();
std::chrono::time_point<std::chrono::steady_clock> timeout;
uint8_t nmk[slac::defs::NMK_LEN];
};
struct MatchedState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
void enter() final {
ctx.log_info("Entered matched state");
ctx.signal_state("MATCHED");
}
HandleEventReturnType handle_event(AllocatorType&, Event) final;
};
struct FailedState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
void enter() final {
ctx.log_info("Entered failed state");
}
HandleEventReturnType handle_event(AllocatorType&, Event) final;
};
} // namespace slac::fsm::ev
#endif // EV_SLAC_STATES_OTHERS_HPP

View File

@@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EV_SLAC_STATES_SOUNDING_HPP
#define EV_SLAC_STATES_SOUNDING_HPP
#include <chrono>
#include <slac/slac.hpp>
#include <everest/slac/fsm/ev/fsm.hpp>
namespace slac::fsm::ev {
struct SoundingState : public FSMSimpleState {
SoundingState(Context& ctx, SessionParamaters session_parameters);
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// returns true, if CM_ATTEN_CHAR.IND is expected and fulfills expectations and response has been send
bool handle_valid_atten_char_ind();
// returns true if further message will need to be send
bool do_sounding();
// timepoint until we need to receive the atten_char_ind message
std::chrono::time_point<std::chrono::steady_clock> sounding_timeout;
// timepoint until we internally want to send the next message
std::chrono::time_point<std::chrono::steady_clock> next_timeout;
int count_start_atten_char_sent{0};
int count_mnbc_sound_sent{0};
SessionParamaters session_parameters;
};
} // namespace slac::fsm::ev
#endif // EV_SLAC_STATES_SOUNDING_HPP

View File

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/slac/fsm/ev/context.hpp>
#include <cstring>
namespace slac::fsm::ev {
void Context::signal_state(const std::string& state) {
if (callbacks.signal_state) {
callbacks.signal_state(state);
}
}
void Context::log_debug(const std::string& text) {
if (callbacks.log_debug) {
callbacks.log_debug(text);
}
}
void Context::log_info(const std::string& text) {
if (callbacks.log_info) {
callbacks.log_info(text);
}
}
void Context::log_warn(const std::string& text) {
if (callbacks.log_warn) {
callbacks.log_warn(text);
}
}
void Context::log_error(const std::string& text) {
if (callbacks.log_error) {
callbacks.log_error(text);
}
}
SessionParamaters::SessionParamaters(const uint8_t* run_id_, const uint8_t* evse_mac_) {
memcpy(run_id, run_id_, sizeof(run_id));
memcpy(evse_mac, evse_mac_, sizeof(evse_mac));
}
} // namespace slac::fsm::ev

View File

@@ -0,0 +1,303 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/slac/fsm/ev/states/others.hpp>
#include <cstring>
#include <random>
#include <endian.h>
#include "timing_helper.hpp"
#include <everest/slac/fsm/ev/states/sounding.hpp>
namespace slac::fsm::ev {
void ResetState::enter() {
ctx.signal_state("UNMATCHED");
ctx.log_info("Entered Reset state");
}
FSMSimpleState::HandleEventReturnType ResetState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::TRIGGER_MATCHING) {
return sa.create_simple<InitSlacState>(ctx);
} else {
return sa.PASS_ON;
}
}
FSMSimpleState::CallbackReturnType ResetState::callback() {
ctx.log_info("Called callback of ResetState");
return {};
}
FSMSimpleState::HandleEventReturnType InitSlacState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
if (check_for_valid_parm_conf()) {
ctx.signal_state("MATCHING");
return sa.create_simple<SoundingState>(ctx, SessionParamaters{run_id, ctx.slac_message.get_src_mac()});
}
return sa.HANDLED_INTERNALLY;
} else if (ev == Event::FAILED) {
return sa.create_simple<FailedState>(ctx);
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
void InitSlacState::enter() {
ctx.log_info("Entered init state");
// generate random run_id
std::random_device rnd_dev;
std::mt19937 rng(rnd_dev());
std::uniform_int_distribution<std::mt19937::result_type> dist256(0, 255);
for (auto& id : this->run_id) {
id = dist256(rng);
}
}
FSMSimpleState::CallbackReturnType InitSlacState::callback() {
if (num_of_tries == 0) {
return send_parm_req();
}
// did already send a parm req, check for timeout
const auto now = std::chrono::steady_clock::now();
const auto time_left = milliseconds_left(now, next_timeout);
if (time_left > 0) {
// still have time
return time_left;
}
// timeout, check if we still have retries
if (num_of_tries < 100) {
// FIXME (aw): the norm says num_of_tries < slac::defs::C_EV_MATCH_RETRY
// but doesn't seem to work in 'real life'
return send_parm_req();
}
// no retries left fail
return Event::FAILED;
}
int InitSlacState::send_parm_req() {
slac::messages::cm_slac_parm_req msg;
msg.application_type = 0x0;
msg.security_type = 0x0;
memcpy(msg.run_id, run_id, sizeof(msg.run_id));
ctx.send_slac_message(ctx.BROADCAST_MAC.data(), msg);
num_of_tries++;
next_timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(slac::defs::TT_MATCH_RESPONSE_MS);
return slac::defs::TT_MATCH_RESPONSE_MS;
}
bool InitSlacState::check_for_valid_parm_conf() {
const auto mmtype = ctx.slac_message.get_mmtype();
if (mmtype != (slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_CNF)) {
return false;
}
// correct message type
const auto& parm_cnf = ctx.slac_message.get_payload<slac::messages::cm_slac_parm_cnf>();
// it is not clear, whether we should use the m-sound target anyhow ... or if we should really validate any of the
// fields except the run id
const auto same_run_id = (memcmp(parm_cnf.run_id, run_id, sizeof(run_id)) == 0);
return same_run_id;
}
MatchRequestState::MatchRequestState(Context& ctx, SessionParamaters session_parameters_) :
FSMSimpleState(ctx), session_parameters(std::move(session_parameters_)) {
}
FSMSimpleState::HandleEventReturnType MatchRequestState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
const auto nmk = check_for_valid_match_req_conf();
if (nmk) {
return sa.create_simple<JoinNetworkState>(ctx, nmk);
}
return sa.HANDLED_INTERNALLY;
} else if (ev == Event::FAILED) {
return sa.create_simple<FailedState>(ctx);
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
void MatchRequestState::enter() {
ctx.log_info("Entered MatchRequestState state");
}
FSMSimpleState::CallbackReturnType MatchRequestState::callback() {
if (num_of_tries == 0) {
return send_match_req();
}
// did already send a parm req, check for timeout
const auto now = std::chrono::steady_clock::now();
const auto time_left = milliseconds_left(now, next_timeout);
if (time_left > 0) {
// still have time
return time_left;
}
// timeout, check if we still have retries
if (num_of_tries < slac::defs::C_EV_MATCH_RETRY) {
return send_match_req();
}
// no retries left fail
return Event::FAILED;
}
const uint8_t* MatchRequestState::check_for_valid_match_req_conf() {
if (num_of_tries == 0) {
return nullptr;
}
const auto mmtype = ctx.slac_message.get_mmtype();
if (mmtype != (slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_CNF)) {
return nullptr;
}
// correct message type
const auto& match_cnf = ctx.slac_message.get_payload<slac::messages::cm_slac_match_cnf>();
const auto run_id_match =
(memcmp(session_parameters.run_id, match_cnf.run_id, sizeof(session_parameters.run_id)) == 0);
if (run_id_match) {
return match_cnf.nmk;
} else {
return nullptr;
}
}
int MatchRequestState::send_match_req() {
slac::messages::cm_slac_match_req msg;
msg.application_type = 0x0;
msg.security_type = 0x0;
msg.mvf_length = htole16(0x3e); // FIXME (aw) fixed constant
memset(msg.pev_id, 0, sizeof(msg.pev_id));
memcpy(msg.pev_mac, ctx.ev_host_mac.data(), sizeof(msg.pev_mac));
memset(msg.evse_id, 0, sizeof(msg.evse_id));
memcpy(msg.evse_mac, session_parameters.evse_mac, sizeof(msg.evse_mac));
memcpy(msg.run_id, session_parameters.run_id, sizeof(msg.run_id));
memset(msg._reserved, 0, sizeof(msg._reserved));
ctx.send_slac_message(session_parameters.evse_mac, msg);
num_of_tries++;
next_timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(slac::defs::TT_MATCH_RESPONSE_MS);
return slac::defs::TT_MATCH_RESPONSE_MS;
}
JoinNetworkState::JoinNetworkState(Context& ctx, const uint8_t* nmk_) : FSMSimpleState(ctx) {
memcpy(nmk, nmk_, sizeof(nmk));
}
FSMSimpleState::HandleEventReturnType JoinNetworkState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
if (check_for_valid_set_key_conf()) {
// FIXME (aw): later on, we also need to distinguish between set_key failed
return sa.create_simple<MatchedState>(ctx);
}
return sa.HANDLED_INTERNALLY;
} else if (ev == Event::FAILED) {
return sa.create_simple<FailedState>(ctx);
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
void JoinNetworkState::enter() {
ctx.log_info("Entered JoinNetwork state");
slac::messages::cm_set_key_req msg;
msg.key_type = slac::defs::CM_SET_KEY_REQ_KEY_TYPE_NMK;
msg.my_nonce = 0xAAAAAAAA;
msg.your_nonce = 0x00000000;
msg.pid = slac::defs::CM_SET_KEY_REQ_PID_HLE;
msg.prn = htole16(slac::defs::CM_SET_KEY_REQ_PRN_UNUSED);
msg.pmn = slac::defs::CM_SET_KEY_REQ_PMN_UNUSED;
msg.cco_capability = slac::defs::CM_SET_KEY_REQ_CCO_CAP_NONE;
slac::utils::generate_nid_from_nmk(msg.nid, nmk);
msg.new_eks = slac::defs::CM_SET_KEY_REQ_PEKS_NMK_KNOWN_TO_STA;
memcpy(msg.new_key, nmk, sizeof(msg.new_key));
ctx.send_slac_message(ctx.EV_PLC_MAC.data(), msg);
timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(SET_KEY_TIMEOUT_MS);
}
FSMSimpleState::CallbackReturnType JoinNetworkState::callback() {
const auto now = std::chrono::steady_clock::now();
const auto time_left = milliseconds_left(now, timeout);
if (time_left > 0) {
// still have time
return time_left;
}
// we reached the set key timeout
return Event::FAILED;
}
bool JoinNetworkState::check_for_valid_set_key_conf() {
const auto mmtype = ctx.slac_message.get_mmtype();
if (mmtype != (slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_CNF)) {
return false;
}
// correct message type
const auto& set_key_cnf = ctx.slac_message.get_payload<slac::messages::cm_set_key_cnf>();
// FIXME (aw): validation of the message?
return true;
}
FSMSimpleState::HandleEventReturnType MatchedState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else if (ev == Event::SLAC_MESSAGE) {
return sa.HANDLED_INTERNALLY;
} else {
return sa.PASS_ON;
}
}
FSMSimpleState::HandleEventReturnType FailedState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else if (ev == Event::SLAC_MESSAGE) {
return sa.HANDLED_INTERNALLY;
} else {
return sa.PASS_ON;
}
}
} // namespace slac::fsm::ev

View File

@@ -0,0 +1,154 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/slac/fsm/ev/states/sounding.hpp>
#include <algorithm>
#include <cstring>
#include <optional>
#include <random>
#include "timing_helper.hpp"
#include <everest/slac/fsm/ev/states/others.hpp>
namespace slac::fsm::ev {
SoundingState::SoundingState(Context& ctx, SessionParamaters session_parameters_) :
FSMSimpleState(ctx), session_parameters(std::move(session_parameters_)) {
}
void SoundingState::enter() {
sounding_timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(slac::defs::TT_EV_ATTEN_RESULTS_MS);
}
FSMSimpleState::CallbackReturnType SoundingState::callback() {
const auto now = std::chrono::steady_clock::now();
const auto sounding_time_left = milliseconds_left(now, sounding_timeout);
if (sounding_time_left <= 0) {
return Event::FAILED;
}
if (count_mnbc_sound_sent == 10) {
// we're already done, return time left until sounding timeout
return sounding_time_left;
}
if (count_start_atten_char_sent == 0) {
// no sounding messages have been send yet
next_timeout = now;
}
const auto next_step_time_left = milliseconds_left(now, next_timeout);
if (next_step_time_left > 0) {
// just idle a bit
return std::min(next_step_time_left, sounding_time_left);
}
// need to issue the next step
if (do_sounding()) {
// FIXME (aw): how to setup the next timeout, we could send the everything right away, but waiting at least a
// bit seems to be a good practice
const auto next_step_delay_ms = 20; // FIXME (aw): which value to use? TP_EV_BATCH_MSG_INTERVAL_MS
next_timeout = now + std::chrono::milliseconds(next_step_delay_ms);
return std::min(next_step_delay_ms, static_cast<int>(sounding_time_left));
}
return sounding_time_left;
}
FSMSimpleState::HandleEventReturnType SoundingState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
if (handle_valid_atten_char_ind()) {
return sa.create_simple<MatchRequestState>(ctx, session_parameters);
}
return sa.HANDLED_INTERNALLY;
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
}
return sa.PASS_ON;
}
bool SoundingState::do_sounding() {
if (count_start_atten_char_sent < slac::defs::C_EV_START_ATTEN_CHAR_INDS) {
slac::messages::cm_start_atten_char_ind msg;
msg.application_type = 0x0;
msg.security_type = 0x0;
msg.num_sounds = slac::defs::C_EV_MATCH_MNBC;
msg.timeout = (slac::defs::TT_EVSE_MATCH_MNBC_MS + 99) / 100; // in multiples of 100ms!
msg.resp_type = 0x01; // fixed value indicating 'other Green Phy station'
memcpy(msg.forwarding_sta, ctx.ev_host_mac.data(), sizeof(msg.forwarding_sta));
memcpy(msg.run_id, session_parameters.run_id, sizeof(msg.run_id));
ctx.send_slac_message(ctx.BROADCAST_MAC.data(), msg);
count_start_atten_char_sent++;
} else if (count_mnbc_sound_sent < slac::defs::C_EV_MATCH_MNBC) {
count_mnbc_sound_sent++;
slac::messages::cm_mnbc_sound_ind msg;
msg.application_type = 0x0;
msg.security_type = 0x0;
memset(msg.sender_id, 0, sizeof(msg.sender_id));
msg.remaining_sound_count = slac::defs::C_EV_MATCH_MNBC - count_mnbc_sound_sent;
memcpy(msg.run_id, session_parameters.run_id, sizeof(msg.run_id));
memset(msg._reserved, 0, sizeof(msg._reserved));
// FIXME (aw): does this have any performance penalties?
std::random_device rnd_dev;
std::mt19937 rng(rnd_dev());
std::uniform_int_distribution<std::mt19937::result_type> dist256(0, 255);
for (auto& random : msg.random) {
random = dist256(rng);
}
ctx.send_slac_message(ctx.BROADCAST_MAC.data(), msg);
}
return (count_mnbc_sound_sent < slac::defs::C_EV_MATCH_MNBC);
}
bool SoundingState::handle_valid_atten_char_ind() {
if (count_mnbc_sound_sent < slac::defs::C_EV_MATCH_MNBC) {
ctx.log_info("Received unexpected message while SOUNDING");
return false;
}
const auto mmtype = ctx.slac_message.get_mmtype();
if (mmtype != (slac::defs::MMTYPE_CM_ATTEN_CHAR | slac::defs::MMTYPE_MODE_IND)) {
ctx.log_info("Received unexpected message after SOUNDING");
return false;
}
// correct message type
const auto& atten_char = ctx.slac_message.get_payload<slac::messages::cm_atten_char_ind>();
const auto run_id_match =
(memcmp(session_parameters.run_id, atten_char.run_id, sizeof(session_parameters.run_id)) == 0);
if (run_id_match == false) {
return false;
}
// reply
slac::messages::cm_atten_char_rsp response;
response.application_type = 0x0;
response.security_type = 0x0;
memcpy(response.source_address, ctx.ev_host_mac.data(), sizeof(response.source_address));
memcpy(response.run_id, atten_char.run_id, sizeof(response.run_id));
memset(response.source_id, 0, sizeof(response.source_id));
memset(response.resp_id, 0, sizeof(response.resp_id));
response.result = 0x0;
ctx.send_slac_message(session_parameters.evse_mac, response);
return true;
}
} // namespace slac::fsm::ev

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EV_SLAC_STATES_TIMING_HELPER_HPP
#define EV_SLAC_STATES_TIMING_HELPER_HPP
#include <chrono>
template <typename TimepointType> auto milliseconds_left(const TimepointType& from, const TimepointType& to) {
return std::chrono::duration_cast<std::chrono::milliseconds>(to - from).count();
}
#endif // EV_SLAC_STATES_TIMING_HELPER_HPP

View File

@@ -0,0 +1,23 @@
add_library(slac_fsm_evse)
add_library(slac::fsm::evse ALIAS slac_fsm_evse)
ev_register_library_target(slac_fsm_evse)
target_include_directories(slac_fsm_evse
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_sources(slac_fsm_evse
PRIVATE
src/misc.cpp
src/context.cpp
src/states/others.cpp
src/states/matching.cpp
src/states/matching_handle_slac.cpp
)
target_link_libraries(slac_fsm_evse
PUBLIC
slac::slac
fsm::fsm
)

View File

@@ -0,0 +1,237 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_CONTEXT_HPP
#define EVSE_SLAC_CONTEXT_HPP
#include <functional>
#include <string>
#include <slac/slac.hpp>
namespace slac::fsm::evse {
namespace _context_detail {
template <typename SlacMessageType> struct MMTYPE;
template <> struct MMTYPE<slac::messages::cm_slac_parm_cnf> {
static const uint16_t value = slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_CNF;
};
template <> struct MMTYPE<slac::messages::cm_atten_char_ind> {
static const uint16_t value = slac::defs::MMTYPE_CM_ATTEN_CHAR | slac::defs::MMTYPE_MODE_IND;
};
template <> struct MMTYPE<slac::messages::cm_set_key_req> {
static const uint16_t value = slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::cm_validate_cnf> {
static const uint16_t value = slac::defs::MMTYPE_CM_VALIDATE | slac::defs::MMTYPE_MODE_CNF;
};
template <> struct MMTYPE<slac::messages::cm_slac_match_cnf> {
static const uint16_t value = slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_CNF;
};
template <> struct MMTYPE<slac::messages::qualcomm::cm_reset_device_req> {
static const uint16_t value = slac::defs::qualcomm::MMTYPE_CM_RESET_DEVICE | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::qualcomm::cm_reset_device_cnf> {
static const uint16_t value = slac::defs::qualcomm::MMTYPE_CM_RESET_DEVICE | slac::defs::MMTYPE_MODE_CNF;
};
template <> struct MMTYPE<slac::messages::qualcomm::link_status_req> {
static const uint16_t value = slac::defs::qualcomm::MMTYPE_LINK_STATUS | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::qualcomm::link_status_cnf> {
static const uint16_t value = slac::defs::qualcomm::MMTYPE_LINK_STATUS | slac::defs::MMTYPE_MODE_CNF;
};
template <> struct MMTYPE<slac::messages::qualcomm::op_attr_req> {
static const uint16_t value = slac::defs::qualcomm::MMTYPE_OP_ATTR | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::qualcomm::op_attr_cnf> {
static const uint16_t value = slac::defs::qualcomm::MMTYPE_OP_ATTR | slac::defs::MMTYPE_MODE_CNF;
};
// This message has no CNF counterpart
template <> struct MMTYPE<slac::messages::lumissil::nscm_reset_device_req> {
static const uint16_t value = slac::defs::lumissil::MMTYPE_NSCM_RESET_DEVICE | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::lumissil::nscm_get_version_req> {
static const uint16_t value = slac::defs::lumissil::MMTYPE_NSCM_GET_VERSION | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::lumissil::nscm_get_version_cnf> {
static const uint16_t value = slac::defs::lumissil::MMTYPE_NSCM_GET_VERSION | slac::defs::MMTYPE_MODE_CNF;
};
template <> struct MMTYPE<slac::messages::lumissil::nscm_get_d_link_status_req> {
static const uint16_t value = slac::defs::lumissil::MMTYPE_NSCM_GET_D_LINK_STATUS | slac::defs::MMTYPE_MODE_REQ;
};
template <> struct MMTYPE<slac::messages::lumissil::nscm_get_d_link_status_cnf> {
static const uint16_t value = slac::defs::lumissil::MMTYPE_NSCM_GET_D_LINK_STATUS | slac::defs::MMTYPE_MODE_CNF;
};
template <typename SlacMessageType> struct MMV {
// this is the default value for homeplug av 2.0 messages, which are
// backward compatible with homeplug av 1.1 messages
// non-backward (to 1.1) compatible message are CM_CHAN_EST,
// CM_AMP_MAP and CM_NW_STATS, these need to use AV_2_0
// older av 1.0 message need to use AV_1_0
static constexpr auto value = slac::defs::MMV::AV_1_1;
};
template <> struct MMV<slac::messages::qualcomm::cm_reset_device_req> {
static constexpr auto value = slac::defs::MMV::AV_1_0;
};
template <> struct MMV<slac::messages::qualcomm::cm_reset_device_cnf> {
static constexpr auto value = slac::defs::MMV::AV_1_0;
};
template <> struct MMV<slac::messages::qualcomm::link_status_req> {
static constexpr auto value = slac::defs::MMV::AV_1_0;
};
template <> struct MMV<slac::messages::qualcomm::link_status_cnf> {
static constexpr auto value = slac::defs::MMV::AV_1_0;
};
template <> struct MMV<slac::messages::qualcomm::op_attr_req> {
static constexpr auto value = slac::defs::MMV::AV_1_0;
};
template <> struct MMV<slac::messages::qualcomm::op_attr_cnf> {
static constexpr auto value = slac::defs::MMV::AV_1_0;
};
template <> struct MMV<slac::messages::lumissil::nscm_reset_device_req> {
static constexpr auto value = slac::defs::MMV::AV_1_0; // FIXME this is unclear
};
template <> struct MMV<slac::messages::lumissil::nscm_get_version_req> {
static constexpr auto value = slac::defs::MMV::AV_1_0; // FIXME this is unclear
};
template <> struct MMV<slac::messages::lumissil::nscm_get_version_cnf> {
static constexpr auto value = slac::defs::MMV::AV_1_0; // FIXME this is unclear
};
template <> struct MMV<slac::messages::lumissil::nscm_get_d_link_status_req> {
static constexpr auto value = slac::defs::MMV::AV_1_0; // FIXME this is unclear
};
template <> struct MMV<slac::messages::lumissil::nscm_get_d_link_status_cnf> {
static constexpr auto value = slac::defs::MMV::AV_1_0; // FIXME this is unclear
};
} // namespace _context_detail
// FIXME (aw): this should be moved to common headers (in libslac)
enum class ModemVendor {
Unknown,
Qualcomm,
Lumissil,
VertexCom,
};
struct ContextCallbacks {
std::function<void(slac::messages::HomeplugMessage&)> send_raw_slac{nullptr};
std::function<void(const std::string&)> signal_state{nullptr};
std::function<void(bool)> signal_dlink_ready{nullptr};
std::function<void()> signal_error_routine_request{nullptr};
std::function<void(const std::string&)> signal_ev_mac_address_parm_req{nullptr};
std::function<void(const std::string&)> signal_ev_mac_address_match_cnf{nullptr};
std::function<void(const std::string&)> log_debug{nullptr};
std::function<void(const std::string&)> log_info{nullptr};
std::function<void(const std::string&)> log_warn{nullptr};
std::function<void(const std::string&)> log_error{nullptr};
};
struct EvseSlacConfig {
// MAC address of our (EVSE) PLC modem
// FIXME (aw): is that used somehow?
uint8_t plc_peer_mac[ETH_ALEN] = {0x00, 0xB0, 0x52, 0x00, 0x00, 0x01};
// FIXME (aw): we probably want to use std::array here
void generate_nmk();
uint8_t session_nmk[slac::defs::NMK_LEN]{};
// flag for using 5% PWM in AC mode
bool ac_mode_five_percent{true};
// timeout for CM_SET_KEY.REQ
int set_key_timeout_ms = 500;
// timeout for CM_SLAC_PARM.REQ
int slac_init_timeout_ms = slac::defs::TT_EVSE_SLAC_INIT_MS;
// Settings CM_DEVICE_RESET.REQ
struct chip_reset_struct {
bool enabled = false;
int timeout_ms = 500;
int delay_ms = 100;
} chip_reset;
// Settings for LINK_STATUS detection
struct link_status_struct {
bool do_detect = false;
int retry_ms = 100;
int poll_in_matched_state_ms = 1000;
int timeout_ms = 5000;
bool debug_simulate_failed_matching = false;
} link_status;
int request_info_delay_ms = 100;
// offset for adjusting the calculated sounding attenuation
int sounding_atten_adjustment = 0;
bool reset_instead_of_fail{false};
bool regenerate_key_on_reset{true};
};
struct Context {
explicit Context(const ContextCallbacks& callbacks_) : callbacks(callbacks_){};
EvseSlacConfig slac_config{};
// event specific payloads
// FIXME (aw): due to the synchroneous nature of the fsm, this could be even a ptr/ref
slac::messages::HomeplugMessage slac_message_payload;
// FIXME (aw): message should be const, but libslac doesn't allow for const ptr - needs changes in libslac
template <typename SlacMessageType> void send_slac_message(const uint8_t* mac, SlacMessageType const& message) {
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac);
hp_message.setup_payload(&message, sizeof(message), _context_detail::MMTYPE<SlacMessageType>::value,
_context_detail::MMV<SlacMessageType>::value);
callbacks.send_raw_slac(hp_message);
}
// signal handlers
void signal_cm_slac_parm_req(const uint8_t* ev_mac);
void signal_cm_slac_match_cnf(const uint8_t* ev_mac);
void signal_dlink_ready(bool value);
void signal_error_routine_request();
void signal_state(const std::string& state);
// logging util
void log_debug(const std::string& text);
void log_info(const std::string& text);
void log_warn(const std::string& text);
void log_error(const std::string& text);
ModemVendor modem_vendor{ModemVendor::Unknown};
uint8_t evse_mac[ETH_ALEN];
private:
const ContextCallbacks& callbacks;
};
} // namespace slac::fsm::evse
#endif // EVSE_SLAC_CONTEXT_HPP

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_FSM_HPP
#define EVSE_SLAC_FSM_HPP
#include <fsm/fsm.hpp>
#include "context.hpp"
namespace slac::fsm::evse {
enum class Event {
RESET,
ENTER_BCD,
LEAVE_BCD,
SLAC_MESSAGE,
// internal events
RETRY_MATCHING,
MATCH_COMPLETE,
FAILED,
SUCCESS,
};
using FSMReturnType = int;
using FSM = ::fsm::FSM<Event, FSMReturnType>;
using FSMSimpleState = ::fsm::states::StateWithContext<FSM::SimpleStateType, Context>;
using FSMCompoundState = ::fsm::states::StateWithContext<FSM::CompoundStateType, Context>;
} // namespace slac::fsm::evse
#endif // EVSE_SLAC_FSM_HPP

View File

@@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_STATES_MATCHING_HPP
#define EVSE_SLAC_STATES_MATCHING_HPP
#include <chrono>
#include <memory>
#include <slac/slac.hpp>
#include "../fsm.hpp"
namespace slac::fsm::evse {
enum class MatchingSubState {
WAIT_FOR_START_ATTEN_CHAR,
SOUNDING,
FINALIZE_SOUNDING,
WAIT_FOR_ATTEN_CHAR_RSP,
WAIT_FOR_SLAC_MATCH,
RECEIVED_SLAC_MATCH,
MATCH_COMPLETE,
FAILED,
};
constexpr auto FINALIZE_SOUNDING_DELAY_MS = 45;
using MatchingTimepoint = std::chrono::time_point<std::chrono::steady_clock>;
struct MatchingSession {
MatchingSession(const uint8_t* ev_mac, const uint8_t* run_id);
// common session related
MatchingSubState state{MatchingSubState::WAIT_FOR_START_ATTEN_CHAR};
uint8_t ev_mac[ETH_ALEN];
uint8_t run_id[slac::defs::RUN_ID_LEN];
// timeout related
MatchingTimepoint next_timeout;
bool timeout_active{false};
// sounding related
int captured_sounds{0};
int captured_aags[slac::defs::AAG_LIST_LEN];
bool received_mnbc_sound{false};
int num_retries{0};
// helper functions
void set_next_timeout(int delay_ms);
void ack_timeout();
bool is_identified_by(const uint8_t* ev_mac, const uint8_t* run_id) const;
slac::messages::cm_atten_char_ind calculate_avg() const;
};
struct MatchingState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
std::vector<MatchingSession> sessions;
// FIXME (aw): this should be const ref, but some of the member functions of HomeplugMessage are not const'd
void handle_slac_message(slac::messages::HomeplugMessage&);
void handle_cm_slac_parm_req(const slac::messages::cm_slac_parm_req&);
void handle_cm_start_atten_char_ind(const slac::messages::cm_start_atten_char_ind&);
void handle_cm_mnbc_sound_ind(const slac::messages::cm_mnbc_sound_ind&);
void handle_cm_atten_profile_ind(const slac::messages::cm_atten_profile_ind&);
void handle_cm_atten_char_rsp(const slac::messages::cm_atten_char_rsp&);
void handle_cm_validate_req(const slac::messages::cm_validate_req&);
void handle_cm_slac_match_req(const slac::messages::cm_slac_match_req&);
void finalize_sounding(MatchingSession& session);
// FIXME (aw): this should be wrapped somewhere else
const uint8_t* tmp_ev_mac;
MatchingTimepoint timeout_slac_parm_req;
bool seen_slac_parm_req{false};
int num_retries{0};
std::unique_ptr<slac::messages::cm_slac_match_cnf> match_cnf_message;
int failed_count{0};
};
} // namespace slac::fsm::evse
#endif // EVSE_SLAC_STATES_MATCHING_HPP

View File

@@ -0,0 +1,112 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_STATES_OTHERS_HPP
#define EVSE_SLAC_STATES_OTHERS_HPP
#include <chrono>
#include <memory>
#include "../fsm.hpp"
namespace slac::fsm::evse {
struct ResetState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// for now returns true if CM_SET_KEY_CNF is received
bool handle_slac_message(slac::messages::HomeplugMessage&);
bool setup_has_been_send{false};
};
struct ResetChipState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// for now returns true if CM_RESET_CNF is received
bool handle_slac_message(slac::messages::HomeplugMessage&);
bool reset_delay_done{false};
bool chip_reset_has_been_sent{false};
enum class SubState {
DELAY,
SEND_RESET,
DONE,
} sub_state{SubState::DELAY};
};
struct IdleState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
};
struct MatchedState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
void leave() final;
CallbackReturnType callback() final;
bool link_status_req_sent{false};
};
struct FailedState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
};
struct WaitForLinkState : public FSMSimpleState {
WaitForLinkState(Context& ctx, std::unique_ptr<slac::messages::cm_slac_match_cnf> sent_match_cnf_message);
HandleEventReturnType handle_event(AllocatorType&, Event) final;
void enter() final;
CallbackReturnType callback() final;
// for now returns true if link up detected is received
bool handle_slac_message(slac::messages::HomeplugMessage&);
bool link_status_req_sent{false};
std::chrono::steady_clock::time_point start_time;
std::unique_ptr<slac::messages::cm_slac_match_cnf> match_cnf_message;
};
struct InitState : public FSMSimpleState {
using FSMSimpleState::FSMSimpleState;
HandleEventReturnType handle_event(AllocatorType&, Event) final;
CallbackReturnType callback() final;
void handle_slac_message(slac::messages::HomeplugMessage&);
// For now we are requesting only one version info packet, but probably there will be more in the future.
enum class SubState {
QUALCOMM_OP_ATTR,
LUMISSIL_GET_VERSION,
DONE,
} sub_state{SubState::QUALCOMM_OP_ATTR};
};
} // namespace slac::fsm::evse
#endif // EVSE_SLAC_STATES_OTHERS_HPP

View File

@@ -0,0 +1,79 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/slac/fsm/evse/context.hpp>
#include <random>
#include "misc.hpp"
namespace slac::fsm::evse {
void EvseSlacConfig::generate_nmk() {
const std::string CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::random_device random_device;
std::mt19937 generator(random_device());
std::uniform_int_distribution<> distribution(0, CHARACTERS.size() - 1);
for (std::size_t i = 0; i < slac::defs::NMK_LEN; ++i) {
session_nmk[i] = (uint8_t)CHARACTERS[distribution(generator)];
}
}
void Context::signal_cm_slac_parm_req(const uint8_t* mac) {
if (callbacks.signal_ev_mac_address_parm_req) {
const auto mac_string = format_mac_addr(mac);
callbacks.signal_ev_mac_address_parm_req(mac_string);
}
}
void Context::signal_cm_slac_match_cnf(const uint8_t* mac) {
if (callbacks.signal_ev_mac_address_match_cnf) {
const auto mac_string = format_mac_addr(mac);
callbacks.signal_ev_mac_address_match_cnf(mac_string);
}
}
void Context::signal_dlink_ready(bool value) {
if (callbacks.signal_dlink_ready) {
callbacks.signal_dlink_ready(value);
}
}
void Context::signal_error_routine_request() {
if (callbacks.signal_error_routine_request) {
callbacks.signal_error_routine_request();
}
}
void Context::signal_state(const std::string& state) {
if (callbacks.signal_state) {
callbacks.signal_state(state);
}
}
void Context::log_debug(const std::string& text) {
if (callbacks.log_debug) {
callbacks.log_debug(text);
}
}
void Context::log_info(const std::string& text) {
if (callbacks.log_info) {
callbacks.log_info(text);
}
}
void Context::log_warn(const std::string& text) {
if (callbacks.log_warn) {
callbacks.log_warn(text);
}
}
void Context::log_error(const std::string& text) {
if (callbacks.log_error) {
callbacks.log_error(text);
}
}
} // namespace slac::fsm::evse

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include "misc.hpp"
#include <net/ethernet.h>
#include <slac/slac.hpp>
std::string format_mac_addr(const uint8_t* mac) {
char string_buffer[ETH_ALEN * 2 + (ETH_ALEN - 1) + 1];
snprintf(string_buffer, sizeof(string_buffer), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
return string_buffer;
}
std::string format_nmk(const uint8_t* nmk) {
char string_buffer[slac::defs::NMK_LEN * 3];
for (int i = 0; i < slac::defs::NMK_LEN; i++) {
snprintf(string_buffer + (i * 3), sizeof(string_buffer) - (i * 3), "%02X:", nmk[i]);
}
return string_buffer;
}
std::string format_run_id(const uint8_t* run_id) {
char string_buffer[2 * slac::defs::RUN_ID_LEN + 1];
snprintf(string_buffer, sizeof(string_buffer), "%02X%02X%02X%02X%02X%02X%02X%02X", run_id[0], run_id[1], run_id[2],
run_id[3], run_id[4], run_id[5], run_id[6], run_id[7]);
return string_buffer;
}
std::string format_mmtype(const uint16_t mmtype) {
char string_buffer[2 + 2 * 2 + 1];
snprintf(string_buffer, sizeof(string_buffer), "0x%04hX", mmtype);
return string_buffer;
}

View File

@@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_MISC_HPP
#define EVSE_SLAC_MISC_HPP
#include <cstdint>
#include <string>
std::string format_nmk(const uint8_t* nmk);
std::string format_mac_addr(const uint8_t* mac);
std::string format_run_id(const uint8_t* run_id);
std::string format_mmtype(const uint16_t mmtype);
#endif // EVSE_SLAC_MISC_HPP

View File

@@ -0,0 +1,187 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/slac/fsm/evse/states/matching.hpp>
#include <cstring>
#include <optional>
#include "matching_handle_slac.hpp"
#include <everest/slac/fsm/evse/states/others.hpp>
namespace slac::fsm::evse {
//
// Helper functions
//
static inline auto remaining_milliseconds(const MatchingTimepoint& timeout, const MatchingTimepoint& now) {
return std::chrono::duration_cast<std::chrono::milliseconds>(timeout - now).count();
}
MatchingSession::MatchingSession(const uint8_t* ev_mac, const uint8_t* run_id) {
memcpy(this->ev_mac, ev_mac, sizeof(this->ev_mac));
memcpy(this->run_id, run_id, sizeof(this->run_id));
memset(captured_aags, 0, sizeof(captured_aags));
}
void MatchingSession::set_next_timeout(int delay_ms) {
next_timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(delay_ms);
timeout_active = true;
}
void MatchingSession::ack_timeout() {
timeout_active = false;
}
bool MatchingSession::is_identified_by(const uint8_t* ev_mac, const uint8_t* run_id) const {
if (0 != memcmp(run_id, this->run_id, sizeof(this->run_id))) {
return false;
}
if (0 != memcmp(ev_mac, this->ev_mac, sizeof(this->ev_mac))) {
return false;
}
return true;
}
bool all_sessions_failed(const std::vector<MatchingSession>& sessions) {
for (const auto& session : sessions) {
if (session.state != MatchingSubState::FAILED) {
return false;
}
}
return true;
}
//
// Matching state related
//
void MatchingState::enter() {
ctx.signal_state("MATCHING");
ctx.log_info("Entered Matching state, waiting for CM_SLAC_PARM_REQ");
// timeout for getting CM_SLAC_PARM_REQ
timeout_slac_parm_req =
std::chrono::steady_clock::now() + std::chrono::milliseconds(ctx.slac_config.slac_init_timeout_ms);
}
FSMSimpleState::CallbackReturnType MatchingState::callback() {
// check timeouts
auto now_tp = std::chrono::steady_clock::now();
std::optional<FSMReturnType> call_back_ms;
if (!seen_slac_parm_req) {
if (now_tp >= timeout_slac_parm_req) {
ctx.log_error("CM_SLAC_PARM_REQ timed out -> FAILED");
return Event::FAILED;
}
call_back_ms = remaining_milliseconds(timeout_slac_parm_req, now_tp);
}
// fallthrough: CM_SLAC_PARM_REQ has been seen, check individual sessions
for (auto& session : sessions) {
// there should always be an active timeout, right?
// FIXME (aw)
while (session.timeout_active) {
// FIXME (aw): this way we only take the first one
if (session.state == MatchingSubState::MATCH_COMPLETE) {
return Event::MATCH_COMPLETE;
}
auto remaining_ms = remaining_milliseconds(session.next_timeout, now_tp);
if (remaining_ms > 0) {
if (call_back_ms.has_value() == false || *call_back_ms > remaining_ms) {
call_back_ms = remaining_ms;
}
break;
}
// fall-through, timeout should be handled now
session.ack_timeout();
if (session.state == MatchingSubState::WAIT_FOR_START_ATTEN_CHAR) {
session_log(ctx, session, LogLevel::ERROR, "Waiting for CM_START_ATTEN_CHAR_IND timed out -> failed");
session.state = MatchingSubState::FAILED;
} else if (session.state == MatchingSubState::SOUNDING) {
session_log(ctx, session, LogLevel::WARN,
"Sounding not yet complete but timed out, going to sub-state FINALIZE_SOUNDING");
session.state = MatchingSubState::FINALIZE_SOUNDING;
session.set_next_timeout(FINALIZE_SOUNDING_DELAY_MS);
} else if (session.state == MatchingSubState::FINALIZE_SOUNDING) {
finalize_sounding(session);
session.num_retries = 0;
} else if (session.state == MatchingSubState::WAIT_FOR_ATTEN_CHAR_RSP) {
session.num_retries++;
if (session.num_retries <= slac::defs::C_EV_MATCH_RETRY) {
session_log(ctx, session, LogLevel::WARN,
"Waiting for CM_ATTEN_CHAR_RSP timed out -> retry matching");
finalize_sounding(session);
} else {
session_log(ctx, session, LogLevel::ERROR, "Waiting for CM_ATTEN_CHAR_RSP timed out -> failed");
session.state = MatchingSubState::FAILED;
}
} else if (session.state == MatchingSubState::WAIT_FOR_SLAC_MATCH) {
session_log(ctx, session, LogLevel::ERROR, "Wating for CM_SLAC_MATCH_REQ timed out -> failed");
session.state = MatchingSubState::FAILED;
}
}
if (all_sessions_failed(sessions)) {
return Event::FAILED;
}
}
if (call_back_ms.has_value() == false) {
// FIXME (aw): this should not happen, should we assert here or something similar?
}
return *call_back_ms;
}
FSMSimpleState::HandleEventReturnType MatchingState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
handle_slac_message(ctx.slac_message_payload);
return sa.HANDLED_INTERNALLY;
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else if (ev == Event::MATCH_COMPLETE) {
// Wait for link up to be confirmed before going to MATCHED state if enabled in config
if (ctx.slac_config.link_status.do_detect) {
return sa.create_simple<WaitForLinkState>(ctx, std::move(match_cnf_message));
} else {
return sa.create_simple<MatchedState>(ctx);
}
} else if (ev == Event::RETRY_MATCHING) {
num_retries++;
if (num_retries == slac::defs::C_EV_MATCH_RETRY) {
ctx.log_error("Reached retry limit for matching");
return sa.create_simple<FailedState>(ctx);
}
// otherwise, reset timeout
timeout_slac_parm_req =
std::chrono::steady_clock::now() + std::chrono::milliseconds(ctx.slac_config.slac_init_timeout_ms);
return sa.HANDLED_INTERNALLY;
} else if (ev == Event::FAILED) {
failed_count++;
if (ctx.slac_config.reset_instead_of_fail and failed_count < 2) {
ctx.log_error("Resetting MatchingState. Waiting for the next CM_SLAC_PARAM.REQ message.");
// Resetting all relevant MatchingState members
sessions.clear();
// timeout for getting CM_SLAC_PARM_REQ
timeout_slac_parm_req =
std::chrono::steady_clock::now() + std::chrono::milliseconds(ctx.slac_config.slac_init_timeout_ms);
seen_slac_parm_req = false;
num_retries = 0;
return sa.HANDLED_INTERNALLY;
}
return sa.create_simple<FailedState>(ctx);
}
return sa.PASS_ON;
}
} // namespace slac::fsm::evse

View File

@@ -0,0 +1,482 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include "matching_handle_slac.hpp"
#include <cstring>
#include <iomanip>
#include <sstream>
#include "../misc.hpp"
namespace slac::fsm::evse {
void session_log(Context& ctx, MatchingSession& session, const LogLevel level, const std::string& text) {
const auto run_id = format_run_id(session.run_id);
const auto mac = format_mac_addr(session.ev_mac);
std::stringstream ss;
ss << "Session (run_id=" << run_id << ", ev_mac=" << mac << "): " << text;
switch (level) {
case LogLevel::DEBUG:
ctx.log_debug(ss.str());
break;
case LogLevel::INFO:
ctx.log_info(ss.str());
break;
case LogLevel::WARN:
ctx.log_warn(ss.str());
break;
case LogLevel::ERROR:
ctx.log_error(ss.str());
break;
}
}
//
// MatchingSession related
//
static MatchingSession* find_session(std::vector<MatchingSession>& sessions, const uint8_t* ev_mac,
const uint8_t* run_id) {
for (auto& session : sessions) {
if (session.is_identified_by(ev_mac, run_id)) {
return &session;
}
}
return nullptr;
}
static auto create_cm_slac_parm_cnf(const MatchingSession& session) {
slac::messages::cm_slac_parm_cnf param_confirm;
memcpy(param_confirm.m_sound_target, slac::defs::BROADCAST_MAC_ADDRESS, sizeof(slac::defs::BROADCAST_MAC_ADDRESS));
param_confirm.num_sounds = slac::defs::CM_SLAC_PARM_CNF_NUM_SOUNDS;
param_confirm.timeout = slac::defs::CM_SLAC_PARM_CNF_TIMEOUT;
param_confirm.resp_type = slac::defs::CM_SLAC_PARM_CNF_RESP_TYPE;
memcpy(param_confirm.forwarding_sta, session.ev_mac, sizeof(param_confirm.forwarding_sta));
param_confirm.application_type = slac::defs::COMMON_APPLICATION_TYPE;
param_confirm.security_type = slac::defs::COMMON_SECURITY_TYPE;
memcpy(param_confirm.run_id, session.run_id, sizeof(param_confirm.run_id));
return param_confirm;
}
static auto create_cm_atten_char_ind(const MatchingSession& session, int atten_offset = 0) {
slac::messages::cm_atten_char_ind atten_char_ind;
atten_char_ind.application_type = slac::defs::COMMON_APPLICATION_TYPE;
atten_char_ind.security_type = slac::defs::COMMON_SECURITY_TYPE;
memcpy(atten_char_ind.source_address, session.ev_mac, sizeof(atten_char_ind.source_address));
memcpy(atten_char_ind.run_id, session.run_id, sizeof(atten_char_ind.run_id));
// memcpy(atten_char_ind.source_id, session_ev_id, sizeof(atten_char_ind.source_id));
memset(atten_char_ind.source_id, 0, sizeof(atten_char_ind.source_id));
// memcpy(atten_char_ind.resp_id, sample_evse_vin, sizeof(atten_char_ind.resp_id));
memset(atten_char_ind.resp_id, 0, sizeof(atten_char_ind.resp_id));
atten_char_ind.num_sounds = session.captured_sounds;
atten_char_ind.attenuation_profile.num_groups = slac::defs::AAG_LIST_LEN;
if (session.captured_sounds != 0) {
for (int i = 0; i < slac::defs::AAG_LIST_LEN; ++i) {
atten_char_ind.attenuation_profile.aag[i] =
session.captured_aags[i] / session.captured_sounds + atten_offset;
}
} else {
// FIXME (aw): what to do here, if we didn't receive any sounds?
memset(atten_char_ind.attenuation_profile.aag, 0x01, sizeof(atten_char_ind.attenuation_profile.aag));
}
return atten_char_ind;
}
// Note (aw): this function doesn't return by value in order to optimize for fewer copies
static void create_cm_slac_match_cnf(slac::messages::cm_slac_match_cnf& match_cnf, const MatchingSession& session,
const slac::messages::cm_slac_match_req& match_req, const uint8_t* session_nmk) {
match_cnf.application_type = slac::defs::COMMON_APPLICATION_TYPE;
match_cnf.security_type = slac::defs::COMMON_SECURITY_TYPE;
match_cnf.mvf_length = htole16(slac::defs::CM_SLAC_MATCH_CNF_MVF_LENGTH);
memcpy(match_cnf.pev_id, match_req.pev_id, sizeof(match_cnf.pev_id));
memcpy(match_cnf.pev_mac, match_req.pev_mac, sizeof(match_cnf.pev_mac));
memcpy(match_cnf.evse_id, match_req.evse_id, sizeof(match_cnf.evse_id));
memcpy(match_cnf.evse_mac, match_req.evse_mac, sizeof(match_cnf.evse_mac));
memcpy(match_cnf.run_id, match_req.run_id, sizeof(match_cnf.run_id));
memset(match_cnf._rerserved, 0, 8);
match_cnf._reserved2 = 0;
slac::utils::generate_nid_from_nmk(match_cnf.nid, session_nmk);
memcpy(match_cnf.nmk, session_nmk, sizeof(match_cnf.nmk));
}
void MatchingState::handle_slac_message(slac::messages::HomeplugMessage& msg) {
const auto mmtype = msg.get_mmtype();
tmp_ev_mac = msg.get_src_mac();
switch (mmtype) {
case (slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_REQ):
handle_cm_slac_parm_req(msg.get_payload<slac::messages::cm_slac_parm_req>());
break;
case (slac::defs::MMTYPE_CM_START_ATTEN_CHAR | slac::defs::MMTYPE_MODE_IND):
handle_cm_start_atten_char_ind(msg.get_payload<slac::messages::cm_start_atten_char_ind>());
break;
case (slac::defs::MMTYPE_CM_MNBC_SOUND | slac::defs::MMTYPE_MODE_IND):
handle_cm_mnbc_sound_ind(msg.get_payload<slac::messages::cm_mnbc_sound_ind>());
break;
case (slac::defs::MMTYPE_CM_ATTEN_PROFILE | slac::defs::MMTYPE_MODE_IND):
handle_cm_atten_profile_ind(msg.get_payload<slac::messages::cm_atten_profile_ind>());
break;
case (slac::defs::MMTYPE_CM_ATTEN_CHAR | slac::defs::MMTYPE_MODE_RSP):
handle_cm_atten_char_rsp(msg.get_payload<slac::messages::cm_atten_char_rsp>());
break;
case (slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_REQ):
handle_cm_slac_match_req(msg.get_payload<slac::messages::cm_slac_match_req>());
break;
case (slac::defs::MMTYPE_CM_VALIDATE | slac::defs::MMTYPE_MODE_REQ):
handle_cm_validate_req(msg.get_payload<slac::messages::cm_validate_req>());
break;
default:
ctx.log_warn("Received non-expected SLAC message of type " + format_mmtype(mmtype));
}
}
static bool validate_cm_slac_parm_req(const slac::messages::cm_slac_parm_req& msg) {
if (msg.application_type not_eq slac::defs::COMMON_APPLICATION_TYPE) {
return false;
}
if (msg.security_type not_eq slac::defs::COMMON_SECURITY_TYPE) {
return false;
}
return true;
}
void MatchingState::handle_cm_slac_parm_req(const slac::messages::cm_slac_parm_req& msg) {
if (not validate_cm_slac_parm_req(msg)) {
ctx.log_warn("Invalid CM_SLAC_PARM.REQ received, ignoring");
return;
}
// set this flag to true, to disable the retry timeout
seen_slac_parm_req = true;
auto session = find_session(sessions, tmp_ev_mac, msg.run_id);
if (session) {
// the matching session existed already, according to [V2G3-A09-16] we should restart
*session = MatchingSession(tmp_ev_mac, msg.run_id);
} else {
// the session didn't exist, lets create it
sessions.emplace_back(MatchingSession{tmp_ev_mac, msg.run_id});
session = &sessions.back();
}
session_log(ctx, *session, LogLevel::INFO, "initialized, waiting for CM_START_ATTEN_CHAR_IND");
// timeout until we need to get cm_start_atten_char_ind
session->set_next_timeout(slac::defs::TT_MATCH_SEQUENCE_MS);
auto param_confirm = create_cm_slac_parm_cnf(*session);
ctx.send_slac_message(param_confirm.forwarding_sta, param_confirm);
ctx.signal_cm_slac_parm_req(tmp_ev_mac);
}
static bool validate_cm_start_atten_char_ind(const slac::messages::cm_start_atten_char_ind& msg) {
if (msg.application_type not_eq slac::defs::COMMON_APPLICATION_TYPE) {
return false;
}
if (msg.security_type not_eq slac::defs::COMMON_SECURITY_TYPE) {
return false;
}
if (msg.num_sounds == 0) { // Don't be strict to the ISO 15118-3
return false;
}
if (msg.timeout == 0) { // Don't be strict to the ISO 15118-3
return false;
}
if (msg.resp_type not_eq slac::defs::CM_SLAC_PARM_CNF_RESP_TYPE) {
return false;
}
return true;
}
void MatchingState::handle_cm_start_atten_char_ind(const slac::messages::cm_start_atten_char_ind& msg) {
if (not validate_cm_start_atten_char_ind(msg)) {
ctx.log_warn("Invalid CM_START_ATTEN_CHAR_IND received, ignoring");
return;
}
auto session = find_session(sessions, tmp_ev_mac, msg.run_id);
if (!session) {
ctx.log_warn("No session found for CM_START_ATTEN_CHAR_IND");
return;
}
if (session->state != MatchingSubState::WAIT_FOR_START_ATTEN_CHAR) {
if (session->state != MatchingSubState::SOUNDING)
session_log(ctx, *session, LogLevel::WARN,
"needs to be in state WAIT_FOR_START_ATTEN_CHAR for CM_START_ATTEN_CHAR_IND");
return;
}
// go to sounding
session_log(ctx, *session, LogLevel::INFO, "received CM_START_ATTEN_CHAR_IND, going to substate SOUNDING");
session->state = MatchingSubState::SOUNDING;
session->set_next_timeout(slac::defs::TT_EVSE_MATCH_MNBC_MS);
}
static bool validate_cm_mnbc_sound_ind(const slac::messages::cm_mnbc_sound_ind& msg) {
if (msg.application_type not_eq slac::defs::COMMON_APPLICATION_TYPE) {
return false;
}
if (msg.security_type not_eq slac::defs::COMMON_SECURITY_TYPE) {
return false;
}
return true;
}
void MatchingState::handle_cm_mnbc_sound_ind(const slac::messages::cm_mnbc_sound_ind& msg) {
if (not validate_cm_mnbc_sound_ind(msg)) {
ctx.log_warn("Invalid CM_MNBC_SOUND_IND received, ignoring");
return;
}
auto session = find_session(sessions, tmp_ev_mac, msg.run_id);
if (!session) {
ctx.log_warn("No session found for CM_MNBC_SOUND_IND");
return;
}
if (session->state != MatchingSubState::SOUNDING) {
session_log(ctx, *session, LogLevel::WARN, "needs to be in state SOUNDING for CM_MNBC_SOUND_IND");
return;
}
session_log(ctx, *session, LogLevel::INFO, "received CM_MNBC_SOUND_IND");
session->received_mnbc_sound = true;
}
void MatchingState::handle_cm_atten_profile_ind(const slac::messages::cm_atten_profile_ind& msg) {
// cm_atten_profile_ind does not carry a run_id, so we can't exactly identify the session
// FIXME (aw): for now, we only take the first one found
MatchingSession* session = nullptr;
for (auto& session_i : sessions) {
if (memcmp(msg.pev_mac, session_i.ev_mac, sizeof(msg.pev_mac)) == 0) {
session = &session_i;
}
}
if (!session) {
ctx.log_warn("No session found for CM_ATTEN_PROFILE_IND");
return;
}
if (session->state != MatchingSubState::SOUNDING) {
session_log(ctx, *session, LogLevel::WARN, "needs to be in state SOUNDING for CM_ATTEN_PROFILE_IND");
return;
}
if (msg.num_groups != slac::defs::AAG_LIST_LEN) {
session_log(ctx, *session, LogLevel::WARN, "mismatch in number of AAG groups");
return;
}
session_log(ctx, *session, LogLevel::INFO, "received CM_ATTEN_PROFILE_IND");
for (int i = 0; i < slac::defs::AAG_LIST_LEN; ++i) {
session->captured_aags[i] += msg.aag[i];
}
session->captured_sounds++;
if (session->captured_sounds < slac::defs::CM_SLAC_PARM_CNF_NUM_SOUNDS) {
return;
}
// fall-through: all sounds captured
session_log(ctx, *session, LogLevel::INFO, "received all sounds, going to substate FINALIZE_SOUNDING");
session->state = MatchingSubState::FINALIZE_SOUNDING;
session->set_next_timeout(FINALIZE_SOUNDING_DELAY_MS);
}
static bool validate_cm_atten_char_rsp(const slac::messages::cm_atten_char_rsp& msg, const MatchingSession& session) {
if (msg.application_type not_eq slac::defs::COMMON_APPLICATION_TYPE) {
return false;
}
if (msg.security_type not_eq slac::defs::COMMON_SECURITY_TYPE) {
return false;
}
if (memcmp(msg.source_address, session.ev_mac, ETH_ALEN)) {
return false;
}
uint8_t source_id_ref[slac::messages::SOURCE_ID_LEN];
memset(source_id_ref, 0, sizeof(source_id_ref));
if (memcmp(source_id_ref, msg.source_id, slac::messages::SOURCE_ID_LEN)) {
return false;
}
uint8_t resp_id_ref[slac::messages::RESP_ID_LEN];
memset(resp_id_ref, 0, sizeof(resp_id_ref));
if (memcmp(resp_id_ref, msg.resp_id, slac::messages::RESP_ID_LEN)) {
return false;
}
if (msg.result not_eq slac::defs::CM_ATTEN_CHAR_RSP_RESULT) {
return false;
}
return true;
}
void MatchingState::handle_cm_atten_char_rsp(const slac::messages::cm_atten_char_rsp& msg) {
auto session = find_session(sessions, tmp_ev_mac, msg.run_id);
if (!session) {
ctx.log_warn("No session found for CM_ATTEN_CHAR_RSP");
return;
}
if (not validate_cm_atten_char_rsp(msg, *session)) {
session_log(ctx, *session, LogLevel::WARN, "Invalid CM_ATTEN_CHAR_RSP received, ignoring");
return;
}
if (session->state != MatchingSubState::WAIT_FOR_ATTEN_CHAR_RSP) {
session_log(ctx, *session, LogLevel::WARN,
"needs to be in state WAIT_FOR_ATTEN_CHAR_RSP for CM_ATTEN_CHAR_RSP");
return;
}
session_log(ctx, *session, LogLevel::INFO, "received CM_ATTEN_CHAR_RSP, going to substate WAIT_FOR_SLAC_MATCH");
session->state = MatchingSubState::WAIT_FOR_SLAC_MATCH;
// FIXME (aw): referring to the standard, it is not clear here, if we should offset from TT_EVSE_MATCH_MNBC
session->set_next_timeout(slac::defs::TT_EVSE_MATCH_SESSION_MS);
}
void MatchingState::handle_cm_validate_req(const slac::messages::cm_validate_req& msg) {
// NOTE: CM_VALIDATE.REQ does not specify its session
// EVSE allowed to not implement: [V2G3-A09-51]
ctx.log_warn("Received CM_VALIDATE.REQ / not implemented - will return failure code");
slac::messages::cm_validate_cnf validate_cnf;
validate_cnf.signal_type = slac::defs::CM_VALIDATE_REQ_SIGNAL_TYPE;
validate_cnf.toggle_num = 0;
validate_cnf.result = slac::defs::CM_VALIDATE_REQ_RESULT_FAILURE;
ctx.send_slac_message(tmp_ev_mac, validate_cnf);
}
static bool validate_cm_slac_match_req(const slac::messages::cm_slac_match_req& msg, const MatchingSession& session,
const Context& ctx) {
if (msg.application_type not_eq slac::defs::COMMON_APPLICATION_TYPE) {
return false;
}
if (msg.security_type not_eq slac::defs::COMMON_SECURITY_TYPE) {
return false;
}
if (msg.mvf_length not_eq slac::defs::CM_SLAC_MATCH_REQ_MVF_LENGTH) {
return false;
}
// PEV ID = 0x00 TC_SECC_CMN_VTB_CmSlacMatch_013/014(?)
uint8_t pev_id_ref[slac::messages::PEV_ID_LEN];
memset(pev_id_ref, 0, sizeof(pev_id_ref));
if (memcmp(pev_id_ref, msg.pev_id, slac::messages::PEV_ID_LEN)) {
return false;
}
// PEV MAC TC_SECC_CMN_VTB_CmSlacMatch_015/016(?)
if (memcmp(msg.pev_mac, session.ev_mac, ETH_ALEN)) {
return false;
}
// EVSE ID = 0x00 TC_SECC_CMN_VTB_CmSlacMatch_017/018(?)
uint8_t evse_id_ref[slac::messages::EVSE_ID_LEN];
memset(evse_id_ref, 0, sizeof(evse_id_ref));
if (memcmp(evse_id_ref, msg.evse_id, slac::messages::EVSE_ID_LEN)) {
return false;
}
// EVSE MAC TC_SECC_CMN_VTB_CmSlacMatch_019/020
if (memcmp(ctx.evse_mac, msg.evse_mac, ETH_ALEN)) {
return false;
}
// RunID TC_SECC_CMN_VTB_CmSlacMatch_021/022
if (memcmp(msg.run_id, session.run_id, slac::defs::RUN_ID_LEN)) {
return false;
}
return true;
}
void MatchingState::handle_cm_slac_match_req(const slac::messages::cm_slac_match_req& msg) {
auto session = find_session(sessions, tmp_ev_mac, msg.run_id);
if (!session) {
ctx.log_warn("No session found for CM_SLAC_MATCH_REQ");
return;
}
if (not validate_cm_slac_match_req(msg, *session, ctx)) {
session_log(ctx, *session, LogLevel::WARN, "Invalid CM_SLAC_MATCH_REQ received, ignoring");
return;
}
if (session->state != MatchingSubState::WAIT_FOR_SLAC_MATCH && session->state != MatchingSubState::MATCH_COMPLETE) {
session_log(ctx, *session, LogLevel::WARN,
"needs to be in state WAIT_FOR_SLAC_MATCH or MATCH_COMPLETE for CM_SLAC_MATCH_REQ");
return;
}
session_log(ctx, *session, LogLevel::INFO,
"Received CM_SLAC_MATCH_REQ, sending CM_SLAC_MATCH_CNF -> session complete");
static constexpr uint8_t wrong_session_nmk[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10};
auto const* session_nmk = ctx.slac_config.session_nmk;
if (ctx.slac_config.link_status.debug_simulate_failed_matching) {
ctx.log_info("Sending wrong NMK to EV to simulate a failed link setup after match request");
session_nmk = wrong_session_nmk;
}
match_cnf_message = std::make_unique<slac::messages::cm_slac_match_cnf>();
create_cm_slac_match_cnf(*match_cnf_message, *session, msg, session_nmk);
ctx.send_slac_message(tmp_ev_mac, *match_cnf_message);
session->state = MatchingSubState::MATCH_COMPLETE;
// call this immediately again in MatchedState::callback to handle things
session->set_next_timeout(0);
ctx.signal_cm_slac_match_cnf(tmp_ev_mac);
}
void MatchingState::finalize_sounding(MatchingSession& session) {
session_log(ctx, session, LogLevel::INFO, "Finalize sounding, sending CM_ATTEN_CHAR_IND");
session.state = MatchingSubState::WAIT_FOR_ATTEN_CHAR_RSP;
auto atten_char = create_cm_atten_char_ind(session, ctx.slac_config.sounding_atten_adjustment);
ctx.send_slac_message(session.ev_mac, atten_char);
session.set_next_timeout(slac::defs::TT_MATCH_RESPONSE_MS);
int aag_overall_sum = 0;
for (size_t i = 0; i < slac::defs::AAG_LIST_LEN; ++i) {
aag_overall_sum += atten_char.attenuation_profile.aag[i];
}
std::ostringstream ss;
ss << "Avg atten.: " << std::fixed << std::setprecision(1)
<< (static_cast<double>(aag_overall_sum) / slac::defs::AAG_LIST_LEN) << " dB";
if (ctx.slac_config.sounding_atten_adjustment != 0) {
ss << " plus offset " << std::to_string(ctx.slac_config.sounding_atten_adjustment) << " dB";
}
ss << ", from " << std::to_string(slac::defs::AAG_LIST_LEN) << " groups, " << session.captured_sounds << " sounds";
session_log(ctx, session, LogLevel::INFO, ss.str());
}
} // namespace slac::fsm::evse

View File

@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef EVSE_SLAC_STATES_MATCHING_HANDLE_SLAC_HPP
#define EVSE_SLAC_STATES_MATCHING_HANDLE_SLAC_HPP
#include <everest/slac/fsm/evse/states/matching.hpp>
namespace slac::fsm::evse {
enum class LogLevel {
DEBUG,
INFO,
WARN,
ERROR
};
void session_log(Context& ctx, MatchingSession& session, const LogLevel level, const std::string& text);
} // namespace slac::fsm::evse
#endif // EVSE_SLAC_STATES_MATCHING_HANDLE_SLAC_HPP

View File

@@ -0,0 +1,421 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <everest/slac/fsm/evse/states/others.hpp>
#include <cstring>
#include <optional>
#include <string_view>
#include <everest/slac/fsm/evse/states/matching.hpp>
#include "../misc.hpp"
namespace slac::fsm::evse {
static auto create_cm_set_key_req(uint8_t const* session_nmk) {
slac::messages::cm_set_key_req set_key_req;
set_key_req.key_type = slac::defs::CM_SET_KEY_REQ_KEY_TYPE_NMK;
set_key_req.my_nonce = 0x00000000;
set_key_req.your_nonce = 0x00000000;
set_key_req.pid = slac::defs::CM_SET_KEY_REQ_PID_HLE;
set_key_req.prn = htole16(slac::defs::CM_SET_KEY_REQ_PRN_UNUSED);
set_key_req.pmn = slac::defs::CM_SET_KEY_REQ_PMN_UNUSED;
set_key_req.cco_capability = slac::defs::CM_SET_KEY_REQ_CCO_CAP_NONE;
slac::utils::generate_nid_from_nmk(set_key_req.nid, session_nmk);
set_key_req.new_eks = slac::defs::CM_SET_KEY_REQ_PEKS_NMK_KNOWN_TO_STA;
memcpy(set_key_req.new_key, session_nmk, sizeof(set_key_req.new_key));
return set_key_req;
}
void ResetState::enter() {
ctx.log_info("Entered Reset state");
if (ctx.slac_config.regenerate_key_on_reset) {
ctx.slac_config.generate_nmk();
}
}
FSMSimpleState::HandleEventReturnType ResetState::handle_event(AllocatorType& sa, Event ev) {
const auto& cfg = ctx.slac_config;
if (ev == Event::SLAC_MESSAGE) {
if (handle_slac_message(ctx.slac_message_payload)) {
if (cfg.chip_reset.enabled) {
// If chip reset is enabled in config, go to ResetChipState and from there to IdleState
return sa.create_simple<ResetChipState>(ctx);
} else {
// If chip reset is disabled, go to IdleState directly
return sa.create_simple<IdleState>(ctx);
}
} else {
return sa.PASS_ON;
}
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
FSMSimpleState::CallbackReturnType ResetState::callback() {
const auto& cfg = ctx.slac_config;
if (setup_has_been_send == false) {
auto set_key_req = create_cm_set_key_req(cfg.session_nmk);
ctx.log_info("New NMK key: " + format_nmk(cfg.session_nmk));
ctx.send_slac_message(cfg.plc_peer_mac, set_key_req);
setup_has_been_send = true;
return cfg.set_key_timeout_ms;
} else {
ctx.log_error("CM_SET_KEY_REQ timeout - failed to setup NMK key");
return {};
}
}
bool ResetState::handle_slac_message(slac::messages::HomeplugMessage& message) {
const auto mmtype = message.get_mmtype();
if (mmtype != (slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_CNF)) {
// unexpected message
// FIXME (aw): need to also deal with CM_VALIDATE.REQ. It is optional in the standard.
ctx.log_warn("Received non-expected SLAC message of type " + format_mmtype(mmtype));
return false;
} else {
ctx.log_info("Received CM_SET_KEY_CNF");
return true;
}
}
void ResetChipState::enter() {
ctx.log_info("Entered HW Chip Reset state");
}
FSMSimpleState::HandleEventReturnType ResetChipState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
if (handle_slac_message(ctx.slac_message_payload)) {
return sa.create_simple<IdleState>(ctx);
} else {
return sa.PASS_ON;
}
} else if (ev == Event::SUCCESS) {
return sa.create_simple<IdleState>(ctx);
} else {
return sa.PASS_ON;
}
}
FSMSimpleState::CallbackReturnType ResetChipState::callback() {
const auto& cfg = ctx.slac_config;
if (sub_state == SubState::DELAY) {
sub_state = SubState::SEND_RESET;
return cfg.chip_reset.delay_ms;
} else if (sub_state == SubState::SEND_RESET) {
if (ctx.modem_vendor == ModemVendor::Qualcomm) {
slac::messages::qualcomm::cm_reset_device_req reset_req;
ctx.log_info("Resetting HW Chip using RS_DEV.REQ");
ctx.send_slac_message(cfg.plc_peer_mac, reset_req);
sub_state = SubState::DONE;
return cfg.chip_reset.timeout_ms;
} else if (ctx.modem_vendor == ModemVendor::Lumissil) {
slac::messages::lumissil::nscm_reset_device_req reset_req;
ctx.log_info("Resetting HW Chip using NSCM_RESET_DEVICE.REQ");
sub_state = SubState::DONE;
ctx.send_slac_message(cfg.plc_peer_mac, reset_req);
// CG5317 does not reply to the reset packet
return Event::SUCCESS;
} else {
ctx.log_info("Chip reset not supported on this chip");
}
} else {
ctx.log_error("Reset timeout, no response received - failed to reset the chip");
return {};
}
return {};
}
bool ResetChipState::handle_slac_message(slac::messages::HomeplugMessage& message) {
const auto mmtype = message.get_mmtype();
if (mmtype != (slac::defs::qualcomm::MMTYPE_CM_RESET_DEVICE | slac::defs::MMTYPE_MODE_CNF)) {
// unexpected message
ctx.log_warn("Received non-expected SLAC message of type " + format_mmtype(mmtype));
return false;
} else {
ctx.log_info("Received RS_DEV.CNF");
return true;
}
}
void IdleState::enter() {
ctx.signal_state("UNMATCHED");
ctx.log_info("Entered Idle state");
}
FSMSimpleState::HandleEventReturnType IdleState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::ENTER_BCD) {
return sa.create_simple<MatchingState>(ctx);
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
static std::optional<bool> check_link_status_cnf(const slac::fsm::evse::ModemVendor modem_vendor,
slac::messages::HomeplugMessage& message) {
const auto mmtype = message.get_mmtype();
if (modem_vendor == ModemVendor::Qualcomm &&
mmtype == (slac::defs::qualcomm::MMTYPE_LINK_STATUS | slac::defs::MMTYPE_MODE_CNF)) {
const auto success = message.get_payload<slac::messages::qualcomm::link_status_cnf>().link_status == 0x01;
return {success};
} else if (modem_vendor == ModemVendor::Lumissil &&
mmtype == (slac::defs::lumissil::MMTYPE_NSCM_GET_D_LINK_STATUS | slac::defs::MMTYPE_MODE_CNF)) {
const auto success =
message.get_payload<slac::messages::lumissil::nscm_get_d_link_status_cnf>().link_status == 0x01;
return {success};
}
return {};
}
static bool send_link_status_req(slac::fsm::evse::Context& ctx) {
if (ctx.modem_vendor == ModemVendor::Qualcomm) {
slac::messages::qualcomm::link_status_req link_status_req;
ctx.send_slac_message(ctx.slac_config.plc_peer_mac, link_status_req);
return true;
} else if (ctx.modem_vendor == ModemVendor::Lumissil) {
slac::messages::lumissil::nscm_get_d_link_status_req link_status_req;
ctx.send_slac_message(ctx.slac_config.plc_peer_mac, link_status_req);
return true;
}
return false;
}
void MatchedState::enter() {
ctx.signal_state("MATCHED");
ctx.signal_dlink_ready(true);
ctx.log_info("Entered Matched state");
}
FSMSimpleState::HandleEventReturnType MatchedState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
auto link_ok = check_link_status_cnf(ctx.modem_vendor, ctx.slac_message_payload);
if (link_ok.has_value()) {
if (link_ok.value()) {
return sa.PASS_ON;
} else {
ctx.log_error("Connection lost in matched state");
ctx.signal_error_routine_request();
return sa.PASS_ON;
}
}
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
}
return sa.PASS_ON;
}
FSMSimpleState::CallbackReturnType MatchedState::callback() {
const auto& link_status = ctx.slac_config.link_status;
if (not link_status.do_detect) {
return {};
}
if (not link_status_req_sent) {
link_status_req_sent = send_link_status_req(ctx);
} else {
// Link is confirmed not up yet, query again
link_status_req_sent = false;
}
return link_status.poll_in_matched_state_ms;
}
void MatchedState::leave() {
ctx.signal_dlink_ready(false);
}
void FailedState::enter() {
if (ctx.slac_config.ac_mode_five_percent) {
ctx.signal_error_routine_request();
}
ctx.log_info("Entered Failed state");
}
FSMSimpleState::HandleEventReturnType FailedState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
void WaitForLinkState::enter() {
ctx.log_info("Waiting for Link to be ready...");
start_time = std::chrono::steady_clock::now();
}
FSMSimpleState::HandleEventReturnType WaitForLinkState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
if (handle_slac_message(ctx.slac_message_payload)) {
return sa.create_simple<MatchedState>(ctx);
} else {
return sa.PASS_ON;
}
} else if (ev == Event::RETRY_MATCHING) {
ctx.log_error("Link could not be established");
// Notify higher layers to on CP signal
return sa.create_simple<FailedState>(ctx);
} else if (ev == Event::RESET) {
return sa.create_simple<ResetState>(ctx);
} else {
return sa.PASS_ON;
}
}
FSMSimpleState::CallbackReturnType WaitForLinkState::callback() {
const auto& cfg = ctx.slac_config;
if (not link_status_req_sent) {
link_status_req_sent = send_link_status_req(ctx);
return cfg.link_status.retry_ms;
} else {
// Did we timeout?
if (std::chrono::steady_clock::now() - start_time > std::chrono::milliseconds(cfg.link_status.timeout_ms)) {
return Event::RETRY_MATCHING;
}
// Link is confirmed not up yet, query again
link_status_req_sent = false;
return cfg.link_status.retry_ms;
}
}
WaitForLinkState::WaitForLinkState(Context& ctx,
std::unique_ptr<slac::messages::cm_slac_match_cnf> sent_match_cnf_message) :
FSMSimpleState(ctx), match_cnf_message(std::move(sent_match_cnf_message)) {
}
bool WaitForLinkState::handle_slac_message(slac::messages::HomeplugMessage& message) {
const auto mmtype = message.get_mmtype();
auto link_ok = check_link_status_cnf(ctx.modem_vendor, message);
if (link_ok.has_value() and link_ok.value()) {
return true;
}
if (mmtype == (slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_REQ)) {
// EV retries MATCH_REQ, so we send the CNF again
ctx.log_info("Received CM_SLAC_MATCH.REQ retry from EV, sending out CM_SLAC_MATCH.CNF again.");
ctx.send_slac_message(message.get_src_mac(), *match_cnf_message);
return false;
}
return false;
}
FSMSimpleState::HandleEventReturnType InitState::handle_event(AllocatorType& sa, Event ev) {
if (ev == Event::SLAC_MESSAGE) {
handle_slac_message(ctx.slac_message_payload);
return sa.PASS_ON;
} else if (ev == Event::SUCCESS) {
return sa.create_simple<ResetState>(ctx);
}
return sa.PASS_ON;
}
FSMSimpleState::CallbackReturnType InitState::callback() {
const auto& cfg = ctx.slac_config;
if (sub_state == SubState::QUALCOMM_OP_ATTR) {
sub_state = SubState::LUMISSIL_GET_VERSION;
slac::messages::qualcomm::op_attr_req op_attr_req;
ctx.send_slac_message(cfg.plc_peer_mac, op_attr_req);
return cfg.request_info_delay_ms;
} else if (sub_state == SubState::LUMISSIL_GET_VERSION) {
sub_state = SubState::DONE;
slac::messages::lumissil::nscm_get_version_req version_req;
ctx.send_slac_message(cfg.plc_peer_mac, version_req);
return cfg.request_info_delay_ms;
} else if (sub_state == SubState::DONE) {
// the requested info may or may not be implemented by the chip,
// so we ignore timeouts here.
return Event::SUCCESS;
}
return {};
}
static std::string get_qualcomm_device_info(slac::messages::qualcomm::op_attr_cnf const& msg) {
const auto get_string_view = [](auto const& raw) constexpr {
static_assert(sizeof(uint8_t) == sizeof(char));
return std::string_view(reinterpret_cast<char const*>(raw), sizeof(raw));
};
std::string result("Qualcomm PLC Device Attributes:");
result += "\n HW Platform: ";
result += get_string_view(msg.hw_platform);
result += "\n SW Platform: ";
result += get_string_view(msg.sw_platform);
result += ("\n Firmware: " + std::to_string(msg.version_major) + "." + std::to_string(msg.version_minor) + "." +
std::to_string(msg.version_pib) + "." + std::to_string(msg.reserved) + "-" +
std::to_string(msg.version_build));
result += "\n Build date: ";
result += get_string_view(msg.build_date);
result += "\n ZC signal: ";
// FIXME: no magic numbers
const auto zc_signal = (msg.line_freq_zc >> 2) & 0x03;
if (zc_signal == 0x01) {
result += "Detected";
} else if (zc_signal == 0x02) {
result += "Missing";
} else {
result += ("Unknown (" + std::to_string(zc_signal) + ")");
}
result += "\n Line frequency: ";
const auto line_freq = (msg.line_freq_zc) & 0x03;
if (line_freq == 0x01) {
result += "50Hz";
} else if (line_freq == 0x02) {
result += "60Hz";
} else {
result += ("Unknown (" + std::to_string(line_freq) + ")");
}
return result;
}
static std::string get_lumissil_device_info(slac::messages::lumissil::nscm_get_version_cnf const& msg) {
return "Lumissil PLC Device Firmware version: " + std::to_string(msg.version_major) + "." +
std::to_string(msg.version_minor) + "." + std::to_string(msg.version_patch) + "." +
std::to_string(msg.version_build);
}
void InitState::handle_slac_message(slac::messages::HomeplugMessage& message) {
const auto mmtype = message.get_mmtype();
if (mmtype == (slac::defs::qualcomm::MMTYPE_OP_ATTR | slac::defs::MMTYPE_MODE_CNF)) {
const auto msg = message.get_payload<slac::messages::qualcomm::op_attr_cnf>();
const auto device_info = get_qualcomm_device_info(msg);
ctx.log_info(device_info);
// This message is only supported on Qualcomm, so we can use it to detect the Vendor
ctx.modem_vendor = ModemVendor::Qualcomm;
} else if (mmtype == (slac::defs::lumissil::MMTYPE_NSCM_GET_VERSION | slac::defs::MMTYPE_MODE_CNF)) {
const auto msg = message.get_payload<slac::messages::lumissil::nscm_get_version_cnf>();
const auto device_info = get_lumissil_device_info(msg);
ctx.log_info(device_info);
// This message is only supported on Qualcomm, so we can use it to detect the Vendor
ctx.modem_vendor = ModemVendor::Lumissil;
}
}
} // namespace slac::fsm::evse

View File

@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#ifndef SLAC_CHANNEL_HPP
#define SLAC_CHANNEL_HPP
#include <memory>
#include <string>
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#include <slac/slac.hpp>
namespace utils {
class PacketSocket;
}
namespace slac {
// TODO (aw):
// - do we need to handle VLAN tags?
// - probably we need to handle different sessions ...
// - channel could own the interface handle and pass it to the packet
// socket
class Channel {
public:
Channel();
// Channel(const std::string& interface_name);
~Channel();
bool open(const std::string& interface_name);
bool read(slac::messages::HomeplugMessage& msg, int timeout);
bool write(slac::messages::HomeplugMessage& msg, int timeout);
const std::string& get_error() const {
return error;
}
bool got_timeout() const {
return did_timeout;
}
const uint8_t* get_mac_addr();
private:
// for debugging only, should be removed
std::unique_ptr<::utils::PacketSocket> socket;
uint8_t orig_if_mac[ETH_ALEN];
std::string error;
bool did_timeout{false};
};
} // namespace slac
#endif // SLAC_CHANNEL_HPP

View File

@@ -0,0 +1,434 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2026 Pionix GmbH and Contributors to EVerest
#ifndef SLAC_SLAC_HPP
#define SLAC_SLAC_HPP
#include <array>
#include <cstdint>
#include <utility>
#include <net/ethernet.h>
namespace slac {
// TODO (aw):
// - is run_id 8 or 16 bytes?
// - is nid 7 or 8 bytes?
namespace defs {
const uint16_t ETH_P_HOMEPLUG_GREENPHY = 0x88E1;
enum class MMV : uint8_t {
AV_1_0 = 0x0,
AV_1_1 = 0x1,
AV_2_0 = 0x2,
};
const int MME_MIN_LENGTH = 60;
const int STATION_ID_LEN = 17;
const int NID_LEN = 7;
const int NID_MOST_SIGNIFANT_BYTE_SHIFT = 4;
const uint8_t NID_SECURITY_LEVEL_SIMPLE_CONNECT = 0b00;
const int NID_SECURITY_LEVEL_OFFSET = 4;
const uint8_t DAKS_HASH[] = {0x08, 0x85, 0x6d, 0xaf, 0x7c, 0xf5, 0x81, 0x85};
const uint8_t NMK_HASH[] = {0x08, 0x85, 0x6d, 0xaf, 0x7c, 0xf5, 0x81, 0x86};
const std::array<std::uint8_t, 8> NMK_HASH_ARR = {0x08, 0x85, 0x6d, 0xaf, 0x7c, 0xf5, 0x81, 0x86};
const int NMK_LEN = 16;
const int AAG_LIST_LEN = 58;
const int RUN_ID_LEN = 8;
// FIXME (aw): where to put these iso15118/3 consts?
const int C_EV_START_ATTEN_CHAR_INDS = 3;
const int C_EV_MATCH_RETRY = 2;
const int C_EV_MATCH_MNBC = 10;
const int TP_EV_BATCH_MSG_INTERVAL_MS = 40; // 20ms - 50ms, interval between start_atten_char and mnbc_sound msgs
const int TT_EV_ATTEN_RESULTS_MS = 1200; // max. 1200ms
const int TT_EVSE_MATCH_MNBC_MS = 600;
const int TT_MATCH_SEQUENCE_MS = 400;
const int TT_MATCH_RESPONSE_MS = 200;
const int TT_EVSE_MATCH_SESSION_MS = 10000;
const int TT_EVSE_SLAC_INIT_MS = 40000; // (20s - 50s)
const int TT_MATCH_JOIN_MS = 12000; // max. 12s
const int T_STEP_EF_MS = 4000; // min. 4s
const uint16_t MMTYPE_CM_SET_KEY = 0x6008;
const uint16_t MMTYPE_CM_SLAC_PARAM = 0x6064;
const uint16_t MMTYPE_CM_START_ATTEN_CHAR = 0x6068;
const uint16_t MMTYPE_CM_ATTEN_CHAR = 0x606C;
const uint16_t MMTYPE_CM_MNBC_SOUND = 0x6074;
const uint16_t MMTYPE_CM_VALIDATE = 0x6078;
const uint16_t MMTYPE_CM_SLAC_MATCH = 0x607C;
const uint16_t MMTYPE_CM_ATTEN_PROFILE = 0x6084;
// Qualcomm Vendor MMEs
namespace qualcomm {
const uint16_t MMTYPE_CM_RESET_DEVICE = 0xA01C;
const uint16_t MMTYPE_LINK_STATUS = 0xA0B8;
const uint16_t MMTYPE_OP_ATTR = 0xA068;
const uint16_t MMTYPE_NW_INFO = 0xA038;
const uint16_t MMTYPE_GET_SW = 0xA000;
const uint16_t MMTYPE_VS_ATTENUATION_CHARACTERISTICS = 0xA14E;
} // namespace qualcomm
// Lumissil Vendor MMEs
namespace lumissil {
const uint16_t MMTYPE_NSCM_RESET_DEVICE = 0xAC70;
const uint16_t MMTYPE_NSCM_GET_VERSION = 0xAC6C;
const uint16_t MMTYPE_NSCM_GET_D_LINK_STATUS = 0xAC9C;
} // namespace lumissil
// Standard mmtypes
const uint16_t MMTYPE_MODE_REQ = 0x0000;
const uint16_t MMTYPE_MODE_CNF = 0x0001;
const uint16_t MMTYPE_MODE_IND = 0x0002;
const uint16_t MMTYPE_MODE_RSP = 0x0003;
const uint16_t MMTYPE_MODE_MASK = 0x0003;
const uint16_t MMTYPE_CATEGORY_STA_CCO = 0x0000;
const uint16_t MMTYPE_CATEGORY_PROXY = 0x2000;
const uint16_t MMTYPE_CATEGORY_CCO_CCO = 0x4000;
const uint16_t MMTYPE_CATEGORY_STA_STA = 0x6000;
const uint16_t MMTYPE_CATEGORY_MANUFACTOR_SPECIFIC = 0x8000;
const uint16_t MMTYPE_CATEGORY_VENDOR_SPECIFIC = 0xA000;
const uint16_t MMTYPE_CATEGORY_MASK = 0xE000;
const uint8_t COMMON_APPLICATION_TYPE = 0x00;
const uint8_t COMMON_SECURITY_TYPE = 0x00;
const uint8_t CM_VALIDATE_REQ_SIGNAL_TYPE = 0x00;
const uint8_t CM_VALIDATE_REQ_RESULT_READY = 0x01;
const uint8_t CM_VALIDATE_REQ_RESULT_FAILURE = 0x03;
const uint16_t CM_SLAC_MATCH_REQ_MVF_LENGTH = 0x3e;
const uint16_t CM_SLAC_MATCH_CNF_MVF_LENGTH = 0x56;
const uint8_t CM_SLAC_PARM_CNF_RESP_TYPE = 0x01; // = other GP station
const uint8_t CM_SLAC_PARM_CNF_NUM_SOUNDS = 10; // typical value
const uint8_t CM_SLAC_PARM_CNF_TIMEOUT = 0x06; // 600ms
const uint8_t CM_SET_KEY_REQ_KEY_TYPE_NMK = 0x01; // NMK (AES-128), Network Management Key
const uint8_t CM_SET_KEY_REQ_PID_HLE = 0x04;
const uint16_t CM_SET_KEY_REQ_PRN_UNUSED = 0x0000;
const uint8_t CM_SET_KEY_REQ_PMN_UNUSED = 0x00;
const uint8_t CM_SET_KEY_REQ_CCO_CAP_NONE = 0x00; // Level-0 CCo Capable, neither QoS nor TDMA
const uint8_t CM_SET_KEY_REQ_PEKS_NMK_KNOWN_TO_STA = 0x01;
const uint8_t CM_SET_KEY_CNF_RESULT_SUCCESS = 0x0;
const uint8_t CM_ATTEN_CHAR_RSP_RESULT = 0x00;
const uint8_t BROADCAST_MAC_ADDRESS[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
} // namespace defs
namespace utils {
void generate_nmk_hs(uint8_t nmk_hs[slac::defs::NMK_LEN], const char* plain_password, int password_len);
void generate_nid_from_nmk(uint8_t nid[slac::defs::NID_LEN], const uint8_t nmk[slac::defs::NMK_LEN]);
} // namespace utils
namespace messages {
typedef struct {
struct ether_header ethernet_header;
struct {
uint8_t mmv; // management message version
uint16_t mmtype; // management message type
} __attribute__((packed)) homeplug_header;
// the rest of this message is potentially payload data
uint8_t payload[ETH_FRAME_LEN - ETH_HLEN - sizeof(homeplug_header)];
} __attribute__((packed)) homeplug_message;
typedef struct {
uint8_t fmni; // fragmentation management number information
uint8_t fmsn; // fragmentation message sequence number
} __attribute__((packed)) homeplug_fragmentation_part;
class HomeplugMessage {
public:
homeplug_message* get_raw_message_ptr() {
return &raw_msg;
};
int get_raw_msg_len() const {
return raw_msg_len;
}
void setup_payload(void const* payload, int len, uint16_t mmtype, const defs::MMV mmv);
void setup_ethernet_header(const uint8_t dst_mac_addr[ETH_ALEN], const uint8_t src_mac_addr[ETH_ALEN] = nullptr);
uint16_t get_mmtype() const;
uint8_t* get_src_mac();
template <typename T> const T& get_payload() {
if (raw_msg.homeplug_header.mmv == static_cast<std::underlying_type_t<defs::MMV>>(defs::MMV::AV_1_0)) {
return *reinterpret_cast<T*>(raw_msg.payload);
}
// if not av 1.0 message, we need to shift by the fragmentation part
return *reinterpret_cast<T*>(raw_msg.payload + sizeof(homeplug_fragmentation_part));
}
bool is_valid() const;
bool keep_source_mac() const {
return keep_src_mac;
}
private:
homeplug_message raw_msg;
int raw_msg_len{-1};
bool keep_src_mac{false};
};
const int M_SOUND_TARGET_LEN = 6;
const int SENDER_ID_LEN = defs::STATION_ID_LEN;
const int SOURCE_ID_LEN = defs::STATION_ID_LEN;
const int RESP_ID_LEN = defs::STATION_ID_LEN;
const int PEV_ID_LEN = defs::STATION_ID_LEN;
const int EVSE_ID_LEN = defs::STATION_ID_LEN;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
// cipher fields are missing, because we restrict to security_type = 0x00
} __attribute__((packed)) cm_slac_parm_req;
typedef struct {
uint8_t m_sound_target[M_SOUND_TARGET_LEN]; // fixed to 0xFFFFFFFFFFFF
uint8_t num_sounds; // number of expected m-sounds
uint8_t timeout; // corresponds to TT_EVSE_match_MNBC, in units of 100ms
uint8_t resp_type; // fixed to 0x01, indicating 'other gp station'
uint8_t forwarding_sta[ETH_ALEN]; // ev host mac address
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint8_t run_id[defs::RUN_ID_LEN]; // matching run identifier, corresponding to the request
// cipher field is missing, because we restrict to security_type = 0x00
} __attribute__((packed)) cm_slac_parm_cnf;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint8_t num_sounds; // number of expected m-sounds
uint8_t timeout; // corresponds to TT_EVSE_match_MNBC
uint8_t resp_type; // fixed to 0x01, indicating 'other gp station'
uint8_t forwarding_sta[ETH_ALEN]; // ev host mac address
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
} __attribute__((packed)) cm_start_atten_char_ind;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint8_t source_address[ETH_ALEN]; // mac address of EV host, which initiates matching
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
uint8_t source_id[SOURCE_ID_LEN]; // unique id of the station, that sent the m-sounds
uint8_t resp_id[RESP_ID_LEN]; // unique id of the station, that is sending this message
uint8_t num_sounds; // number of sounds used for attenuation profile
struct {
uint8_t num_groups; // number of OFDM carrier groups
uint8_t aag[defs::AAG_LIST_LEN]; // AAG_1 .. AAG_N
} __attribute__((packed)) attenuation_profile;
} __attribute__((packed)) cm_atten_char_ind;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint8_t source_address[ETH_ALEN]; // mac address of EV host, which initiates matching
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
uint8_t source_id[SOURCE_ID_LEN]; // unique id of the station, that sent the m-sounds
uint8_t resp_id[RESP_ID_LEN]; // unique id of the station, that is sending this message
uint8_t result; // fixed to 0x00, indicates successful SLAC process
} __attribute__((packed)) cm_atten_char_rsp;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint8_t sender_id[SENDER_ID_LEN]; // sender id, if application_type = 0x00, it should be the pev's vin code
uint8_t remaining_sound_count; // count of remaining sound messages
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
uint8_t _reserved[8]; // note: this is to pad the run_id, which is defined to be 16 bytes for this message
uint8_t random[16]; // random value
} __attribute__((packed)) cm_mnbc_sound_ind;
// note: this message doesn't seem to part of hpgp, it is defined in ISO15118-3
typedef struct {
uint8_t pev_mac[ETH_ALEN]; // mac address of the EV host
uint8_t num_groups; // number of OFDM carrier groups
uint8_t _reserved;
uint8_t aag[defs::AAG_LIST_LEN]; // list of average attenuation for each group
} __attribute__((packed)) cm_atten_profile_ind;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint16_t mvf_length; // fixed to 0x3e = 62 bytes following
uint8_t pev_id[PEV_ID_LEN]; // vin code of PEV
uint8_t pev_mac[ETH_ALEN]; // mac address of the EV host
uint8_t evse_id[EVSE_ID_LEN]; // EVSE id
uint8_t evse_mac[ETH_ALEN]; // mac address of the EVSE
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
uint8_t _reserved[8]; // note: this is to pad the run_id, which is defined to be 16 bytes for this message
} __attribute__((packed)) cm_slac_match_req;
typedef struct {
uint8_t application_type; // fixed to 0x00, indicating 'pev-evse matching'
uint8_t security_type; // fixed to 0x00, indicating 'no security'
uint16_t mvf_length; // fixed to 0x56 = 86 bytes following
uint8_t pev_id[PEV_ID_LEN]; // vin code of PEV
uint8_t pev_mac[ETH_ALEN]; // mac address of the EV host
uint8_t evse_id[EVSE_ID_LEN]; // EVSE id
uint8_t evse_mac[ETH_ALEN]; // mac address of the EVSE
uint8_t run_id[defs::RUN_ID_LEN]; // indentifier for a matching run
uint8_t _rerserved[8]; // note: this is to pad the run_id, which is defined to be 16 bytes for this message
uint8_t nid[defs::NID_LEN]; // network id derived from the nmk
uint8_t _reserved2; // note: this is to pad the nid, which is defined to be 8 bytes for this message
uint8_t nmk[defs::NMK_LEN]; // private nmk of the EVSE
} __attribute__((packed)) cm_slac_match_cnf;
typedef struct {
uint8_t signal_type; // fixed to 0x00: PEV S2 toggles on control pilot line
uint8_t timer; // in the first request response exchange: should be set to 0x00
// in the second request response exchange: 0x00 = 100ms, 0x01 = 200ms TT_EVSE_vald_toggle
uint8_t result; // in the first request response exchange: should be set to 0x01 = ready
// in the second request response exchange: should be set to 0x01 = ready
} __attribute__((packed)) cm_validate_req;
typedef struct {
uint8_t signal_type; // fixed to 0x00: PEV S2 toggles on control pilot line
uint8_t toggle_num; // in the first request response exchange: should be set to 0x00
// in the second request response exchange: number of detected BC
// edges during TT_EVSE_vald_toggle
uint8_t result; // 0x00 = not ready, 0x01 = ready, 0x02 = success, 0x03 = failure, 0x04 = not required
} __attribute__((packed)) cm_validate_cnf;
typedef struct {
uint8_t key_type; // fixed to 0x01, indicating NMK
uint32_t my_nonce; // fixed to 0x00000000: encrypted payload not used
uint32_t your_nonce; // fixed to 0x00000000: encrypted payload not used
uint8_t pid; // fixed to 0x04: HLE protocol
uint16_t prn; // fixed to 0x0000: encrypted payload not used
uint8_t pmn; // fixed to 0x00: encrypted payload not used
uint8_t cco_capability; // CCo capability according to the station role
uint8_t nid[defs::NID_LEN]; // 54 LSBs = NID, 2 MSBs = 0b00
uint8_t new_eks; // fixed to 0x01: NMK
uint8_t new_key[defs::NMK_LEN]; // new NMK
} __attribute__((packed)) cm_set_key_req;
typedef struct {
uint8_t result; // 0x00 = success, 0x01 = failure, 0x02 - 0xFF = reserved
uint32_t my_nonce;
uint32_t your_nonce;
uint8_t pid;
uint16_t prn;
uint8_t pmn;
uint8_t cco_capability;
} __attribute__((packed)) cm_set_key_cnf;
namespace qualcomm {
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0xb0, 0x52}; // Qualcomm Vendor MME code
} __attribute__((packed)) cm_reset_device_req;
typedef struct {
uint8_t vendor_mme[3]; // Vendor MME code
uint8_t success;
} __attribute__((packed)) cm_reset_device_cnf;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0xb0, 0x52}; // Qualcomm Vendor MME code
} __attribute__((packed)) link_status_req;
typedef struct {
uint8_t vendor_mme[3]; // Vendor MME code
uint8_t reserved;
uint8_t link_status;
} __attribute__((packed)) link_status_cnf;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0xb0, 0x52}; // Qualcomm Vendor MME code
uint32_t cookie{0x12345}; // some cookie we will also get in the reply
uint8_t report_type{0}; // binary report
} __attribute__((packed)) op_attr_req;
typedef struct {
uint8_t vendor_mme[3]; // Vendor MME code
uint16_t success; // 0x00 means success
uint32_t cookie;
uint8_t report_type; // should be 0x00 (binary)
uint16_t size; // should be 118, otherwise we do not know the structure
uint8_t hw_platform[16];
uint8_t sw_platform[16];
uint32_t version_major;
uint32_t version_minor;
uint32_t version_pib;
uint32_t version_build;
uint32_t reserved;
uint8_t build_date[8];
uint8_t release_type[12];
uint8_t sdram_type;
uint8_t reserved2;
uint8_t line_freq_zc;
uint32_t sdram_size;
uint8_t authorization_mode;
} __attribute__((packed)) op_attr_cnf;
} // namespace qualcomm
namespace lumissil {
typedef struct {
uint16_t version;
uint32_t reserved;
uint8_t request_id;
uint8_t reserved2[12];
} __attribute__((packed)) lms_header;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0x16, 0xE8}; // Lumissil Vendor MME code
lms_header lms;
uint8_t mode{0}; // Normal reset
} __attribute__((packed)) nscm_reset_device_req;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0x16, 0xE8}; // Lumissil Vendor MME code
lms_header lms;
} __attribute__((packed)) nscm_get_version_req;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0x16, 0xE8}; // Lumissil Vendor MME code
lms_header lms;
uint16_t version_major;
uint16_t version_minor;
uint16_t version_patch;
uint16_t version_build;
uint16_t reserved;
} __attribute__((packed)) nscm_get_version_cnf;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0x16, 0xE8}; // Lumissil Vendor MME code
lms_header lms;
} __attribute__((packed)) nscm_get_d_link_status_req;
typedef struct {
uint8_t vendor_mme[3] = {0x00, 0x16, 0xE8}; // Lumissil Vendor MME code
lms_header lms;
uint8_t link_status;
} __attribute__((packed)) nscm_get_d_link_status_cnf;
// There is no CNF for this reset packet
} // namespace lumissil
} // namespace messages
} // namespace slac
#endif // SLAC_SLAC_HPP

View File

@@ -0,0 +1,18 @@
add_library(slac_io)
add_library(slac::io ALIAS slac_io)
ev_register_library_target(slac_io)
target_include_directories(slac_io
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_sources(slac_io
PRIVATE
src/io.cpp
)
target_link_libraries(slac_io
PUBLIC
slac::slac
)

View File

@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#ifndef SLAC_IO_HPP
#define SLAC_IO_HPP
#include <condition_variable>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <slac/channel.hpp>
class SlacIO {
public:
using InputHandlerFnType = void(slac::messages::HomeplugMessage&);
void init(const std::string& if_name);
void run(std::function<InputHandlerFnType> callback);
void send(slac::messages::HomeplugMessage& msg);
void quit();
// cannot be const while libslac's SlacChannel::get_mac_addr() isn't const
const uint8_t* get_mac_addr() /* const */;
private:
void loop();
slac::Channel slac_channel;
slac::messages::HomeplugMessage incoming_msg;
std::function<InputHandlerFnType> input_handler;
std::thread loop_thread;
bool running{false};
};
#endif // SLAC_IO_HPP

View File

@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include <everest/slac/io.hpp>
#include <stdexcept>
#include <thread>
void SlacIO::init(const std::string& if_name) {
if (!slac_channel.open(if_name)) {
throw std::runtime_error(slac_channel.get_error());
}
}
void SlacIO::run(std::function<InputHandlerFnType> callback) {
input_handler = callback;
running = true;
loop_thread = std::thread(&SlacIO::loop, this);
}
void SlacIO::quit() {
if (!running) {
return;
}
running = false;
loop_thread.join();
}
void SlacIO::loop() {
while (running) {
if (slac_channel.read(incoming_msg, 10)) {
input_handler(incoming_msg);
}
}
}
void SlacIO::send(slac::messages::HomeplugMessage& msg) {
// FIXME (aw): handle errors
slac_channel.write(msg, 1);
}
const uint8_t* SlacIO::get_mac_addr() /* const */ {
return slac_channel.get_mac_addr();
}

View File

@@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include <slac/channel.hpp>
#include <algorithm>
#include <cassert>
#include <cstring>
#include "packet_socket.hpp"
namespace slac {
Channel::Channel() : socket(nullptr){};
bool Channel::open(const std::string& interface_name) {
did_timeout = false;
auto if_info = ::utils::InterfaceInfo(interface_name);
if (!if_info.is_valid()) {
error = if_info.get_error();
return false;
}
memcpy(orig_if_mac, if_info.get_mac(), sizeof(orig_if_mac));
socket = std::make_unique<::utils::PacketSocket>(if_info, defs::ETH_P_HOMEPLUG_GREENPHY);
if (!socket->is_valid()) {
error = socket->get_error();
socket.reset();
return false;
}
return true;
}
Channel::~Channel() = default;
bool Channel::read(slac::messages::HomeplugMessage& msg, int timeout) {
did_timeout = false;
using IOResult = ::utils::PacketSocket::IOResult;
if (socket) {
switch (socket->read(reinterpret_cast<uint8_t*>(msg.get_raw_message_ptr()), timeout)) {
// FIXME (aw): this enum conversion looks ugly
case IOResult::Failure:
error = socket->get_error();
return false;
case IOResult::Timeout:
did_timeout = true;
return false;
case IOResult::Ok:
return true;
}
}
error = "No IO socket available\n";
return false;
}
bool Channel::write(slac::messages::HomeplugMessage& msg, int timeout) {
using IOResult = ::utils::PacketSocket::IOResult;
assert(("Homeplug message is not valid\n", msg.is_valid()));
did_timeout = false;
if (socket) {
auto raw_msg_ether_shost = msg.get_src_mac();
if (!msg.keep_source_mac()) {
memcpy(raw_msg_ether_shost, orig_if_mac, sizeof(orig_if_mac));
}
switch (socket->write(msg.get_raw_message_ptr(), msg.get_raw_msg_len(), timeout)) {
case IOResult::Failure:
error = socket->get_error();
return false;
case IOResult::Timeout:
did_timeout = true;
return false;
case IOResult::Ok:
return true;
}
}
error = "No IO socket available\n";
return false;
}
const uint8_t* Channel::get_mac_addr() {
return orig_if_mac;
}
} // namespace slac

View File

@@ -0,0 +1,148 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "packet_socket.hpp"
#include <cstring>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <linux/if_packet.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
namespace utils {
InterfaceInfo::InterfaceInfo(const std::string& interface_name) {
// fetch all interfaces
struct ifaddrs* if_addrs;
int ret = getifaddrs(&if_addrs);
if (ret == -1) {
error = "Error while calling getifaddrs(): ";
error += strerror(errno);
return;
}
// iterate through them and list them
struct ifaddrs* cur_if_addr = if_addrs;
while (cur_if_addr) {
if (cur_if_addr->ifa_addr && cur_if_addr->ifa_addr->sa_family == AF_PACKET) {
if (0 == interface_name.compare(cur_if_addr->ifa_name)) {
const auto* addr_info = reinterpret_cast<struct sockaddr_ll*>(cur_if_addr->ifa_addr);
memcpy(mac, addr_info->sll_addr, 6);
interface_index = addr_info->sll_ifindex;
valid = true;
break;
}
}
cur_if_addr = cur_if_addr->ifa_next;
}
freeifaddrs(if_addrs);
if (!valid) {
error = "Interface " + interface_name + " not found";
}
}
PacketSocket::PacketSocket(const InterfaceInfo& if_info, int protocol) {
// FIXME (aw): do we need to use O_NONBLOCKING?
socket_fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(protocol));
if (socket_fd == -1) {
error = "Couldn't create the socket: ";
error += strerror(errno);
return;
}
// bind this packet socket to a specific interface
struct sockaddr_ll sock_addr = {
AF_PACKET, // sll_family
htons(protocol), // sll_protocol
if_info.get_index(), // sll_ifindex
0x00, // sll_hatype, set on receiving
0x00, // sll_pkttype, set on receiving
ETH_ALEN, // sll_halen
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // sll_addr[8]
};
if (-1 == bind(socket_fd, (struct sockaddr*)&sock_addr, sizeof(sock_addr))) {
error = "Failed to bind the socket: ";
error += strerror(errno);
close(socket_fd);
}
// everything should have worked out
valid = true;
}
PacketSocket::IOResult PacketSocket::read(uint8_t* buffer, int timeout) {
struct pollfd poll_fd = {
socket_fd, // file descriptor
POLLIN, // requested event
0 // returned event
};
int ret = poll(&poll_fd, 1, timeout);
if (-1 == ret) {
error = std::string("poll() failed with: ") + strerror(errno);
return IOResult::Failure;
}
if (0 == ret) {
return IOResult::Timeout;
}
if ((poll_fd.revents & POLLIN) == 0) {
error = "poll() set other flag than POLLIN";
return IOResult::Failure;
}
bytes_read = ::read(socket_fd, buffer, MIN_BUFFER_SIZE);
if (bytes_read == -1) {
error = std::string("read() failed with: ") + strerror(errno);
return IOResult::Failure;
}
return IOResult::Ok;
}
PacketSocket::IOResult PacketSocket::write(const void* buf, size_t size, int timeout) {
struct pollfd poll_fd = {
socket_fd, // file descriptor
POLLOUT, // requested event
0 // returned event
};
int ret = poll(&poll_fd, 1, timeout);
if (ret == -1) {
error = std::string("poll() failed with: ") + strerror(errno);
return IOResult::Failure;
}
if (ret == 0) {
return IOResult::Timeout;
}
if ((poll_fd.revents & POLLOUT) == 0) {
error = "poll() set other flag than POLLOUT";
return IOResult::Failure;
}
auto bytes_written = ::write(socket_fd, buf, size);
if (-1 == bytes_written) {
error = std::string("write() failed with: ") + strerror(errno);
return IOResult::Failure;
}
if (bytes_written != size) {
error = "write() only send part of the datagram - this should not happen";
return IOResult::Failure;
}
return IOResult::Ok;
}
} // namespace utils

View File

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#ifndef SRC_PACKET_SOCKET_HPP
#define SRC_PACKET_SOCKET_HPP
#include <cstdint>
#include <string>
#include <linux/if_ether.h>
namespace utils {
class InterfaceInfo {
public:
explicit InterfaceInfo(const std::string& interface_name);
bool is_valid() {
return valid;
};
const std::string& get_error() const {
return error;
};
const int get_index() const {
return interface_index;
};
const uint8_t* get_mac() const {
return mac;
};
private:
bool valid{false};
int interface_index{0};
std::string error;
uint8_t mac[ETH_ALEN];
};
class PacketSocket {
public:
enum class IOResult {
Ok,
Failure,
Timeout
};
PacketSocket(const InterfaceInfo& if_info, int protocol);
bool is_valid() {
return valid;
};
const std::string& get_error() {
return error;
}
IOResult read(uint8_t* buffer, int timeout);
int get_last_read_size() const {
return bytes_read;
};
IOResult write(const void* buf, size_t count, int timeout);
static const int MIN_BUFFER_SIZE = ETH_FRAME_LEN;
private:
int bytes_read{-1};
bool valid{false};
std::string error;
int socket_fd{-1};
};
} // namespace utils
#endif // SRC_PACKET_SOCKET_HPP

View File

@@ -0,0 +1,135 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include <slac/slac.hpp>
#include <algorithm>
#include <cassert>
#include <cstring>
#include <arpa/inet.h>
#include <endian.h>
#include <everest/tls/openssl_util.hpp>
#include <stdexcept>
namespace slac {
namespace utils {
// note on byte order:
// - sha256 takes the most significant byte first from the lowest
// memory address
// - for the generation of the aes-128, or NMK-HS, the first octet of
// the sha256 output is taken as the zero octet for the NMK-HS
// - for the generation of NID, the NMK is fed into sha256, so having
// a const char* as input should be the proper byte ordering already
void generate_nmk_hs(uint8_t nmk_hs[slac::defs::NMK_LEN], const char* plain_password, int password_len) {
// do pbkdf1 (use sha256 as hashing function, iterate 1000 times,
// use salt)
std::vector<std::uint8_t> input(plain_password, plain_password + password_len);
input.insert(input.end(), slac::defs::NMK_HASH_ARR.begin(), slac::defs::NMK_HASH_ARR.end());
openssl::sha_256_digest_t digest;
openssl::sha_256(input.data(), input.size(), digest);
for (int i = 0; i < 1000 - 1; ++i) {
openssl::sha_256(digest.data(), openssl::sha_256_digest_size, digest);
}
memcpy(nmk_hs, digest.data(), slac::defs::NMK_LEN);
}
void generate_nid_from_nmk(uint8_t nid[slac::defs::NID_LEN], const uint8_t nmk[slac::defs::NMK_LEN]) {
// msb of least significant octet of NMK should be the leftmost bit
// of the input, which corresponds to the usual const char* order
// do pbkdf1 (use sha256 as hashing function, iterate 5 times, no
// salt)
openssl::sha_256_digest_t digest;
openssl::sha_256(nmk, slac::defs::NMK_LEN, digest);
for (int i = 0; i < 5 - 1; ++i) {
openssl::sha_256(digest.data(), openssl::sha_256_digest_size, digest);
}
// use leftmost 52 bits of the hash output
// left most bit should be bit 7 of the nid
memcpy(nid, digest.data(), slac::defs::NID_LEN - 1); // (bits 52 - 5)
nid[slac::defs::NID_LEN - 1] =
(slac::defs::NID_SECURITY_LEVEL_SIMPLE_CONNECT << slac::defs::NID_SECURITY_LEVEL_OFFSET) |
((static_cast<uint8_t>(digest.data()[6])) >> slac::defs::NID_MOST_SIGNIFANT_BYTE_SHIFT);
}
} // namespace utils
namespace messages {
static constexpr auto effective_payload_length(const defs::MMV mmv) {
if (mmv == defs::MMV::AV_1_0) {
return sizeof(homeplug_message::payload);
} else {
return sizeof(homeplug_message::payload) - sizeof(homeplug_fragmentation_part);
}
}
void HomeplugMessage::setup_payload(void const* payload, int len, uint16_t mmtype, const defs::MMV mmv) {
if (len > effective_payload_length(mmv)) {
throw std::runtime_error("Homeplug Payload length too long");
}
raw_msg.homeplug_header.mmv = static_cast<std::underlying_type_t<defs::MMV>>(mmv);
raw_msg.homeplug_header.mmtype = htole16(mmtype);
uint8_t* dst = raw_msg.payload;
if (mmv != defs::MMV::AV_1_0) {
homeplug_fragmentation_part fragmentation_part{};
fragmentation_part.fmni = 0; // not implemented
fragmentation_part.fmsn = 0; // not implemented
memcpy(dst, &fragmentation_part, sizeof(fragmentation_part));
dst += sizeof(fragmentation_part); // adjust effective payload start
}
// copy payload into place
memcpy(dst, payload, len);
// get pointer to the end of buffer
uint8_t* dst_end = dst + len;
// calculate raw message length
raw_msg_len = dst_end - reinterpret_cast<uint8_t*>(&raw_msg);
// do padding
auto padding_len = defs::MME_MIN_LENGTH - raw_msg_len;
if (padding_len > 0) {
memset(dst_end, 0x00, padding_len);
raw_msg_len = defs::MME_MIN_LENGTH;
}
}
void HomeplugMessage::setup_ethernet_header(const uint8_t dst_mac_addr[ETH_ALEN],
const uint8_t src_mac_addr[ETH_ALEN]) {
// ethernet frame byte order is big endian
raw_msg.ethernet_header.ether_type = htons(defs::ETH_P_HOMEPLUG_GREENPHY);
if (dst_mac_addr) {
memcpy(raw_msg.ethernet_header.ether_dhost, dst_mac_addr, ETH_ALEN);
}
if (src_mac_addr) {
memcpy(raw_msg.ethernet_header.ether_shost, src_mac_addr, ETH_ALEN);
keep_src_mac = true;
} else {
keep_src_mac = false;
}
}
uint16_t HomeplugMessage::get_mmtype() const {
return le16toh(raw_msg.homeplug_header.mmtype);
}
uint8_t* HomeplugMessage::get_src_mac() {
return raw_msg.ethernet_header.ether_shost;
}
bool HomeplugMessage::is_valid() const {
return raw_msg_len >= defs::MME_MIN_LENGTH;
}
} // namespace messages
} // namespace slac

View File

@@ -0,0 +1,41 @@
add_executable(evse_slac_test)
target_sources(evse_slac_test
PRIVATE
evse_slac_test.cpp
)
target_link_libraries(evse_slac_test
PRIVATE
slac_fsm_evse
fmt::fmt
)
target_compile_features(evse_slac_test PRIVATE cxx_std_17)
add_executable(evse_vs_ev)
target_sources(evse_vs_ev
PRIVATE
evse_vs_ev/socket_pair_bridge.cpp
evse_vs_ev/plc_emu.cpp
evse_vs_ev/main.cpp
)
target_link_libraries(evse_vs_ev
PRIVATE
slac::fsm::evse
slac::fsm::ev
Threads::Threads
)
target_compile_features(evse_vs_ev PRIVATE cxx_std_17)
add_executable(bridger)
target_sources(bridger
PRIVATE
evse_vs_ev/plc_emu.cpp
bridger.cpp
)
target_link_libraries(bridger
PRIVATE
slac::slac
fmt::fmt
)
target_compile_features(bridger PRIVATE cxx_std_17)

View File

@@ -0,0 +1,130 @@
#include <cstdio>
#include <cstring>
#include <string>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <slac/slac.hpp>
#include "evse_vs_ev/plc_emu.hpp"
void print_mac(const uint8_t* mac) {
printf("%2X:%2X:%2X:%2X:%2X:%2X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
// FIXME (aw): this helper doesn't really belong here
static void exit_with_error(const char* msg) {
fprintf(stderr, "%s (%s)\n", msg, strerror(errno));
exit(-EXIT_FAILURE);
}
struct InterfaceInfo {
uint8_t mac[ETH_ALEN];
int interface_index{-1};
};
InterfaceInfo get_interface_info(const std::string& interface_name) {
InterfaceInfo if_info;
struct ifaddrs* if_addrs;
if (-1 == getifaddrs(&if_addrs)) {
// FIXME (aw): proper error handling?
return if_info;
}
// iterate through them and list them
struct ifaddrs* cur_if_addr = if_addrs;
while (cur_if_addr) {
if (cur_if_addr->ifa_addr && cur_if_addr->ifa_addr->sa_family == AF_PACKET) {
if (0 == interface_name.compare(cur_if_addr->ifa_name)) {
const auto* addr_info = reinterpret_cast<struct sockaddr_ll*>(cur_if_addr->ifa_addr);
memcpy(if_info.mac, addr_info->sll_addr, sizeof(if_info.mac));
if_info.interface_index = addr_info->sll_ifindex;
break;
}
}
cur_if_addr = cur_if_addr->ifa_next;
}
freeifaddrs(if_addrs);
return if_info;
}
int create_raw_homeplug_socket(const InterfaceInfo& interface_info) {
const uint16_t homeplug_protocol = slac::defs::ETH_P_HOMEPLUG_GREENPHY;
const auto socket_fd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(homeplug_protocol));
if (socket_fd == -1) {
exit_with_error("Couldn't create the socket");
}
// bind this packet socket to a specific interface
struct sockaddr_ll sock_addr = {
AF_PACKET, // sll_family
htons(homeplug_protocol), // sll_protocol
interface_info.interface_index, // sll_ifindex
0x00, // sll_hatype, set on receiving
0x00, // sll_pkttype, set on receiving
ETH_ALEN, // sll_halen
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // sll_addr[8]
};
if (-1 == bind(socket_fd, (struct sockaddr*)&sock_addr, sizeof(sock_addr))) {
exit_with_error("Failed to bind the socket");
}
return socket_fd;
}
void loop(int ev_fd, int evse_fd) {
constexpr static auto EV_INDEX = 0;
constexpr static auto EVSE_INDEX = 1;
struct pollfd pollfds[] = {
{ev_fd, POLLIN, 0},
{evse_fd, POLLIN, 0},
};
static constexpr auto num_fds = sizeof(pollfds) / sizeof(struct pollfd);
while (true) {
const auto status = poll(pollfds, num_fds, -1);
if (status == -1) {
exit_with_error("bridge poll");
}
if (pollfds[EV_INDEX].revents & POLLIN) {
printf("Received ev input\n");
handle_ev_input(ev_fd, evse_fd);
}
if (pollfds[EVSE_INDEX].revents & POLLIN) {
printf("Received evse input\n");
handle_evse_input(evse_fd, ev_fd);
}
}
}
int main(int argc, char* argv[]) {
const auto ev_bridge_device_info = get_interface_info("vev-bridge");
const auto evse_bridge_device_info = get_interface_info("vevse-bridge");
const auto ev_fd = create_raw_homeplug_socket(ev_bridge_device_info);
const auto evse_fd = create_raw_homeplug_socket(evse_bridge_device_info);
loop(ev_fd, evse_fd);
// printf("mac: ");
// print_mac(info.mac);
// printf("index: %d\n", info.interface_index);
return 0;
}

View File

@@ -0,0 +1,311 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <chrono>
#include <cstdio>
#include <optional>
#include <thread>
#include <everest/slac/fsm/evse/fsm.hpp>
#include <everest/slac/fsm/evse/states/others.hpp>
#include <fmt/format.h>
static auto create_cm_set_key_cnf() {
// FIXME (aw): needs to be fully implemented!
slac::messages::cm_set_key_cnf set_key_cnf;
slac::messages::HomeplugMessage hp_message;
hp_message.setup_payload(&set_key_cnf, sizeof(set_key_cnf),
(slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_CNF), slac::defs::MMV::AV_1_1);
return hp_message;
}
static auto create_cm_validate_req() {
slac::messages::cm_validate_req validate_req;
validate_req.signal_type = slac::defs::CM_VALIDATE_REQ_SIGNAL_TYPE;
validate_req.timer = 0;
validate_req.result = slac::defs::CM_VALIDATE_REQ_RESULT_READY;
slac::messages::HomeplugMessage hp_message;
hp_message.setup_payload(&validate_req, sizeof(validate_req),
(slac::defs::MMTYPE_CM_VALIDATE | slac::defs::MMTYPE_MODE_REQ), slac::defs::MMV::AV_1_1);
return hp_message;
}
struct EVSession {
EVSession(const std::array<uint8_t, 8>& run_id_, const std::array<uint8_t, 6>& mac_) : run_id(run_id_), mac(mac_){};
// FIXME (aw): all these create_cm_* need to be fully implemented!
auto create_cm_slac_parm_req() {
slac::messages::cm_slac_parm_req parm_req;
std::copy(run_id.begin(), run_id.end(), parm_req.run_id);
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac.data(), mac.data());
hp_message.setup_payload(&parm_req, sizeof(parm_req),
(slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_REQ),
slac::defs::MMV::AV_1_1);
return hp_message;
}
auto create_cm_start_atten_char_ind() {
slac::messages::cm_start_atten_char_ind atten_char_ind;
std::copy(run_id.begin(), run_id.end(), atten_char_ind.run_id);
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac.data(), mac.data());
hp_message.setup_payload(&atten_char_ind, sizeof(atten_char_ind),
(slac::defs::MMTYPE_CM_START_ATTEN_CHAR | slac::defs::MMTYPE_MODE_IND),
slac::defs::MMV::AV_1_1);
return hp_message;
}
auto create_cm_mnbc_sound_ind() {
slac::messages::cm_mnbc_sound_ind sound_ind;
std::copy(run_id.begin(), run_id.end(), sound_ind.run_id);
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac.data(), mac.data());
hp_message.setup_payload(&sound_ind, sizeof(sound_ind),
(slac::defs::MMTYPE_CM_MNBC_SOUND | slac::defs::MMTYPE_MODE_IND),
slac::defs::MMV::AV_1_1);
return hp_message;
}
auto create_cm_atten_profile_ind() {
slac::messages::cm_atten_profile_ind profile_ind;
std::copy(mac.begin(), mac.end(), profile_ind.pev_mac);
profile_ind.num_groups = slac::defs::AAG_LIST_LEN;
for (int i = 0; i < slac::defs::AAG_LIST_LEN; ++i) {
profile_ind.aag[i] = i;
}
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac.data(), mac.data());
hp_message.setup_payload(&profile_ind, sizeof(profile_ind),
(slac::defs::MMTYPE_CM_ATTEN_PROFILE | slac::defs::MMTYPE_MODE_IND),
slac::defs::MMV::AV_1_1);
return hp_message;
}
auto create_cm_atten_char_rsp() {
slac::messages::cm_atten_char_rsp atten_char;
std::copy(run_id.begin(), run_id.end(), atten_char.run_id);
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac.data(), mac.data());
hp_message.setup_payload(&atten_char, sizeof(atten_char),
(slac::defs::MMTYPE_CM_ATTEN_CHAR | slac::defs::MMTYPE_MODE_RSP),
slac::defs::MMV::AV_1_1);
return hp_message;
}
auto create_cm_slac_match_req() {
slac::messages::cm_slac_match_req match_req;
std::copy(run_id.begin(), run_id.end(), match_req.run_id);
slac::messages::HomeplugMessage hp_message;
hp_message.setup_ethernet_header(mac.data(), mac.data());
hp_message.setup_payload(&match_req, sizeof(match_req),
(slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_REQ),
slac::defs::MMV::AV_1_1);
return hp_message;
}
private:
std::array<uint8_t, 8> run_id;
const std::array<uint8_t, ETH_ALEN>& mac;
};
void feed_machine_for(slac::fsm::evse::FSM& machine, int period_ms,
fsm::FeedResult<slac::fsm::evse::FSMReturnType> feed_result) {
using namespace std::chrono;
auto end_tp = steady_clock::now() + milliseconds(period_ms);
while (true) {
if (feed_result.transition()) {
// need to call feed
} else if (feed_result.unhandled_event()) {
printf("DEBUG: got an unhandled event\n");
break;
} else if (feed_result.internal_error()) {
printf("ERROR: internal FSM error\n");
exit(EXIT_FAILURE);
} else if (feed_result.has_value() == false) {
// nothing to do
break;
} else if (feed_result.has_value() == true) {
const auto timeout = *feed_result;
if (timeout == 0) {
// need to call feed directly
} else {
auto next_tp = steady_clock::now() + milliseconds(timeout);
if (next_tp > end_tp) {
break;
}
std::this_thread::sleep_until(next_tp);
}
} else {
printf("ERROR: unknown feed result case\n");
exit(EXIT_FAILURE);
}
feed_result = machine.feed();
}
std::this_thread::sleep_until(end_tp);
}
int main(int argc, char* argv[]) {
const auto ATTENUATION_ADJUSTMENT = 10;
printf("Hi from SLAC!\n");
std::optional<slac::messages::HomeplugMessage> msg_in;
slac::fsm::evse::ContextCallbacks callbacks;
callbacks.log = [](const std::string& text) { fmt::print("SLAC LOG: {}\n", text); };
callbacks.send_raw_slac = [&msg_in](slac::messages::HomeplugMessage& hp_message) { msg_in = hp_message; };
auto ctx = slac::fsm::evse::Context(callbacks);
ctx.slac_config.sounding_atten_adjustment = ATTENUATION_ADJUSTMENT;
ctx.slac_config.chip_reset.enabled = false;
auto machine = slac::fsm::evse::FSM();
//
// reset machine
//
machine.reset<slac::fsm::evse::ResetState>(ctx);
auto fr = machine.feed();
// assert that CM_SET_KEY_REQ gets set!
if (!msg_in.has_value() || msg_in->get_mmtype() != (slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_REQ)) {
printf("Expected CM_SET_KEY_REQ!\n");
exit(EXIT_FAILURE);
} else {
msg_in.reset();
}
feed_machine_for(machine, 230, fr);
// feed in CM_SET_KEY_CNF
ctx.slac_message_payload = create_cm_set_key_cnf();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
// should be idle state now, send ENTER_BCD, to enter MATCHING
machine.handle_event(slac::fsm::evse::Event::ENTER_BCD);
fr = machine.feed();
feed_machine_for(machine, 300, fr);
// create session 1 and inject CM_SLAC_PARM_REQ
auto session_1 = EVSession({0, 1, 2, 3, 4, 5, 6, 7}, {0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe});
ctx.slac_message_payload = session_1.create_cm_slac_parm_req();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
// assert that CM_SLAC_PARM_CNF gets set!
if (!msg_in.has_value() ||
msg_in->get_mmtype() != (slac::defs::MMTYPE_CM_SLAC_PARAM | slac::defs::MMTYPE_MODE_CNF)) {
printf("Expected CM_SLAC_PARM_CNF!\n");
exit(EXIT_FAILURE);
} else {
msg_in.reset();
}
feed_machine_for(machine, 233, fr);
// inject CM_START_ATTEN_CHAR_IND
ctx.slac_message_payload = session_1.create_cm_start_atten_char_ind();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
// inject all the soundings ...
for (int i = 0; i < slac::defs::CM_SLAC_PARM_CNF_NUM_SOUNDS - 1; i++) {
ctx.slac_message_payload = session_1.create_cm_mnbc_sound_ind();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
ctx.slac_message_payload = session_1.create_cm_atten_profile_ind();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
}
feed_machine_for(machine, 700 + 1 * 45, fr);
// assert that CM_ATTEN_CHAR_IND gets set!
if (!msg_in.has_value() ||
msg_in->get_mmtype() != (slac::defs::MMTYPE_CM_ATTEN_CHAR | slac::defs::MMTYPE_MODE_IND)) {
printf("Expected CM_ATTEN_CHAR_IND!\n");
exit(EXIT_FAILURE);
} else {
auto atten_char_ind = msg_in->get_payload<slac::messages::cm_atten_char_ind>();
for (int i = 0; i < slac::defs::AAG_LIST_LEN; ++i) {
if (atten_char_ind.attenuation_profile.aag[i] != i + ATTENUATION_ADJUSTMENT) {
printf("Averaging not correct in ATTEN_CHAR_IND\n");
exit(EXIT_FAILURE);
}
}
msg_in.reset();
}
// "async" insert an CM_VALIDATE.REQ
ctx.slac_message_payload = create_cm_validate_req();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
// assert that CM_VALIDATE.CNF gets set!
if (!msg_in.has_value() || msg_in->get_mmtype() != (slac::defs::MMTYPE_CM_VALIDATE | slac::defs::MMTYPE_MODE_CNF)) {
printf("Expected CM_VALIDATE.CNF!\n");
exit(EXIT_FAILURE);
} else {
// check for correct "failure" result
auto validate_cnf = msg_in->get_payload<slac::messages::cm_validate_cnf>();
if (validate_cnf.result != slac::defs::CM_VALIDATE_REQ_RESULT_FAILURE) {
printf("Expected result field of CM_VALIDATE.CNF to be set to failure\n");
exit(EXIT_FAILURE);
}
}
// inject CM_ATTEN_CHAR_RSP
ctx.slac_message_payload = session_1.create_cm_atten_char_rsp();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
feed_machine_for(machine, 1000, fr);
// inject messages from a second session
auto session_2 = EVSession({9, 1, 2, 3, 4, 5, 6, 7}, {0xbe, 0xaf, 0xbe, 0xaf, 0xbe, 0xaf});
ctx.slac_message_payload = session_2.create_cm_slac_parm_req();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
feed_machine_for(machine, 1000, fr);
// inject CM_SLAC_MATCH_REQ
ctx.slac_message_payload = session_1.create_cm_slac_match_req();
machine.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
fr = machine.feed();
// assert that CM_SLAC_MATCH_CNF gets set!
if (!msg_in.has_value() ||
msg_in->get_mmtype() != (slac::defs::MMTYPE_CM_SLAC_MATCH | slac::defs::MMTYPE_MODE_CNF)) {
printf("Expected CM_ATTEN_CHAR_IND!\n");
exit(EXIT_FAILURE);
} else {
msg_in.reset();
}
return 0;
}

View File

@@ -0,0 +1,190 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include <array>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <thread>
#include <poll.h>
#include <sys/eventfd.h>
#include <unistd.h>
#include <everest/slac/fsm/ev/fsm.hpp>
#include <everest/slac/fsm/ev/states/others.hpp>
#include <everest/slac/fsm/evse/fsm.hpp>
#include <everest/slac/fsm/evse/states/others.hpp>
#include "plc_emu.hpp"
#include "socket_pair_bridge.hpp"
class EvseFsmController {
public:
explicit EvseFsmController(int evse_fd);
void trigger_enter_bcd();
void run();
private:
static constexpr uint64_t ENTER_BCD_EVENT_CODE = 1;
slac::fsm::evse::ContextCallbacks callbacks;
slac::fsm::evse::Context ctx{callbacks};
slac::fsm::evse::FSM fsm;
std::array<struct pollfd, 2> pollfds;
};
EvseFsmController::EvseFsmController(int evse_fd) :
pollfds({{
{evse_fd, POLLIN, 0},
{eventfd(0, 0), POLLIN, 0},
}}) {
ctx.slac_config.chip_reset.enabled = false;
callbacks.log = [](const std::string& msg) { printf("EVSE log: %s\n", msg.c_str()); };
callbacks.send_raw_slac = [evse_fd](slac::messages::HomeplugMessage& msg) {
write(evse_fd, msg.get_raw_message_ptr(), msg.get_raw_msg_len());
};
fsm.reset<slac::fsm::evse::ResetState>(ctx);
}
void EvseFsmController::trigger_enter_bcd() {
write(pollfds[1].fd, &ENTER_BCD_EVENT_CODE, sizeof(ENTER_BCD_EVENT_CODE));
}
void EvseFsmController::run() {
while (true) {
auto feed_result = fsm.feed();
if (feed_result.transition()) {
// call immediately again
continue;
} else if (feed_result.internal_error() || feed_result.unhandled_event()) {
throw std::runtime_error("Evse fsm: internal error / unhandled event");
// FIXME (aw): would need to log here!
}
const auto timeout = (feed_result.has_value()) ? *feed_result : -1;
if (timeout == 0) {
continue;
}
const auto poll_result = poll(pollfds.data(), pollfds.size(), timeout);
if (poll_result == 0) {
// timeout
continue;
}
// check for fds
if (pollfds[0].revents & POLLIN) {
auto raw_msg = ctx.slac_message_payload.get_raw_message_ptr();
read(pollfds[0].fd, raw_msg, sizeof(slac::messages::homeplug_message));
fsm.handle_event(slac::fsm::evse::Event::SLAC_MESSAGE);
}
if (pollfds[1].revents & POLLIN) {
uint64_t tmp;
read(pollfds[1].fd, &tmp, sizeof(tmp));
if (tmp == ENTER_BCD_EVENT_CODE) {
fsm.handle_event(slac::fsm::evse::Event::ENTER_BCD);
}
// new event, for now, we do not care, later on we could check, if it is an exit event code
}
}
}
class EvFsmController {
public:
explicit EvFsmController(int ev_fd);
void run();
private:
slac::fsm::ev::ContextCallbacks callbacks;
slac::fsm::ev::Context ctx{callbacks};
slac::fsm::ev::FSM fsm;
std::array<struct pollfd, 2> pollfds;
};
EvFsmController::EvFsmController(int ev_fd) {
pollfds = {{
{ev_fd, POLLIN, 0},
{eventfd(0, 0), POLLIN, 0},
}};
callbacks.log = [](const std::string& msg) { printf("EV log: %s\n", msg.c_str()); };
callbacks.send_raw_slac = [ev_fd](slac::messages::HomeplugMessage& msg) {
write(ev_fd, msg.get_raw_message_ptr(), msg.get_raw_msg_len());
};
fsm.reset<slac::fsm::ev::ResetState>(ctx);
}
void EvFsmController::run() {
// start connecting
fsm.handle_event(slac::fsm::ev::Event::TRIGGER_MATCHING);
while (true) {
auto feed_result = fsm.feed();
if (feed_result.transition()) {
// call immediately again
continue;
} else if (feed_result.internal_error() || feed_result.unhandled_event()) {
throw std::runtime_error("Evse fsm: internal error / unhandled event");
// FIXME (aw): would need to log here!
}
const auto timeout = (feed_result.has_value()) ? *feed_result : -1;
if (timeout == 0) {
continue;
}
const auto poll_result = poll(pollfds.data(), pollfds.size(), timeout);
if (poll_result == 0) {
// timeout
continue;
}
if (pollfds[0].revents & POLLIN) {
auto raw_msg = ctx.slac_message.get_raw_message_ptr();
read(pollfds[0].fd, raw_msg, sizeof(slac::messages::homeplug_message));
fsm.handle_event(slac::fsm::ev::Event::SLAC_MESSAGE);
}
if (pollfds[1].revents & POLLIN) {
uint64_t tmp;
read(pollfds[1].fd, &tmp, sizeof(tmp));
// new event, for now, we do not care, later on we could check, if it is an exit event code
}
}
}
auto main(int argc, char* argv[]) -> int {
SocketPairBridge spb{handle_ev_input, handle_evse_input};
EvseFsmController evse_ctrl{spb.get_evse_socket()};
EvFsmController ev_ctrl{spb.get_ev_socket()};
std::thread evse_thread(&EvseFsmController::run, &evse_ctrl);
// give the EVSE some time to get ready
std::this_thread::sleep_for(std::chrono::milliseconds(500));
evse_ctrl.trigger_enter_bcd();
std::thread ev_thread(&EvFsmController::run, &ev_ctrl);
// the fsm controller could poll on its fd by itself and look for new slac messages
evse_thread.join();
ev_thread.join();
// each controller needs to have a feeding thread, and an async input function for the slac message
return 0;
}

View File

@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include "plc_emu.hpp"
#include <cstdio>
#include <cstring>
#include <random>
#include <unistd.h>
#include <slac/slac.hpp>
// NOTE (aw): this is only intended to be used for this test, if multiple instances use these handle functions, they
// will race the buffer
static slac::messages::HomeplugMessage homeplug_message;
constexpr static uint8_t EV_MAC_ADDR[ETH_ALEN] = {0x00, 0x7d, 0xfa, 0x09, 0xfe, 0x76};
constexpr static uint8_t EVSE_MAC_ADDR[ETH_ALEN] = {0x6e, 0x3f, 0x46, 0x32, 0xbf, 0xc6};
constexpr static uint8_t PLC_SRC_MAC_ADDR[ETH_ALEN] = {0x00, 0x01, 0x87, 0x0e, 0xa3, 0x55};
static void handle_set_key_req(int origin_fd) {
slac::messages::cm_set_key_cnf set_key_cnf;
// FIXME (aw): proper message and mac header setup!
homeplug_message.setup_payload(&set_key_cnf, sizeof(set_key_cnf),
(slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_CNF),
slac::defs::MMV::AV_1_1);
auto raw = homeplug_message.get_raw_message_ptr();
memcpy(raw->ethernet_header.ether_dhost, raw->ethernet_header.ether_shost,
sizeof(raw->ethernet_header.ether_dhost));
memcpy(raw->ethernet_header.ether_shost, PLC_SRC_MAC_ADDR, sizeof(PLC_SRC_MAC_ADDR));
write(origin_fd, raw, homeplug_message.get_raw_msg_len());
}
static void attach_atten_profile(int evse_bridge_fd) {
slac::messages::cm_atten_profile_ind atten_profile;
memcpy(atten_profile.pev_mac, homeplug_message.get_src_mac(), sizeof(atten_profile.pev_mac));
atten_profile.num_groups = slac::defs::AAG_LIST_LEN;
std::random_device rnd_dev;
std::mt19937 rng(rnd_dev());
std::uniform_int_distribution<std::mt19937::result_type> db_dist(20, 27);
for (auto i = 0; i < atten_profile.num_groups; ++i) {
atten_profile.aag[i] = db_dist(rng);
}
homeplug_message.setup_payload(&atten_profile, sizeof(atten_profile),
(slac::defs::MMTYPE_CM_ATTEN_PROFILE | slac::defs::MMTYPE_MODE_IND),
slac::defs::MMV::AV_1_1);
auto raw = homeplug_message.get_raw_message_ptr();
memcpy(raw->ethernet_header.ether_shost, PLC_SRC_MAC_ADDR, sizeof(PLC_SRC_MAC_ADDR));
write(evse_bridge_fd, raw, homeplug_message.get_raw_msg_len());
}
void handle_ev_input(int ev_bridge_fd, int evse_bridge_fd) {
auto raw_hp_message = homeplug_message.get_raw_message_ptr();
auto bytes_read = read(ev_bridge_fd, raw_hp_message, sizeof(slac::messages::homeplug_message));
const auto mmtype = homeplug_message.get_mmtype();
// printf("EV send message of size %d and type 0x%hx\n", bytes_read, mmtype);
// patch in "our" mac address
memcpy(raw_hp_message->ethernet_header.ether_shost, EV_MAC_ADDR, sizeof(EV_MAC_ADDR));
if (mmtype == (slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_REQ)) {
handle_set_key_req(ev_bridge_fd);
} else {
// default: forward message
write(evse_bridge_fd, raw_hp_message, bytes_read);
}
if (mmtype == (slac::defs::MMTYPE_CM_MNBC_SOUND | slac::defs::MMTYPE_MODE_IND)) {
// also attach CM_ATTEN_PROFILE.IND
attach_atten_profile(evse_bridge_fd);
}
}
void handle_evse_input(int evse_bridge_fd, int ev_bridge_fd) {
auto raw_hp_message = homeplug_message.get_raw_message_ptr();
auto bytes_read = read(evse_bridge_fd, raw_hp_message, sizeof(slac::messages::homeplug_message));
const auto mmtype = homeplug_message.get_mmtype();
// patch in "our" mac address
memcpy(raw_hp_message->ethernet_header.ether_shost, EVSE_MAC_ADDR, sizeof(EVSE_MAC_ADDR));
if (mmtype == (slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_REQ)) {
handle_set_key_req(evse_bridge_fd);
} else {
// default: forward message
write(ev_bridge_fd, raw_hp_message, bytes_read);
}
}

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef TESTS_EVSE_VS_EV_PLC_EMU_HPP
#define TESTS_EVSE_VS_EV_PLC_EMU_HPP
#include "socket_pair_bridge.hpp"
void handle_ev_input(int ev_bridge_fd, int evse_bridge_fd);
void handle_evse_input(int evse_bridge_fd, int ev_bridge_fd);
#endif // TESTS_EVSE_VS_EV_PLC_EMU_HPP

View File

@@ -0,0 +1,100 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#include "socket_pair_bridge.hpp"
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <poll.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <sys/socket.h>
#include <unistd.h>
// FIXME (aw): this helper doesn't really belong here
static void exit_with_error(const char* msg) {
fprintf(stderr, "%s (%s)\n", msg, strerror(errno));
exit(-EXIT_FAILURE);
}
SocketPairBridge::SocketPairBridge(const SocketInputHandler& ev_input_handler,
const SocketInputHandler& evse_input_handler) :
handle_ev_input(ev_input_handler), handle_evse_input(evse_input_handler) {
std::array<int, 2> fd_pair;
auto ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fd_pair.data());
if (ret) {
exit_with_error("ev socketpair creation failed");
}
sockets.ev_fd = fd_pair.at(0);
sockets.ev_bridge_fd = fd_pair.at(1);
ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fd_pair.data());
if (ret) {
exit_with_error("evse socketpair creation failed");
}
sockets.evse_fd = fd_pair.at(0);
sockets.evse_bridge_fd = fd_pair.at(1);
event_fd = eventfd(0, 0);
if (event_fd == -1) {
exit_with_error("eventfd failed");
}
// NOTE (aw): using 'this' here is safe
loop_thread = std::thread(&SocketPairBridge::loop, this);
}
SocketPairBridge::~SocketPairBridge() {
uint64_t event_value = 0x1;
write(event_fd, &event_value, sizeof(event_value));
loop_thread.join();
close(event_fd);
close(sockets.evse_bridge_fd);
close(sockets.evse_fd);
close(sockets.ev_bridge_fd);
close(sockets.ev_fd);
}
void SocketPairBridge::loop() {
constexpr static auto EV_INDEX = 0;
constexpr static auto EVSE_INDEX = 1;
constexpr static auto ABORT_INDEX = 2;
struct pollfd pollfds[] = {
{sockets.ev_bridge_fd, POLLIN, 0},
{sockets.evse_bridge_fd, POLLIN, 0},
{event_fd, POLLIN, 0},
};
static constexpr auto num_fds = sizeof(pollfds) / sizeof(struct pollfd);
while (true) {
const auto status = poll(pollfds, num_fds, -1);
if (status == -1) {
exit_with_error("bridge poll");
}
if (pollfds[ABORT_INDEX].revents & POLLIN) {
uint64_t tmp;
read(event_fd, &tmp, sizeof(tmp));
printf("Received shutdown event, exiting\n");
return;
}
if (pollfds[EV_INDEX].revents & POLLIN) {
handle_ev_input(sockets.ev_bridge_fd, sockets.evse_bridge_fd);
}
if (pollfds[EVSE_INDEX].revents & POLLIN) {
handle_evse_input(sockets.evse_bridge_fd, sockets.ev_bridge_fd);
}
}
}

View File

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 - 2023 Pionix GmbH and Contributors to EVerest
#ifndef TESTS_EVSE_VS_EV_SOCKET_PAIR_BRIDGE_HPP
#define TESTS_EVSE_VS_EV_SOCKET_PAIR_BRIDGE_HPP
#include <cstdint>
#include <functional>
#include <thread>
class SocketPairBridge {
public:
struct Sockets {
int ev_fd;
int ev_bridge_fd;
int evse_fd;
int evse_bridge_fd;
};
using SocketInputHandler = std::function<void(int, int)>;
SocketPairBridge(const SocketInputHandler& ev_input_handler, const SocketInputHandler& evse_input_handler);
// FIXME (aw): disable copy constructors!
int get_ev_socket() {
return sockets.ev_fd;
};
int get_evse_socket() {
return sockets.evse_fd;
}
~SocketPairBridge();
private:
SocketInputHandler handle_ev_input;
SocketInputHandler handle_evse_input;
void loop();
Sockets sockets{};
int event_fd;
std::thread loop_thread;
uint8_t transfer_buffer[1024];
};
#endif // TESTS_EVSE_VS_EV_SOCKET_PAIR_BRIDGE_HPP

View File

@@ -0,0 +1,24 @@
set(TEST_TARGET_NAME slac_unit_test)
add_executable(${TEST_TARGET_NAME} libslac_unit_test.cpp)
target_include_directories(${TEST_TARGET_NAME} PUBLIC${GTEST_INCLUDE_DIRS})
if(DISABLE_EDM)
find_package(GTest REQUIRED)
else()
set(GTEST_LIBRARIES
GTest::gmock
GTest::gtest
GTest::gtest_main
)
endif()
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
${GTEST_LIBRARIES}
slac::slac
)
include(GoogleTest)
gtest_discover_tests(${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})

View File

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <slac/slac.hpp>
#include <stdexcept>
namespace libslac {
class LibSLACUnitTest : public ::testing::Test {
protected:
void SetUp() override {
}
void TearDown() override {
}
};
TEST_F(LibSLACUnitTest, test_generate_nmk_hs) {
uint8_t nmk_hs[slac::defs::NMK_LEN] = {0};
std::string plain_password = "EVerest";
slac::utils::generate_nmk_hs(nmk_hs, plain_password.c_str(), plain_password.length());
ASSERT_THAT(nmk_hs, testing::ElementsAre(0xf7, 0x0f, 0x4e, 0x02, 0x2b, 0xb1, 0x08, 0x1b, 0xcb, 0xa1, 0xa0, 0x9d,
0xf6, 0xe0, 0x8f, 0x6b));
}
TEST_F(LibSLACUnitTest, test_generate_nid_from_nmk) {
uint8_t nid[slac::defs::NID_LEN] = {0};
const uint8_t sample_nmk[] = {0x34, 0x52, 0x23, 0x54, 0x45, 0xae, 0xf2, 0xd4,
0x55, 0xfe, 0xff, 0x31, 0xa3, 0xb3, 0x03, 0xad};
slac::utils::generate_nid_from_nmk(nid, sample_nmk);
ASSERT_THAT(nid, testing::ElementsAre(0xb1, 0xc9, 0x13, 0xb5, 0xaa, 0x07, 0x02));
}
TEST_F(LibSLACUnitTest, test_generate_nid_from_nmk_check_security_bits) {
uint8_t nid[slac::defs::NID_LEN] = {0};
const uint8_t sample_nmk[] = {0x34, 0x52, 0x23, 0x54, 0x45, 0xae, 0xf2, 0xd4,
0x55, 0xfe, 0xff, 0x31, 0xa3, 0xb3, 0x03, 0xad};
slac::utils::generate_nid_from_nmk(nid, sample_nmk);
ASSERT_TRUE((nid[6] >> 4) == 0x00);
}
TEST_F(LibSLACUnitTest, test_setup_payload) {
slac::messages::cm_set_key_cnf set_key_cnf;
slac::messages::HomeplugMessage message;
ASSERT_THROW(message.setup_payload(&set_key_cnf, 2000, // this is way too large
(slac::defs::MMTYPE_CM_SET_KEY | slac::defs::MMTYPE_MODE_CNF),
slac::defs::MMV::AV_1_1),
std::runtime_error);
}
} // namespace libslac