iso15118 0.1.0

ISO 15118 vehicle-to-grid (V2G) communication — EXI codec, message sets, session engines and Plug & Charge signatures; sans-I/O and no_std-capable, for both EVCC and SECC
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! Session state: the clock, the spec timers, and the message-ordering rules.
//!
//! This is the layer between the message codecs and a working EVCC or SECC, and
//! it is deliberately the part with no I/O in it at all. Three pieces:
//!
//! * [`Instant`] and [`Millis`] — the caller's monotonic clock, injected.
//! * [`Connection`] — V2GTP framing over a byte stream, in both directions,
//!   bounded in bytes *and* in frames.
//! * [`Timers`] — the V2G deadlines, armed and disarmed against that clock, with
//!   every constant tied to the requirement it comes from.
//! * [`iso2::Sequencer`] and [`iso20::Sequencer`] — the message-ordering graphs,
//!   one per protocol generation.
//!
//! # Two kinds of deadline
//!
//! A per-message timeout ([`Timer::Message`]) bounds the answer to one request.
//! A *loop* budget ([`Timer::Ongoing`]) bounds a whole phase: several phases are
//! not one exchange but a repetition the peer keeps making while the answer is
//! `..._Ongoing` — authorization waiting on a driver, the DC isolation test,
//! schedule exchange waiting on a tariff. The per-message timeout does not cover
//! that case, because it restarts on every repeat: a station that answers
//! `Ongoing` promptly for ever is never late. [`Flow::loop_timeout`] is the
//! second kind, it starts on the first request of a phase, and the repeats do
//! not restart it.
//!
//! # Why sequencing is its own thing
//!
//! ISO 15118's ordering rules are not decoration. `FAILED_SequenceError` is a
//! defined response code and terminating the session is the defined reaction,
//! so "which requests are legal right now" is protocol logic that both sides
//! need and that neither should re-derive from prose. Keeping it in one place,
//! shared by the EVCC and the SECC, means the two cannot disagree about the
//! flow — and it makes each ordering rule a unit test rather than a field
//! observation.
//!
//! The sequencers hold no buffers and no keys, so a session snapshot is small,
//! `Clone`, and (with the `serde` feature) serialisable — which is what
//! pause/resume across a power cycle needs.
//!
//! # What is *not* here
//!
//! Policy. Whether to authorize, which schedule to offer, how much current to
//! deliver — none of that is protocol, and none of it is decided here. The role
//! drivers surface those as decisions for the application to answer.

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;

/// The message-ordering rules of whichever protocol generation a session
/// negotiated.
///
/// Present only when at least one protocol generation is enabled — with none,
/// there is no flow to track.
///
/// The two generations have different message sets and different graphs, but a
/// session driver needs to ask the same question of either — "is this legal
/// now?" — so this is the one place the two are joined.
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Flow {
    /// An ISO 15118-2 session.
    #[cfg(feature = "iso2")]
    #[cfg_attr(docsrs, doc(cfg(feature = "iso2")))]
    Iso2(iso2::Sequencer),
    /// An ISO 15118-20 session.
    #[cfg(feature = "iso20-common")]
    #[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
    Iso20(iso20::Sequencer),
}

#[cfg(any(feature = "iso2", feature = "iso20-common"))]
impl Flow {
    /// A fresh flow for `protocol`, or `None` when this build does not have
    /// that protocol enabled.
    #[must_use]
    #[allow(unused_variables, reason = "every arm is behind a feature")]
    pub fn new(protocol: crate::Protocol) -> Option<Self> {
        match protocol {
            #[cfg(feature = "iso2")]
            crate::Protocol::Iso2 => Some(Self::Iso2(iso2::Sequencer::new())),
            #[cfg(feature = "iso20-common")]
            crate::Protocol::Iso20 => Some(Self::Iso20(iso20::Sequencer::new())),
            _ => None,
        }
    }

    /// Records that `message` arrived.
    ///
    /// A response, or an element that is not a message at all, is not
    /// sequenced: the ordering rules constrain requests.
    #[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(())
    }

    /// Whether `message` would be accepted right now, without accepting it.
    ///
    /// The dry run [`Flow::accept`] is the committing version of. A sender uses
    /// it to check a request *before* anything else about sending it can fail,
    /// so that a message which never reached the wire never advanced the flow
    /// either.
    #[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(())
    }

    /// True when the session has reached its terminal phase — stopped or
    /// paused.
    #[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(),
        }
    }

    /// True when the session was paused rather than terminated.
    ///
    /// A paused session keeps its session id, its authorization and (in -20)
    /// its selected service and agreed schedule, and the vehicle may resume it.
    #[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(),
        }
    }

    /// Records that a `FAILED_*` response has gone past, after which the only
    /// request either side may still send is `SessionStopReq`.
    ///
    /// The session drivers call this for you; it is public because a caller
    /// driving the sequencers directly needs it too.
    pub fn failed(&mut self) {
        match self {
            #[cfg(feature = "iso2")]
            Self::Iso2(s) => s.failed(),
            #[cfg(feature = "iso20-common")]
            Self::Iso20(s) => s.failed(),
        }
    }

    /// True once a failure response has ended the session's forward progress.
    #[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(),
        }
    }

    /// Picks up a paused ISO 15118-20 session from just after `SessionSetup`.
    ///
    /// Returns `false` for an ISO 15118-2 flow, which has no negotiated state
    /// to skip: a resumed -2 session repeats service discovery and
    /// authorization, and only the session id carries over.
    #[cfg(feature = "iso20-common")]
    #[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
    pub fn resume(&mut self, service: iso20::Service) -> bool {
        match self {
            #[cfg(feature = "iso2")]
            Self::Iso2(_) => false,
            Self::Iso20(s) => {
                s.resume(service);
                true
            }
        }
    }

    /// How long the session may stay in the phase it is in.
    ///
    /// Several phases of both generations are loops the peer repeats while the
    /// answer is `..._Ongoing` — authorization, parameter discovery, the DC
    /// isolation test. Each has a bound separate from the per-message response
    /// timeout; this is that bound, and `None` for a phase that is not a loop.
    /// The role drivers arm [`Timer::Ongoing`] from it.
    #[must_use]
    pub const fn loop_timeout(&self) -> Option<Millis> {
        match self {
            #[cfg(feature = "iso2")]
            Self::Iso2(s) => s.loop_timeout(),
            #[cfg(feature = "iso20-common")]
            Self::Iso20(s) => s.loop_timeout(),
        }
    }

    /// The name of the phase the session is in, for logs.
    #[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(),
        }
    }
}

/// Why a message was not accepted into the flow.
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlowError {
    /// The message was out of sequence.
    Sequence(SequenceError),
    /// The message was a response, or an element that is not a message, where a
    /// request was expected.
    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 {}

/// A peer sent a request that its position in the flow does not allow.
///
/// ISO 15118 prescribes the reaction: answer with `FAILED_SequenceError` and
/// end the session. [`SequenceError::response_code`] is that code as its EXI
/// enumeration index, so the caller can put it in the response without knowing
/// which protocol generation raised it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SequenceError {
    /// The request that arrived, by element name.
    pub got: &'static str,
    /// The phase the session was in.
    pub phase: &'static str,
    /// `FAILED_SequenceError` as its EXI enumeration index in the protocol
    /// generation that raised it.
    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 {}

/// A V2G session identifier.
///
/// Eight bytes on the wire in both protocol generations, and the value a paused
/// session is resumed under. All-zero means "no session yet": an EVCC opening a
/// fresh session sends [`SessionId::NONE`], and the SECC answers with the one it
/// assigned.
///
/// The bytes must be unpredictable — a session id is what a resumed session is
/// recognised by — so the caller supplies them from its own randomness. This
/// crate has no RNG.
#[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 {
    /// Length of a session id in bytes.
    pub const LEN: usize = 8;

    /// The all-zero id, meaning "no session yet".
    pub const NONE: Self = Self([0; Self::LEN]);

    /// Wraps eight caller-supplied random bytes.
    #[must_use]
    pub const fn new(bytes: [u8; Self::LEN]) -> Self {
        Self(bytes)
    }

    /// The bytes, as they travel.
    #[must_use]
    pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
        &self.0
    }

    /// True for [`SessionId::NONE`].
    #[must_use]
    pub fn is_none(&self) -> bool {
        self.0 == [0; Self::LEN]
    }

    /// Reads an id off the wire.
    ///
    /// The two generations do not agree on the length, and the difference is
    /// real: ISO 15118-2 types `SessionID` as `hexBinary` with `maxLength = 8`,
    /// so a shorter one is legal and is zero-extended here — which is how it
    /// compares equal to what the SECC assigned. ISO 15118-20 types it as
    /// `length = 8`, exactly, and the codec refuses a short one before it ever
    /// reaches this function.
    ///
    /// Anything longer than eight bytes is a schema violation in either
    /// generation and is refused.
    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(())
    }
}

/// A `SessionID` longer than the schema allows.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionIdError {
    /// The length that arrived.
    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());
    }

    /// ISO 15118-2 permits a short `SessionID`; ISO 15118-20 does not, and the
    /// codec refuses one there before it reaches this type.
    #[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 }));
    }

    #[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"
        );
    }
}