use alloc::vec::Vec;
use core::fmt;
use crate::session::{Instant, Millis};
use crate::trace::{trace_close, trace_event};
use super::{
APPLICATION_TYPE_PEV_EVSE, AttenCharInd, AttenCharRsp, AttenProfile, BROADCAST, MacAddr,
Mmtype, Mmv, MnbcSoundInd, NID_LEN, NMK_LEN, RunId, SlacError, SlacMatchCnf, SlacMatchReq,
SlacParmCnf, SlacParmReq, StartAttenCharInd, StationId, timers,
};
const MMV: Mmv = Mmv::Av1_1;
#[allow(unused_variables, reason = "`reason` is only read by the tracing macro")]
fn accept_frame(raw: &[u8]) -> Option<super::Frame<'_>> {
match super::parse_frame(raw) {
Ok(f) => Some(f),
Err(reason) => {
trace_event!(reason = %reason, "frame dropped");
None
}
}
}
#[allow(unused_variables, reason = "both are only read by the tracing macro")]
fn decoded<T>(result: Result<T, SlacError>, mmtype: Mmtype) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(reason) => {
trace_event!(message = ?mmtype, reason = %reason, "frame dropped");
None
}
}
}
const RESP_TYPE: u8 = 0x01;
pub const MAX_STATIONS: usize = 32;
pub const MAX_PENDING_FRAMES: usize = 16;
pub const MAX_PENDING_EVENTS: usize = 64;
pub const MAX_FRAME_LEN: usize = 192;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outgoing {
pub destination: MacAddr,
pub frame: Vec<u8>,
}
fn frame(
destination: MacAddr,
source: MacAddr,
mmtype: Mmtype,
encode: impl Fn(&mut [u8]) -> Result<usize, SlacError>,
) -> Result<Outgoing, SlacError> {
let mut payload = [0u8; MAX_FRAME_LEN];
let n = encode(&mut payload)?;
let mut out = alloc::vec![0u8; MAX_FRAME_LEN];
let len = super::write_frame(&mut out, destination, source, MMV, mmtype, &payload[..n])?;
out.truncate(len);
Ok(Outgoing { destination, frame: out })
}
#[cfg(test)]
mod frame_size {
use super::{MAX_FRAME_LEN, MMV};
use crate::slac::{AttenCharInd, ETHERNET_HEADER_LEN};
#[test]
fn the_longest_message_fits_with_room_to_spare() {
let needed = ETHERNET_HEADER_LEN + 3 + MMV.fragmentation_len() + AttenCharInd::LEN;
assert!(needed <= MAX_FRAME_LEN, "{needed} bytes needed, {MAX_FRAME_LEN} available");
}
}
#[derive(Debug, Clone)]
pub struct EvseConfig {
pub mac: MacAddr,
pub id: StationId,
pub nmk: [u8; NMK_LEN],
pub nid: [u8; NID_LEN],
pub attenuation_limit: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvseEvent {
RunStarted(RunId),
Measured {
mean_attenuation: u8,
sounds: u8,
},
Matched {
pev_id: StationId,
pev_mac: MacAddr,
nmk: [u8; NMK_LEN],
},
Failed(Reason),
}
#[derive(Debug)]
pub struct Evse {
config: EvseConfig,
state: EvseState,
run: Option<Run>,
measured: Option<AttenProfile>,
deadline: Option<Instant>,
out: Vec<Outgoing>,
events: Vec<EvseEvent>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EvseState {
Idle,
Sounding,
Measured,
Matched,
Failed,
}
#[derive(Debug, Clone)]
struct Run {
id: RunId,
pev_mac: MacAddr,
pev_id: StationId,
sounds: u8,
expected_sounds: u8,
}
impl Evse {
#[must_use]
pub fn new(config: EvseConfig) -> Self {
Self {
config,
state: EvseState::Idle,
run: None,
measured: None,
deadline: None,
out: Vec::new(),
events: Vec::new(),
}
}
pub fn handle_frame(&mut self, now: Instant, raw: &[u8]) {
let Some(f) = accept_frame(raw) else { return };
match f.mmtype {
Mmtype::SlacParmReq => {
if let Some(m) = decoded(SlacParmReq::decode(f.payload), f.mmtype) {
self.on_parm_req(now, f.source, m);
}
}
Mmtype::StartAttenCharInd => {
if let Some(m) = decoded(StartAttenCharInd::decode(f.payload), f.mmtype) {
self.on_start_atten(now, f.source, &m);
}
}
Mmtype::MnbcSoundInd => {
if let Some(m) = decoded(MnbcSoundInd::decode(f.payload), f.mmtype) {
self.on_sound(now, f.source, &m);
}
}
Mmtype::AttenCharRsp => {
if let Some(m) = decoded(AttenCharRsp::decode(f.payload), f.mmtype) {
self.on_atten_rsp(f.source, &m);
}
}
Mmtype::SlacMatchReq => {
if let Some(m) = decoded(SlacMatchReq::decode(f.payload), f.mmtype) {
self.on_match_req(f.source, &m);
}
}
_ => {}
}
}
fn on_parm_req(&mut self, now: Instant, source: MacAddr, req: SlacParmReq) {
if self.state == EvseState::Matched {
return;
}
if let Some(run) = &self.run
&& matches!(self.state, EvseState::Sounding | EvseState::Measured)
&& (run.pev_mac != source || run.id != req.run_id)
{
return;
}
self.run = Some(Run {
id: req.run_id,
pev_mac: source,
pev_id: [0; super::STATION_ID_LEN],
sounds: 0,
expected_sounds: timers::C_EV_MATCH_MNBC,
});
self.state = EvseState::Sounding;
self.note(EvseEvent::RunStarted(req.run_id));
let cnf = SlacParmCnf {
m_sound_target: BROADCAST,
num_sounds: timers::C_EV_MATCH_MNBC,
timeout: u8::try_from(timers::TT_EVSE_MATCH_MNBC_MS / 100).unwrap_or(6),
resp_type: RESP_TYPE,
forwarding_sta: source,
run_id: req.run_id,
};
self.queue(source, Mmtype::SlacParmCnf, |b| cnf.encode(b));
self.arm(now, Millis::from_millis(u64::from(timers::TT_EVSE_MATCH_SESSION_MS)));
}
fn on_start_atten(&mut self, now: Instant, source: MacAddr, ind: &StartAttenCharInd) {
let Some(run) = self.run.as_mut().filter(|r| r.id == ind.run_id && r.pev_mac == source)
else {
return;
};
run.expected_sounds = ind.num_sounds;
self.arm(now, Millis::from_millis(u64::from(timers::TT_EVSE_MATCH_MNBC_MS)));
}
fn on_sound(&mut self, now: Instant, source: MacAddr, ind: &MnbcSoundInd) {
let Some(run) = self.run.as_mut().filter(|r| r.id == ind.run_id && r.pev_mac == source)
else {
return;
};
run.pev_id = ind.sender_id;
run.sounds = run.sounds.saturating_add(1);
if run.sounds >= run.expected_sounds {
self.finish_sounding(now);
}
}
pub fn observe(&mut self, profile: &AttenProfile) {
self.measured = Some(*profile);
}
fn finish_sounding(&mut self, now: Instant) {
if self.state != EvseState::Sounding {
return;
}
let Some(run) = self.run.clone() else { return };
let Some(profile) = self.measured else {
self.fail(Reason::NoMeasurement);
return;
};
let mean = profile.mean_attenuation().unwrap_or(u8::MAX);
if self.config.attenuation_limit.is_some_and(|limit| mean > limit) {
self.fail(Reason::TooFarAway);
return;
}
self.state = EvseState::Measured;
trace_event!(attenuation_db = mean, sounds = run.sounds, "sounding measured");
self.note(EvseEvent::Measured { mean_attenuation: mean, sounds: run.sounds });
let ind = AttenCharInd {
source_address: run.pev_mac,
run_id: run.id,
source_id: run.pev_id,
resp_id: self.config.id,
num_sounds: run.sounds,
profile,
};
self.queue(run.pev_mac, Mmtype::AttenCharInd, |b| ind.encode(b));
self.arm(now, Millis::from_millis(u64::from(timers::TT_EVSE_MATCH_SESSION_MS)));
}
fn on_atten_rsp(&mut self, source: MacAddr, rsp: &AttenCharRsp) {
if self.run.as_ref().is_none_or(|r| r.id != rsp.run_id || r.pev_mac != source) {
return;
}
if rsp.resp_id != self.config.id {
return;
}
if rsp.result != 0 {
self.fail(Reason::Rejected);
}
}
fn on_match_req(&mut self, source: MacAddr, req: &SlacMatchReq) {
let Some(run) = self.run.clone().filter(|r| r.id == req.run_id && r.pev_mac == source)
else {
return;
};
if req.evse_mac != self.config.mac || req.evse_id != self.config.id {
return;
}
if self.state != EvseState::Measured {
self.fail(Reason::OutOfSequence);
return;
}
let cnf = SlacMatchCnf {
pev_id: req.pev_id,
pev_mac: req.pev_mac,
evse_id: self.config.id,
evse_mac: self.config.mac,
run_id: run.id,
nid: self.config.nid,
nmk: self.config.nmk,
};
self.queue(req.pev_mac, Mmtype::SlacMatchCnf, |b| cnf.encode(b));
self.state = EvseState::Matched;
self.deadline = None;
self.events.push(EvseEvent::Matched {
pev_id: req.pev_id,
pev_mac: req.pev_mac,
nmk: self.config.nmk,
});
}
#[must_use]
pub const fn poll_timeout(&self) -> Option<Instant> {
self.deadline
}
pub fn handle_timeout(&mut self, now: Instant) {
if self.deadline.is_some_and(|d| d <= now) {
self.deadline = None;
match self.state {
EvseState::Sounding if self.run.as_ref().is_some_and(|r| r.sounds > 0) => {
self.finish_sounding(now);
}
EvseState::Sounding | EvseState::Measured => self.fail(Reason::Timeout),
EvseState::Idle | EvseState::Matched | EvseState::Failed => {}
}
}
}
pub fn poll_transmit(&mut self) -> Option<Outgoing> {
if self.out.is_empty() { None } else { Some(self.out.remove(0)) }
}
pub fn poll_event(&mut self) -> Option<EvseEvent> {
if self.events.is_empty() { None } else { Some(self.events.remove(0)) }
}
#[must_use]
pub const fn is_matched(&self) -> bool {
matches!(self.state, EvseState::Matched)
}
pub fn reset(&mut self) {
self.state = EvseState::Idle;
self.run = None;
self.measured = None;
self.deadline = None;
self.out.clear();
}
fn queue(
&mut self,
to: MacAddr,
mmtype: Mmtype,
encode: impl Fn(&mut [u8]) -> Result<usize, SlacError>,
) {
match frame(to, self.config.mac, mmtype, encode) {
Ok(out) => {
if self.out.len() < MAX_PENDING_FRAMES {
self.out.push(out);
}
}
Err(_) => self.fail(Reason::EncodingFailed),
}
}
fn arm(&mut self, now: Instant, after: Millis) {
self.deadline = Some(now + after);
}
fn note(&mut self, event: EvseEvent) {
if self.events.len() < MAX_PENDING_EVENTS {
self.events.push(event);
}
}
fn fail(&mut self, reason: Reason) {
if self.state == EvseState::Failed {
return;
}
self.state = EvseState::Failed;
self.deadline = None;
trace_close!(reason = %reason, "matching failed");
self.events.push(EvseEvent::Failed(reason));
}
}
#[derive(Debug, Clone)]
pub struct EvConfig {
pub mac: MacAddr,
pub id: StationId,
pub run_id: RunId,
pub sounding_payload: [u8; 16],
pub attenuation_limit: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvEvent {
StationFound(MacAddr),
Measurement {
evse_mac: MacAddr,
mean_attenuation: u8,
},
Matched {
evse_mac: MacAddr,
evse_id: StationId,
nmk: [u8; NMK_LEN],
nid: [u8; NID_LEN],
},
Failed(Reason),
}
#[derive(Debug)]
pub struct Ev {
config: EvConfig,
state: EvState,
best: Option<(MacAddr, StationId, u8)>,
answered: Vec<MacAddr>,
remaining_sounds: u8,
remaining_starts: u8,
deadline: Option<Instant>,
results_deadline: Option<Instant>,
out: Vec<Outgoing>,
events: Vec<EvEvent>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EvState {
Idle,
Probing,
Sounding,
AwaitingMeasurements,
AwaitingKey,
Matched,
Failed,
}
impl Ev {
#[must_use]
pub fn new(config: EvConfig) -> Self {
Self {
config,
state: EvState::Idle,
best: None,
answered: Vec::new(),
remaining_sounds: 0,
remaining_starts: 0,
deadline: None,
results_deadline: None,
out: Vec::new(),
events: Vec::new(),
}
}
pub fn start(&mut self, now: Instant) {
self.best = None;
self.answered.clear();
self.remaining_sounds = 0;
self.remaining_starts = 0;
self.out.clear();
self.results_deadline = None;
let req = SlacParmReq { run_id: self.config.run_id };
self.queue(BROADCAST, Mmtype::SlacParmReq, |b| req.encode(b));
self.state = EvState::Probing;
self.arm(now, Millis::from_millis(u64::from(timers::TT_MATCH_RESPONSE_MS)));
}
pub fn handle_frame(&mut self, now: Instant, raw: &[u8]) {
let Some(f) = accept_frame(raw) else { return };
match f.mmtype {
Mmtype::SlacParmCnf => {
let Some(cnf) = decoded(SlacParmCnf::decode(f.payload), f.mmtype) else { return };
if cnf.run_id == self.config.run_id
&& cnf.forwarding_sta == self.config.mac
&& !self.answered.contains(&f.source)
&& self.answered.len() < MAX_STATIONS
{
self.answered.push(f.source);
self.note(EvEvent::StationFound(f.source));
}
}
Mmtype::AttenCharInd => {
let Some(ind) = decoded(AttenCharInd::decode(f.payload), f.mmtype) else { return };
if ind.run_id == self.config.run_id
&& ind.source_address == self.config.mac
&& ind.source_id == self.config.id
{
self.on_measurement(now, f.source, &ind);
}
}
Mmtype::SlacMatchCnf => {
if let Some(cnf) = decoded(SlacMatchCnf::decode(f.payload), f.mmtype) {
self.on_match_cnf(f.source, &cnf);
}
}
_ => {}
}
}
fn on_match_cnf(&mut self, source: MacAddr, cnf: &SlacMatchCnf) {
if self.state != EvState::AwaitingKey || cnf.run_id != self.config.run_id {
return;
}
let Some((chosen_mac, chosen_id, _)) = self.best else { return };
if source != chosen_mac || cnf.evse_mac != chosen_mac || cnf.evse_id != chosen_id {
return;
}
if cnf.pev_mac != self.config.mac || cnf.pev_id != self.config.id {
return;
}
self.state = EvState::Matched;
self.deadline = None;
self.events.push(EvEvent::Matched {
evse_mac: cnf.evse_mac,
evse_id: cnf.evse_id,
nmk: cnf.nmk,
nid: cnf.nid,
});
}
fn on_measurement(&mut self, now: Instant, source: MacAddr, ind: &AttenCharInd) {
let Some(mean) = ind.profile.mean_attenuation() else { return };
self.note(EvEvent::Measurement { evse_mac: source, mean_attenuation: mean });
if self.config.attenuation_limit.is_some_and(|limit| mean > limit) {
return;
}
if self.best.is_none_or(|(_, _, best)| mean < best) {
self.best = Some((source, ind.resp_id, mean));
}
if self.state == EvState::AwaitingMeasurements {
let next =
now.saturating_add(Millis::from_millis(u64::from(timers::TT_MATCH_RESPONSE_MS)));
self.deadline = Some(match self.results_deadline {
Some(end) if end < next => end,
_ => next,
});
}
}
#[must_use]
pub const fn poll_timeout(&self) -> Option<Instant> {
self.deadline
}
pub fn handle_timeout(&mut self, now: Instant) {
if self.deadline.is_none_or(|d| d > now) {
return;
}
self.deadline = None;
match self.state {
EvState::Probing => {
if self.answered.is_empty() {
self.fail(Reason::NoStation);
} else {
self.begin_sounding(now);
}
}
EvState::Sounding => self.send_next_sound(now),
EvState::AwaitingMeasurements => self.choose(now),
EvState::AwaitingKey => self.fail(Reason::Timeout),
EvState::Idle | EvState::Matched | EvState::Failed => {}
}
}
fn begin_sounding(&mut self, now: Instant) {
self.remaining_starts = timers::C_EV_START_ATTEN_CHAR_INDS;
self.remaining_sounds = timers::C_EV_MATCH_MNBC;
self.state = EvState::Sounding;
self.send_next_sound(now);
}
fn send_next_sound(&mut self, now: Instant) {
let interval = Millis::from_millis(u64::from(timers::TP_EV_BATCH_MSG_INTERVAL_MS));
if self.remaining_starts > 0 {
self.remaining_starts -= 1;
let ind = StartAttenCharInd {
num_sounds: timers::C_EV_MATCH_MNBC,
timeout: u8::try_from(timers::TT_EVSE_MATCH_MNBC_MS / 100).unwrap_or(6),
resp_type: RESP_TYPE,
forwarding_sta: self.config.mac,
run_id: self.config.run_id,
};
self.queue(BROADCAST, Mmtype::StartAttenCharInd, |b| ind.encode(b));
self.arm(now, interval);
return;
}
if self.remaining_sounds > 0 {
self.remaining_sounds -= 1;
let ind = MnbcSoundInd {
sender_id: self.config.id,
remaining_sound_count: self.remaining_sounds,
run_id: self.config.run_id,
random: self.config.sounding_payload,
};
self.queue(BROADCAST, Mmtype::MnbcSoundInd, |b| ind.encode(b));
self.arm(now, interval);
return;
}
self.state = EvState::AwaitingMeasurements;
self.arm(now, Millis::from_millis(u64::from(timers::TT_EV_ATTEN_RESULTS_MS)));
self.results_deadline = self.deadline;
}
fn choose(&mut self, now: Instant) {
let Some((evse_mac, evse_id, _)) = self.best else {
self.fail(Reason::TooFarAway);
return;
};
let rsp = AttenCharRsp {
source_address: self.config.mac,
run_id: self.config.run_id,
source_id: self.config.id,
resp_id: evse_id,
result: 0,
};
self.queue(evse_mac, Mmtype::AttenCharRsp, |b| rsp.encode(b));
let req = SlacMatchReq {
pev_id: self.config.id,
pev_mac: self.config.mac,
evse_id,
evse_mac,
run_id: self.config.run_id,
};
self.queue(evse_mac, Mmtype::SlacMatchReq, |b| req.encode(b));
self.state = EvState::AwaitingKey;
self.arm(now, Millis::from_millis(u64::from(timers::TT_MATCH_JOIN_MS)));
}
pub fn poll_transmit(&mut self) -> Option<Outgoing> {
if self.out.is_empty() { None } else { Some(self.out.remove(0)) }
}
pub fn poll_event(&mut self) -> Option<EvEvent> {
if self.events.is_empty() { None } else { Some(self.events.remove(0)) }
}
#[must_use]
pub const fn is_matched(&self) -> bool {
matches!(self.state, EvState::Matched)
}
#[must_use]
pub fn stations(&self) -> &[MacAddr] {
&self.answered
}
fn queue(
&mut self,
to: MacAddr,
mmtype: Mmtype,
encode: impl Fn(&mut [u8]) -> Result<usize, SlacError>,
) {
match frame(to, self.config.mac, mmtype, encode) {
Ok(out) => {
if self.out.len() < MAX_PENDING_FRAMES {
self.out.push(out);
}
}
Err(_) => self.fail(Reason::EncodingFailed),
}
}
fn arm(&mut self, now: Instant, after: Millis) {
self.deadline = Some(now + after);
}
fn note(&mut self, event: EvEvent) {
if self.events.len() < MAX_PENDING_EVENTS {
self.events.push(event);
}
}
fn fail(&mut self, reason: Reason) {
if self.state == EvState::Failed {
return;
}
self.state = EvState::Failed;
self.deadline = None;
trace_close!(reason = %reason, "matching failed");
self.events.push(EvEvent::Failed(reason));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Reason {
NoStation,
TooFarAway,
Timeout,
OutOfSequence,
Rejected,
NoMeasurement,
EncodingFailed,
}
impl fmt::Display for Reason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NoStation => "no charging station answered",
Self::TooFarAway => "no station was close enough",
Self::Timeout => "the peer stopped answering",
Self::OutOfSequence => "the key was requested before the measurement",
Self::Rejected => "the peer rejected the run",
Self::NoMeasurement => "the modem reported no attenuation measurement",
Self::EncodingFailed => "a SLAC message could not be encoded",
})
}
}
pub const APPLICATION_TYPE: u8 = APPLICATION_TYPE_PEV_EVSE;