use core::fmt;
mod clock;
mod conn;
pub mod timers;
pub use clock::{Instant, Millis};
pub use conn::{Connection, ConnectionError, MAX_PENDING_FRAMES};
pub use timers::{Timer, Timers};
#[cfg(feature = "iso2")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso2")))]
pub mod iso2;
#[cfg(feature = "iso20-common")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
pub mod iso20;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Security {
Tls,
None,
}
impl Security {
#[must_use]
pub const fn permits_plug_and_charge(self) -> bool {
matches!(self, Self::Tls)
}
}
impl fmt::Display for Security {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Tls => "TLS",
Self::None => "no transport security",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Role {
Evcc,
Secc,
}
impl Role {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Evcc => "EVCC",
Self::Secc => "SECC",
}
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Flow {
#[cfg(feature = "iso2")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso2")))]
Iso2(iso2::Sequencer),
#[cfg(feature = "iso20-common")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
Iso20(iso20::Sequencer),
}
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
impl Flow {
#[must_use]
#[allow(unused_variables, reason = "the body is entirely feature-gated")]
pub const fn supports(protocol: crate::Protocol) -> bool {
match protocol {
#[cfg(feature = "iso2")]
crate::Protocol::Iso2 => true,
#[cfg(feature = "iso20-common")]
crate::Protocol::Iso20 => true,
_ => false,
}
}
#[must_use]
#[allow(unused_variables, reason = "every arm is behind a feature")]
pub fn new(protocol: crate::Protocol, security: Security) -> Option<Self> {
match protocol {
#[cfg(feature = "iso2")]
crate::Protocol::Iso2 => Some(Self::Iso2(iso2::Sequencer::new(security))),
#[cfg(feature = "iso20-common")]
crate::Protocol::Iso20 => Some(Self::Iso20(iso20::Sequencer::new())),
_ => None,
}
}
#[allow(unused_variables, reason = "every arm is behind a feature")]
pub fn accept(&mut self, message: &crate::message::Message) -> Result<(), FlowError> {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => {
let request = iso2::Request::of(message).ok_or(FlowError::NotARequest)?;
s.accept(request)?;
}
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => {
let request = iso20::Request::of(message).ok_or(FlowError::NotARequest)?;
s.accept(request)?;
}
}
Ok(())
}
#[allow(unused_variables, reason = "every arm is behind a feature")]
pub fn permits(&self, message: &crate::message::Message) -> Result<(), FlowError> {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => {
let request = iso2::Request::of(message).ok_or(FlowError::NotARequest)?;
if !s.permits(request) {
return Err(FlowError::Sequence(s.refusal(request)));
}
}
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => {
let request = iso20::Request::of(message).ok_or(FlowError::NotARequest)?;
if !s.permits(request) {
return Err(FlowError::Sequence(s.refusal(request)));
}
}
}
Ok(())
}
#[must_use]
pub fn is_finished(&self) -> bool {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => s.is_finished(),
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => s.is_finished(),
}
}
#[must_use]
pub fn is_paused(&self) -> bool {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => s.is_paused(),
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => s.is_paused(),
}
}
pub fn failed(&mut self) {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => s.failed(),
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => s.failed(),
}
}
#[must_use]
pub fn is_failed(&self) -> bool {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => s.is_failed(),
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => s.is_failed(),
}
}
#[cfg(feature = "iso20-common")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
pub fn resume(&mut self, energy_transfer: iso20::EnergyTransfer) -> bool {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(_) => false,
Self::Iso20(s) => {
s.resume(energy_transfer);
true
}
}
}
#[must_use]
pub const fn loop_timeout(&self, role: Role) -> Option<Millis> {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => s.loop_timeout(role),
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => s.loop_timeout(role),
}
}
#[must_use]
pub const fn phase_name(&self) -> &'static str {
match self {
#[cfg(feature = "iso2")]
Self::Iso2(s) => s.phase_name(),
#[cfg(feature = "iso20-common")]
Self::Iso20(s) => s.phase_name(),
}
}
}
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlowError {
Sequence(SequenceError),
NotARequest,
}
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
impl From<SequenceError> for FlowError {
fn from(e: SequenceError) -> Self {
Self::Sequence(e)
}
}
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
impl fmt::Display for FlowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Sequence(e) => write!(f, "{e}"),
Self::NotARequest => f.write_str("expected a request"),
}
}
}
#[cfg(all(feature = "std", any(feature = "iso2", feature = "iso20-common")))]
impl std::error::Error for FlowError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SequenceError {
pub got: &'static str,
pub phase: &'static str,
pub response_code: u8,
}
impl fmt::Display for SequenceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} is out of sequence in phase {}", self.got, self.phase)
}
}
#[cfg(feature = "std")]
impl std::error::Error for SequenceError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SessionId([u8; Self::LEN]);
impl SessionId {
pub const LEN: usize = 8;
pub const NONE: Self = Self([0; Self::LEN]);
#[must_use]
pub const fn new(bytes: [u8; Self::LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
&self.0
}
#[must_use]
pub fn is_none(&self) -> bool {
self.0 == [0; Self::LEN]
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, SessionIdError> {
if bytes.len() > Self::LEN {
return Err(SessionIdError { len: bytes.len() });
}
let mut out = [0u8; Self::LEN];
out[..bytes.len()].copy_from_slice(bytes);
Ok(Self(out))
}
}
impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
write!(f, "{byte:02X}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionIdError {
pub len: usize,
}
impl fmt::Display for SessionIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SessionID is {} bytes, the schema allows at most {}", self.len, SessionId::LEN)
}
}
#[cfg(feature = "std")]
impl std::error::Error for SessionIdError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_session_has_no_id() {
assert!(SessionId::NONE.is_none());
assert!(!SessionId::new([1, 0, 0, 0, 0, 0, 0, 0]).is_none());
}
#[test]
fn a_short_session_id_is_zero_extended() {
let short = SessionId::from_slice(&[0xAB, 0xCD]).unwrap();
assert_eq!(short.as_bytes(), &[0xAB, 0xCD, 0, 0, 0, 0, 0, 0]);
}
#[test]
fn an_over_long_session_id_is_refused() {
assert_eq!(SessionId::from_slice(&[0; 9]), Err(SessionIdError { len: 9 }));
}
#[cfg(all(feature = "serde", feature = "iso2", feature = "iso20-common"))]
#[test]
fn a_session_snapshot_survives_a_power_cycle() {
use crate::iso2::{EnergyTransferMode, PaymentOption};
let mut flow = Flow::new(crate::Protocol::Iso2, Security::Tls).unwrap();
for request in [
iso2::Request::SessionSetup,
iso2::Request::ServiceDiscovery,
iso2::Request::PaymentServiceSelection(PaymentOption::Contract),
iso2::Request::PaymentDetails,
iso2::Request::Authorization,
iso2::Request::ChargeParameterDiscovery(EnergyTransferMode::DCExtended),
] {
let Flow::Iso2(s) = &mut flow else { unreachable!() };
s.accept(request).unwrap();
}
let stored = serde_json::to_string(&flow).unwrap();
let restored: Flow = serde_json::from_str(&stored).unwrap();
let (Flow::Iso2(before), Flow::Iso2(after)) = (&flow, &restored) else { unreachable!() };
assert_eq!(after.phase(), before.phase());
assert_eq!(after.transfer(), before.transfer());
assert_eq!(after.payment(), before.payment());
assert!(after.permits(iso2::Request::CableCheck));
assert!(!after.permits(iso2::Request::PowerDelivery(crate::iso2::ChargeProgress::Start)));
let mut flow = Flow::new(crate::Protocol::Iso20, Security::Tls).unwrap();
flow.resume(iso20::EnergyTransfer::unidirectional(iso20::Service::Dc));
let restored: Flow = serde_json::from_str(&serde_json::to_string(&flow).unwrap()).unwrap();
let Flow::Iso20(after) = &restored else { unreachable!() };
assert_eq!(after.service(), Some(iso20::Service::Dc));
assert!(after.permits(iso20::Request::ChargeParameterDiscovery));
}
#[test]
fn session_ids_display_as_hex() {
extern crate alloc;
use alloc::string::ToString;
assert_eq!(
SessionId::new([0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B]).to_string(),
"3D4CBF93374ED89B"
);
}
}