pub mod matching;
mod message;
use core::fmt;
pub use message::{
AttenCharInd, AttenCharRsp, AttenProfile, AttenProfileInd, MnbcSoundInd, RunId, SetKeyCnf,
SetKeyReq, SlacMatchCnf, SlacMatchReq, SlacParmCnf, SlacParmReq, StartAttenCharInd, StationId,
ValidateCnf, ValidateReq,
};
pub const ETHERTYPE_HOMEPLUG_AV: u16 = 0x88E1;
pub const ETHERNET_HEADER_LEN: usize = 14;
pub const MIN_FRAME_LEN: usize = 60;
pub const STATION_ID_LEN: usize = 17;
pub const RUN_ID_LEN: usize = 8;
pub const NMK_LEN: usize = 16;
pub const NID_LEN: usize = 7;
pub const AAG_LEN: usize = 58;
pub type MacAddr = [u8; 6];
pub const BROADCAST: MacAddr = [0xFF; 6];
pub const APPLICATION_TYPE_PEV_EVSE: u8 = 0x00;
pub const SECURITY_TYPE_NONE: u8 = 0x00;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Mmv {
Av1_0,
Av1_1,
Av2_0,
}
impl Mmv {
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::Av1_0 => 0x00,
Self::Av1_1 => 0x01,
Self::Av2_0 => 0x02,
}
}
pub const fn from_u8(value: u8) -> Result<Self, SlacError> {
match value {
0x00 => Ok(Self::Av1_0),
0x01 => Ok(Self::Av1_1),
0x02 => Ok(Self::Av2_0),
other => Err(SlacError::UnknownMmv(other)),
}
}
#[must_use]
pub const fn fragmentation_len(self) -> usize {
match self {
Self::Av1_0 => 0,
Self::Av1_1 | Self::Av2_0 => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Mmtype {
SetKeyReq,
SetKeyCnf,
SlacParmReq,
SlacParmCnf,
StartAttenCharInd,
AttenCharInd,
AttenCharRsp,
MnbcSoundInd,
ValidateReq,
ValidateCnf,
SlacMatchReq,
SlacMatchCnf,
AttenProfileInd,
Other(u16),
}
impl Mmtype {
#[must_use]
pub const fn as_u16(self) -> u16 {
match self {
Self::SetKeyReq => 0x6008,
Self::SetKeyCnf => 0x6009,
Self::SlacParmReq => 0x6064,
Self::SlacParmCnf => 0x6065,
Self::StartAttenCharInd => 0x606A,
Self::AttenCharInd => 0x606E,
Self::AttenCharRsp => 0x606F,
Self::MnbcSoundInd => 0x6076,
Self::ValidateReq => 0x6078,
Self::ValidateCnf => 0x6079,
Self::SlacMatchReq => 0x607C,
Self::SlacMatchCnf => 0x607D,
Self::AttenProfileInd => 0x6086,
Self::Other(v) => v,
}
}
#[must_use]
pub const fn from_u16(value: u16) -> Self {
match value {
0x6008 => Self::SetKeyReq,
0x6009 => Self::SetKeyCnf,
0x6064 => Self::SlacParmReq,
0x6065 => Self::SlacParmCnf,
0x606A => Self::StartAttenCharInd,
0x606E => Self::AttenCharInd,
0x606F => Self::AttenCharRsp,
0x6076 => Self::MnbcSoundInd,
0x6078 => Self::ValidateReq,
0x6079 => Self::ValidateCnf,
0x607C => Self::SlacMatchReq,
0x607D => Self::SlacMatchCnf,
0x6086 => Self::AttenProfileInd,
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Frame<'a> {
pub destination: MacAddr,
pub source: MacAddr,
pub mmv: Mmv,
pub mmtype: Mmtype,
pub payload: &'a [u8],
}
pub fn write_frame(
out: &mut [u8],
destination: MacAddr,
source: MacAddr,
mmv: Mmv,
mmtype: Mmtype,
payload: &[u8],
) -> Result<usize, SlacError> {
let header_len = ETHERNET_HEADER_LEN + 3 + mmv.fragmentation_len();
let len = (header_len + payload.len()).max(MIN_FRAME_LEN);
if out.len() < len {
return Err(SlacError::BufferTooSmall);
}
out[..len].fill(0);
out[..6].copy_from_slice(&destination);
out[6..12].copy_from_slice(&source);
out[12..14].copy_from_slice(ÐERTYPE_HOMEPLUG_AV.to_be_bytes());
out[14] = mmv.as_u8();
out[15..17].copy_from_slice(&mmtype.as_u16().to_le_bytes());
out[header_len..header_len + payload.len()].copy_from_slice(payload);
Ok(len)
}
pub fn parse_frame(frame: &[u8]) -> Result<Frame<'_>, SlacError> {
if frame.len() < ETHERNET_HEADER_LEN + 3 {
return Err(SlacError::Truncated);
}
let ethertype = u16::from_be_bytes([frame[12], frame[13]]);
if ethertype != ETHERTYPE_HOMEPLUG_AV {
return Err(SlacError::NotHomeplug(ethertype));
}
let mmv = Mmv::from_u8(frame[14])?;
let mmtype = Mmtype::from_u16(u16::from_le_bytes([frame[15], frame[16]]));
let body_start = ETHERNET_HEADER_LEN + 3 + mmv.fragmentation_len();
let payload = frame.get(body_start..).ok_or(SlacError::Truncated)?;
let mut destination = [0u8; 6];
let mut source = [0u8; 6];
destination.copy_from_slice(&frame[..6]);
source.copy_from_slice(&frame[6..12]);
Ok(Frame { destination, source, mmv, mmtype, payload })
}
pub mod timers {
pub const C_EV_START_ATTEN_CHAR_INDS: u8 = 3;
pub const C_EV_MATCH_RETRY: u8 = 2;
pub const C_EV_MATCH_MNBC: u8 = 10;
pub const TP_EV_BATCH_MSG_INTERVAL_MS: u32 = 40;
pub const TT_EV_ATTEN_RESULTS_MS: u32 = 1200;
pub const TT_EVSE_MATCH_MNBC_MS: u32 = 600;
pub const TT_MATCH_SEQUENCE_MS: u32 = 400;
pub const TT_MATCH_RESPONSE_MS: u32 = 200;
pub const TT_EVSE_MATCH_SESSION_MS: u32 = 10_000;
pub const TT_EVSE_SLAC_INIT_MS: u32 = 40_000;
pub const TT_MATCH_JOIN_MS: u32 = 12_000;
pub const T_STEP_EF_MS: u32 = 4_000;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SlacError {
Truncated,
BufferTooSmall,
NotHomeplug(u16),
UnknownMmv(u8),
UnexpectedMmtype {
expected: Mmtype,
actual: Mmtype,
},
UnsupportedProfile {
application_type: u8,
security_type: u8,
},
BadLength {
declared: usize,
expected: usize,
},
TooManyGroups(u8),
}
impl fmt::Display for SlacError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Truncated => f.write_str("truncated SLAC message"),
Self::BufferTooSmall => f.write_str("output buffer too small for SLAC frame"),
Self::NotHomeplug(t) => write!(f, "EtherType {t:#06x} is not HomePlug AV"),
Self::UnknownMmv(v) => write!(f, "unknown HomePlug message version {v:#04x}"),
Self::UnexpectedMmtype { expected, actual } => {
write!(f, "expected {expected:?}, got {actual:?}")
}
Self::UnsupportedProfile { application_type, security_type } => write!(
f,
"unsupported SLAC profile: application_type {application_type:#04x}, \
security_type {security_type:#04x}"
),
Self::BadLength { declared, expected } => {
write!(f, "length field says {declared}, layout requires {expected}")
}
Self::TooManyGroups(n) => write!(f, "{n} carrier groups exceeds the {AAG_LEN} maximum"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for SlacError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mmtype_variants_follow_the_homeplug_encoding() {
assert_eq!(Mmtype::SlacParmReq.as_u16() & 0x3, 0);
assert_eq!(Mmtype::SlacParmCnf.as_u16() & 0x3, 1);
assert_eq!(Mmtype::StartAttenCharInd.as_u16() & 0x3, 2);
assert_eq!(Mmtype::AttenCharRsp.as_u16() & 0x3, 3);
assert_eq!(Mmtype::AttenCharInd.as_u16() & !0x3, 0x606C);
assert_eq!(Mmtype::SlacMatchReq.as_u16() & !0x3, 0x607C);
}
#[test]
fn mmtype_roundtrips() {
for t in [
Mmtype::SetKeyReq,
Mmtype::SetKeyCnf,
Mmtype::SlacParmReq,
Mmtype::SlacParmCnf,
Mmtype::StartAttenCharInd,
Mmtype::AttenCharInd,
Mmtype::AttenCharRsp,
Mmtype::MnbcSoundInd,
Mmtype::ValidateReq,
Mmtype::ValidateCnf,
Mmtype::SlacMatchReq,
Mmtype::SlacMatchCnf,
Mmtype::AttenProfileInd,
Mmtype::Other(0x1234),
] {
assert_eq!(Mmtype::from_u16(t.as_u16()), t);
}
}
#[test]
fn frame_roundtrips_and_pads_to_the_ethernet_minimum() {
let mut buf = [0u8; 128];
let src = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
let n = write_frame(&mut buf, BROADCAST, src, Mmv::Av1_1, Mmtype::SlacParmReq, &[1, 2, 3])
.unwrap();
assert_eq!(n, MIN_FRAME_LEN, "short MMEs must be padded");
let f = parse_frame(&buf[..n]).unwrap();
assert_eq!(f.destination, BROADCAST);
assert_eq!(f.source, src);
assert_eq!(f.mmv, Mmv::Av1_1);
assert_eq!(f.mmtype, Mmtype::SlacParmReq);
assert_eq!(&f.payload[..3], &[1, 2, 3]);
}
#[test]
fn mmtype_is_little_endian_on_the_wire() {
let mut buf = [0u8; 128];
write_frame(&mut buf, BROADCAST, [0; 6], Mmv::Av1_1, Mmtype::SlacParmReq, &[]).unwrap();
assert_eq!(&buf[15..17], &[0x64, 0x60]);
assert_eq!(&buf[12..14], &[0x88, 0xE1]);
}
#[test]
fn av_1_0_has_no_fragmentation_field() {
let mut buf = [0u8; 128];
write_frame(&mut buf, BROADCAST, [0; 6], Mmv::Av1_0, Mmtype::SlacParmReq, &[0xAB]).unwrap();
assert_eq!(buf[17], 0xAB, "payload starts right after the header");
let mut buf11 = [0u8; 128];
write_frame(&mut buf11, BROADCAST, [0; 6], Mmv::Av1_1, Mmtype::SlacParmReq, &[0xAB])
.unwrap();
assert_eq!(&buf11[17..19], &[0, 0], "two bytes of fragmentation info");
assert_eq!(buf11[19], 0xAB);
}
#[test]
fn a_non_homeplug_ethertype_is_rejected() {
let mut frame = [0u8; 64];
frame[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
assert_eq!(parse_frame(&frame), Err(SlacError::NotHomeplug(0x0800)));
}
#[test]
fn a_short_frame_is_rejected() {
assert_eq!(parse_frame(&[0u8; 16]), Err(SlacError::Truncated));
}
const _: () = {
use timers::*;
assert!(TT_MATCH_RESPONSE_MS < TT_MATCH_SEQUENCE_MS);
assert!(TT_EVSE_MATCH_MNBC_MS < TT_EV_ATTEN_RESULTS_MS);
assert!(TT_EVSE_MATCH_SESSION_MS < TT_MATCH_JOIN_MS);
};
}