pub mod schedule;
use crate::iso2::{
ChargeProgress, ChargingSession, EnergyTransferMode, PaymentOption, ResponseCode,
};
use super::SequenceError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Request {
SessionSetup,
ServiceDiscovery,
ServiceDetail,
PaymentServiceSelection(PaymentOption),
CertificateInstallation,
CertificateUpdate,
PaymentDetails,
Authorization,
ChargeParameterDiscovery(EnergyTransferMode),
CableCheck,
PreCharge,
PowerDelivery(ChargeProgress),
ChargingStatus,
CurrentDemand,
MeteringReceipt,
WeldingDetection,
SessionStop(ChargingSession),
}
impl Request {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::SessionSetup => "SessionSetupReq",
Self::ServiceDiscovery => "ServiceDiscoveryReq",
Self::ServiceDetail => "ServiceDetailReq",
Self::PaymentServiceSelection(_) => "PaymentServiceSelectionReq",
Self::CertificateInstallation => "CertificateInstallationReq",
Self::CertificateUpdate => "CertificateUpdateReq",
Self::PaymentDetails => "PaymentDetailsReq",
Self::Authorization => "AuthorizationReq",
Self::ChargeParameterDiscovery(_) => "ChargeParameterDiscoveryReq",
Self::CableCheck => "CableCheckReq",
Self::PreCharge => "PreChargeReq",
Self::PowerDelivery(_) => "PowerDeliveryReq",
Self::ChargingStatus => "ChargingStatusReq",
Self::CurrentDemand => "CurrentDemandReq",
Self::MeteringReceipt => "MeteringReceiptReq",
Self::WeldingDetection => "WeldingDetectionReq",
Self::SessionStop(_) => "SessionStopReq",
}
}
#[must_use]
pub fn of(message: &crate::message::Message) -> Option<Self> {
let crate::message::Message::Iso2(doc) = message else { return None };
let crate::iso2::Document::V2GMessage(v2g) = &**doc else { return None };
Self::of_body(v2g.body.choice.as_ref()?)
}
#[must_use]
pub fn of_body(body: &crate::iso2::BodyChoice) -> Option<Self> {
use crate::iso2::BodyChoice as B;
Some(match body {
B::SessionSetupReq(_) => Self::SessionSetup,
B::ServiceDiscoveryReq(_) => Self::ServiceDiscovery,
B::ServiceDetailReq(_) => Self::ServiceDetail,
B::PaymentServiceSelectionReq(req) => {
Self::PaymentServiceSelection(req.selected_payment_option)
}
B::CertificateInstallationReq(_) => Self::CertificateInstallation,
B::CertificateUpdateReq(_) => Self::CertificateUpdate,
B::PaymentDetailsReq(_) => Self::PaymentDetails,
B::AuthorizationReq(_) => Self::Authorization,
B::ChargeParameterDiscoveryReq(req) => {
Self::ChargeParameterDiscovery(req.requested_energy_transfer_mode)
}
B::CableCheckReq(_) => Self::CableCheck,
B::PreChargeReq(_) => Self::PreCharge,
B::PowerDeliveryReq(req) => Self::PowerDelivery(req.charge_progress),
B::ChargingStatusReq(_) => Self::ChargingStatus,
B::CurrentDemandReq(_) => Self::CurrentDemand,
B::MeteringReceiptReq(_) => Self::MeteringReceipt,
B::WeldingDetectionReq(_) => Self::WeldingDetection,
B::SessionStopReq(req) => Self::SessionStop(req.charging_session),
_ => return None,
})
}
#[must_use]
pub const fn response_timeout(self) -> super::Millis {
use super::timers::iso2 as t;
match self {
Self::CurrentDemand => t::MSG_TIMEOUT_CURRENT_DEMAND,
Self::ServiceDetail
| Self::PaymentDetails
| Self::PowerDelivery(_)
| Self::CertificateInstallation
| Self::CertificateUpdate => t::MSG_TIMEOUT_BACKEND,
_ => t::MSG_TIMEOUT_DEFAULT,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Phase {
Start,
SessionSetup,
ServiceDiscovery,
ServiceSelected,
PaymentDetails,
Authorized,
ChargeParameters,
CableCheck,
PreCharge,
Charging,
PowerStopped,
WeldingDetection,
Paused,
Stopped,
}
impl Phase {
#[must_use]
pub const fn loop_timeout(self) -> Option<super::Millis> {
use super::timers::iso2 as t;
Some(match self {
Self::CableCheck => t::EVCC_CABLE_CHECK_TIMEOUT,
Self::PreCharge => t::EVCC_PRE_CHARGE_TIMEOUT,
Self::Authorized | Self::ChargeParameters | Self::WeldingDetection => {
t::EVCC_ONGOING_TIMEOUT
}
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Transfer {
Ac,
Dc,
}
impl From<EnergyTransferMode> for Transfer {
fn from(mode: EnergyTransferMode) -> Self {
match mode {
EnergyTransferMode::ACSinglePhaseCore | EnergyTransferMode::ACThreePhaseCore => {
Self::Ac
}
_ => Self::Dc,
}
}
}
#[derive(Debug, Clone)]
pub struct Sequencer {
phase: Phase,
transfer: Option<Transfer>,
payment: Option<PaymentOption>,
renegotiated: bool,
failed: bool,
}
impl Default for Sequencer {
fn default() -> Self {
Self::new()
}
}
impl Sequencer {
#[must_use]
pub const fn new() -> Self {
Self {
phase: Phase::Start,
transfer: None,
payment: None,
renegotiated: false,
failed: false,
}
}
pub const fn failed(&mut self) {
self.failed = true;
}
#[must_use]
pub const fn is_failed(&self) -> bool {
self.failed
}
#[must_use]
pub const fn phase(&self) -> Phase {
self.phase
}
#[must_use]
pub const fn transfer(&self) -> Option<Transfer> {
self.transfer
}
#[must_use]
pub const fn payment(&self) -> Option<PaymentOption> {
self.payment
}
#[must_use]
pub const fn is_finished(&self) -> bool {
matches!(self.phase, Phase::Stopped | Phase::Paused)
}
#[must_use]
pub const fn is_paused(&self) -> bool {
matches!(self.phase, Phase::Paused)
}
#[must_use]
pub const fn has_renegotiated(&self) -> bool {
self.renegotiated
}
pub fn accept(&mut self, request: Request) -> Result<Phase, SequenceError> {
let Some(next) = self.next_phase(request) else {
return Err(self.refusal(request));
};
match request {
Request::PaymentServiceSelection(option) => self.payment = Some(option),
Request::ChargeParameterDiscovery(mode) => self.transfer = Some(mode.into()),
Request::PowerDelivery(ChargeProgress::Renegotiate) => self.renegotiated = true,
_ => {}
}
self.phase = next;
Ok(next)
}
#[must_use]
pub fn permits(&self, request: Request) -> bool {
self.next_phase(request).is_some()
}
#[must_use]
pub const fn refusal(&self, request: Request) -> SequenceError {
SequenceError {
got: request.name(),
phase: self.phase_name(),
response_code: ResponseCode::FAILEDSequenceError as u8,
}
}
#[allow(
clippy::match_same_arms,
reason = "one arm per ordering rule; merging them by target phase would \
hide which rule is which"
)]
fn next_phase(&self, request: Request) -> Option<Phase> {
use ChargeProgress as P;
use Phase as F;
use Request as R;
use ChargingSession as S;
let dc = self.transfer == Some(Transfer::Dc);
let contract = self.payment == Some(PaymentOption::Contract);
if self.failed {
return match (self.phase, request) {
(F::Start | F::Stopped | F::Paused, _) => None,
(_, R::SessionStop(S::Terminate)) => Some(F::Stopped),
(_, R::SessionStop(S::Pause)) => Some(F::Paused),
_ => None,
};
}
Some(match (self.phase, request) {
(F::Start, R::SessionSetup) => F::SessionSetup,
(F::SessionSetup, R::ServiceDiscovery) => F::ServiceDiscovery,
(F::ServiceDiscovery, R::ServiceDetail) => F::ServiceDiscovery,
(F::ServiceDiscovery, R::PaymentServiceSelection(_)) => F::ServiceSelected,
(F::ServiceSelected, R::CertificateInstallation | R::CertificateUpdate) if contract => {
F::ServiceSelected
}
(F::ServiceSelected, R::PaymentDetails) if contract => F::PaymentDetails,
(F::ServiceSelected, R::Authorization) if !contract => F::Authorized,
(F::PaymentDetails, R::Authorization) => F::Authorized,
(F::Authorized, R::Authorization) => F::Authorized,
(F::Authorized, R::ChargeParameterDiscovery(_)) => F::ChargeParameters,
(F::ChargeParameters, R::ChargeParameterDiscovery(_)) => F::ChargeParameters,
(F::ChargeParameters, R::CableCheck) if dc => F::CableCheck,
(F::ChargeParameters, R::PowerDelivery(P::Start)) if !dc || self.renegotiated => {
F::Charging
}
(F::CableCheck, R::CableCheck) => F::CableCheck,
(F::CableCheck, R::PreCharge) => F::PreCharge,
(F::PreCharge, R::PreCharge) => F::PreCharge,
(F::PreCharge, R::PowerDelivery(P::Start)) => F::Charging,
(F::Charging, R::ChargingStatus) if !dc => F::Charging,
(F::Charging, R::CurrentDemand) if dc => F::Charging,
(F::Charging, R::MeteringReceipt) if contract => F::Charging,
(F::Charging, R::PowerDelivery(P::Renegotiate)) => F::ChargeParameters,
(F::Charging, R::PowerDelivery(P::Stop)) => F::PowerStopped,
(F::PowerStopped, R::WeldingDetection) if dc => F::WeldingDetection,
(F::WeldingDetection, R::WeldingDetection) => F::WeldingDetection,
(F::Start | F::Stopped | F::Paused, R::SessionStop(_)) => return None,
(_, R::SessionStop(S::Terminate)) => F::Stopped,
(_, R::SessionStop(S::Pause)) => F::Paused,
_ => return None,
})
}
#[must_use]
pub const fn loop_timeout(&self) -> Option<super::Millis> {
self.phase.loop_timeout()
}
#[must_use]
pub const fn phase_name(&self) -> &'static str {
match self.phase {
Phase::Start => "Start",
Phase::SessionSetup => "SessionSetup",
Phase::ServiceDiscovery => "ServiceDiscovery",
Phase::ServiceSelected => "ServiceSelected",
Phase::PaymentDetails => "PaymentDetails",
Phase::Authorized => "Authorized",
Phase::ChargeParameters => "ChargeParameters",
Phase::CableCheck => "CableCheck",
Phase::PreCharge => "PreCharge",
Phase::Charging => "Charging",
Phase::PowerStopped => "PowerStopped",
Phase::WeldingDetection => "WeldingDetection",
Phase::Paused => "Paused",
Phase::Stopped => "Stopped",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DC: EnergyTransferMode = EnergyTransferMode::DCExtended;
const AC: EnergyTransferMode = EnergyTransferMode::ACThreePhaseCore;
const EIM: PaymentOption = PaymentOption::ExternalPayment;
const PNC: PaymentOption = PaymentOption::Contract;
fn run(requests: &[Request]) -> Result<Sequencer, SequenceError> {
let mut s = Sequencer::new();
for &r in requests {
s.accept(r)?;
}
Ok(s)
}
fn up_to_authorized(payment: PaymentOption) -> Sequencer {
let mut steps = alloc::vec![
Request::SessionSetup,
Request::ServiceDiscovery,
Request::PaymentServiceSelection(payment),
];
if payment == PNC {
steps.push(Request::PaymentDetails);
}
steps.push(Request::Authorization);
run(&steps).unwrap()
}
#[test]
fn the_ac_eim_flow_runs_end_to_end() {
let mut s = up_to_authorized(EIM);
for r in [
Request::ChargeParameterDiscovery(AC),
Request::PowerDelivery(ChargeProgress::Start),
Request::ChargingStatus,
Request::ChargingStatus,
Request::PowerDelivery(ChargeProgress::Stop),
Request::SessionStop(ChargingSession::Terminate),
] {
s.accept(r).unwrap_or_else(|e| panic!("{r:?}: {e}"));
}
assert!(s.is_finished());
}
#[test]
fn the_dc_pnc_flow_runs_end_to_end() {
let mut s = up_to_authorized(PNC);
for r in [
Request::ChargeParameterDiscovery(DC),
Request::CableCheck,
Request::CableCheck,
Request::PreCharge,
Request::PowerDelivery(ChargeProgress::Start),
Request::CurrentDemand,
Request::MeteringReceipt,
Request::CurrentDemand,
Request::PowerDelivery(ChargeProgress::Stop),
Request::WeldingDetection,
Request::SessionStop(ChargingSession::Terminate),
] {
s.accept(r).unwrap_or_else(|e| panic!("{r:?}: {e}"));
}
assert!(s.is_finished());
}
#[test]
fn dc_cannot_skip_the_cable_check() {
let mut s = up_to_authorized(EIM);
s.accept(Request::ChargeParameterDiscovery(DC)).unwrap();
let e = s.accept(Request::PowerDelivery(ChargeProgress::Start)).unwrap_err();
assert_eq!(e.got, "PowerDeliveryReq");
assert_eq!(e.response_code, ResponseCode::FAILEDSequenceError as u8);
assert_eq!(s.phase(), Phase::ChargeParameters, "a refusal must not advance the session");
}
#[test]
fn ac_has_no_cable_check_to_run() {
let mut s = up_to_authorized(EIM);
s.accept(Request::ChargeParameterDiscovery(AC)).unwrap();
assert!(!s.permits(Request::CableCheck));
assert!(!s.permits(Request::WeldingDetection));
}
#[test]
fn external_identification_has_no_contract_to_present() {
let s = run(&[
Request::SessionSetup,
Request::ServiceDiscovery,
Request::PaymentServiceSelection(EIM),
])
.unwrap();
assert!(!s.permits(Request::PaymentDetails));
assert!(!s.permits(Request::CertificateInstallation));
assert!(s.permits(Request::Authorization));
}
#[test]
fn a_contract_must_be_presented_before_authorization() {
let mut s = run(&[
Request::SessionSetup,
Request::ServiceDiscovery,
Request::PaymentServiceSelection(PNC),
])
.unwrap();
assert!(s.accept(Request::Authorization).is_err());
assert!(s.permits(Request::PaymentDetails));
}
#[test]
fn a_metering_receipt_needs_a_contract_to_receipt() {
let mut s = up_to_authorized(EIM);
s.accept(Request::ChargeParameterDiscovery(AC)).unwrap();
s.accept(Request::PowerDelivery(ChargeProgress::Start)).unwrap();
assert!(!s.permits(Request::MeteringReceipt), "there is no contract to sign against");
}
#[test]
fn renegotiation_returns_to_charge_parameter_discovery() {
let mut s = up_to_authorized(EIM);
s.accept(Request::ChargeParameterDiscovery(AC)).unwrap();
s.accept(Request::PowerDelivery(ChargeProgress::Start)).unwrap();
s.accept(Request::ChargingStatus).unwrap();
assert!(!s.has_renegotiated());
assert_eq!(
s.accept(Request::PowerDelivery(ChargeProgress::Renegotiate)).unwrap(),
Phase::ChargeParameters
);
assert!(s.has_renegotiated(), "the charger is revising a schedule, not making one");
s.accept(Request::ChargeParameterDiscovery(AC)).unwrap();
s.accept(Request::PowerDelivery(ChargeProgress::Start)).unwrap();
assert_eq!(s.phase(), Phase::Charging);
}
#[test]
fn dc_renegotiation_does_not_repeat_the_cable_check() {
let mut s = up_to_authorized(EIM);
for r in [
Request::ChargeParameterDiscovery(DC),
Request::CableCheck,
Request::PreCharge,
Request::PowerDelivery(ChargeProgress::Start),
Request::CurrentDemand,
Request::PowerDelivery(ChargeProgress::Renegotiate),
Request::ChargeParameterDiscovery(DC),
] {
s.accept(r).unwrap_or_else(|e| panic!("{r:?}: {e}"));
}
assert!(s.has_renegotiated());
assert_eq!(
s.accept(Request::PowerDelivery(ChargeProgress::Start)).unwrap(),
Phase::Charging
);
assert!(s.permits(Request::CurrentDemand));
}
#[test]
fn nothing_but_session_setup_starts_a_session() {
let s = Sequencer::new();
for r in [
Request::ServiceDiscovery,
Request::Authorization,
Request::SessionStop(ChargingSession::Terminate),
] {
assert!(!s.permits(r), "{r:?} must not start a session");
}
assert!(s.permits(Request::SessionSetup));
}
#[test]
fn a_stopped_session_accepts_nothing() {
let mut s = up_to_authorized(EIM);
for r in [
Request::ChargeParameterDiscovery(AC),
Request::PowerDelivery(ChargeProgress::Start),
Request::PowerDelivery(ChargeProgress::Stop),
Request::SessionStop(ChargingSession::Terminate),
] {
s.accept(r).unwrap();
}
assert!(!s.permits(Request::SessionSetup));
assert!(!s.permits(Request::ChargingStatus));
assert!(!s.permits(Request::SessionStop(ChargingSession::Terminate)));
}
#[test]
fn a_failure_leaves_only_session_stop() {
let mut s = up_to_authorized(EIM);
s.accept(Request::ChargeParameterDiscovery(DC)).unwrap();
s.failed();
assert!(s.is_failed());
for r in [Request::CableCheck, Request::ChargeParameterDiscovery(DC), Request::SessionSetup]
{
assert!(!s.permits(r), "{r:?} must not survive a failure response");
}
s.accept(Request::SessionStop(ChargingSession::Terminate)).unwrap();
assert!(s.is_finished());
}
#[test]
fn a_session_can_be_stopped_from_any_established_phase() {
for setup in [
&[Request::SessionSetup][..],
&[Request::SessionSetup, Request::ServiceDiscovery],
&[
Request::SessionSetup,
Request::ServiceDiscovery,
Request::PaymentServiceSelection(EIM),
Request::Authorization,
Request::ChargeParameterDiscovery(DC),
Request::CableCheck,
],
] {
let mut s = run(setup).unwrap();
let phase = s.phase();
s.accept(Request::SessionStop(ChargingSession::Terminate))
.unwrap_or_else(|e| panic!("stopping from {phase:?}: {e}"));
}
assert!(!Sequencer::new().permits(Request::SessionStop(ChargingSession::Terminate)));
}
#[test]
fn a_paused_session_is_finished_but_not_terminated() {
let mut s = up_to_authorized(EIM);
s.accept(Request::ChargeParameterDiscovery(AC)).unwrap();
s.accept(Request::PowerDelivery(ChargeProgress::Start)).unwrap();
s.accept(Request::PowerDelivery(ChargeProgress::Stop)).unwrap();
s.accept(Request::SessionStop(ChargingSession::Pause)).unwrap();
assert!(s.is_finished());
assert!(s.is_paused(), "a pause keeps the session id for a later resume");
}
#[test]
fn only_the_loops_have_a_loop_budget() {
use crate::session::timers::iso2 as t;
assert_eq!(Phase::CableCheck.loop_timeout(), Some(t::EVCC_CABLE_CHECK_TIMEOUT));
assert_eq!(Phase::PreCharge.loop_timeout(), Some(t::EVCC_PRE_CHARGE_TIMEOUT));
assert_eq!(Phase::Authorized.loop_timeout(), Some(t::EVCC_ONGOING_TIMEOUT));
assert_eq!(Phase::ChargeParameters.loop_timeout(), Some(t::EVCC_ONGOING_TIMEOUT));
assert_eq!(Phase::WeldingDetection.loop_timeout(), Some(t::EVCC_ONGOING_TIMEOUT));
for phase in [Phase::Start, Phase::SessionSetup, Phase::Charging, Phase::Stopped] {
assert_eq!(phase.loop_timeout(), None, "{phase:?} is not a loop");
}
assert!(
Phase::PreCharge.loop_timeout() < Phase::CableCheck.loop_timeout(),
"an isolation test takes longer than a voltage match"
);
}
#[test]
fn the_dc_charge_loop_gets_the_tight_timeout() {
use crate::session::timers::iso2 as t;
assert_eq!(Request::CurrentDemand.response_timeout(), t::MSG_TIMEOUT_CURRENT_DEMAND);
assert_eq!(Request::PaymentDetails.response_timeout(), t::MSG_TIMEOUT_BACKEND);
assert_eq!(
Request::SessionStop(ChargingSession::Terminate).response_timeout(),
t::MSG_TIMEOUT_DEFAULT
);
}
}