Skip to main content

ocpp_types/
envelope.rs

1//! OCPP-J WebSocket message envelopes (`CALL`/`CALLRESULT`/`CALLERROR`, and
2//! 2.1's `CALLRESULTERROR`/`SEND`) -- the array-based wrapper every OCPP
3//! message travels in, as opposed to the message payload types themselves
4//! (`v16`/`v201`/`v21`). See the OCPP-J specification, section 4.
5
6/// A unique identifier correlating a `Call`/`SendMessage` with its
7/// `CallResult`/`CallError`/`CallResultError`. Identical across 1.6J,
8/// 2.0.1, and 2.1: a string of at most 36 characters, to allow for
9/// UUIDs/GUIDs (per the OCPP-J specification).
10#[derive(Debug, Clone, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct MessageId(heapless::String<36>);
13
14impl MessageId {
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18}
19
20impl TryFrom<&str> for MessageId {
21    type Error = ();
22
23    fn try_from(value: &str) -> Result<Self, Self::Error> {
24        heapless::String::try_from(value).map(Self)
25    }
26}
27
28/// Placeholder for OCPP-J's `errorDetails`, which the specification
29/// deliberately leaves undefined in shape ("this JSON object describes
30/// error details in an undefined way") and recommends leaving empty in
31/// practice. Serializes as `{}`; deserializes by accepting and discarding
32/// whatever object is present, so it round-trips regardless of what a
33/// real implementation happens to send. Use a custom type via
34/// [`CallError`]/[`CallResultError`]'s `D` parameter for typed details.
35#[cfg(feature = "serde")]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub struct EmptyPayload;
38
39#[cfg(feature = "serde")]
40impl serde::Serialize for EmptyPayload {
41    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
42        use serde::ser::SerializeMap;
43        serializer.serialize_map(Some(0))?.end()
44    }
45}
46
47#[cfg(feature = "serde")]
48impl<'de> serde::Deserialize<'de> for EmptyPayload {
49    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
50        struct EmptyPayloadVisitor;
51
52        impl<'de> serde::de::Visitor<'de> for EmptyPayloadVisitor {
53            type Value = EmptyPayload;
54
55            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56                f.write_str("a JSON object")
57            }
58
59            fn visit_map<A: serde::de::MapAccess<'de>>(
60                self,
61                mut map: A,
62            ) -> Result<Self::Value, A::Error> {
63                while map
64                    .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
65                    .is_some()
66                {}
67                Ok(EmptyPayload)
68            }
69        }
70
71        deserializer.deserialize_map(EmptyPayloadVisitor)
72    }
73}
74
75/// An OCPP-J `CALL`: `[2, "<MessageId>", "<Action>", {Payload}]`. `T`'s
76/// action name is written on serialize and validated (not merely assumed)
77/// on deserialize, so a `Call<T>` you get back always really is the `T`
78/// you asked for.
79#[cfg(feature = "serde")]
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct Call<T> {
82    pub message_id: MessageId,
83    pub payload: T,
84}
85
86#[cfg(feature = "serde")]
87impl<T: crate::Action + serde::Serialize> serde::Serialize for Call<T> {
88    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
89        use serde::ser::SerializeTuple;
90
91        let mut tup = serializer.serialize_tuple(4)?;
92        tup.serialize_element(&2u8)?;
93        tup.serialize_element(&self.message_id)?;
94        tup.serialize_element(T::ACTION)?;
95        tup.serialize_element(&self.payload)?;
96        tup.end()
97    }
98}
99
100#[cfg(feature = "serde")]
101impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::Deserialize<'de> for Call<T> {
102    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103        struct CallVisitor<T>(core::marker::PhantomData<T>);
104
105        impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::de::Visitor<'de> for CallVisitor<T> {
106            type Value = Call<T>;
107
108            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109                write!(f, "a CALL array [2, messageId, \"{}\", payload]", T::ACTION)
110            }
111
112            fn visit_seq<A: serde::de::SeqAccess<'de>>(
113                self,
114                mut seq: A,
115            ) -> Result<Self::Value, A::Error> {
116                let message_type_id: u8 = seq
117                    .next_element()?
118                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
119                if message_type_id != 2 {
120                    return Err(serde::de::Error::invalid_value(
121                        serde::de::Unexpected::Unsigned(message_type_id as u64),
122                        &"2 (CALL)",
123                    ));
124                }
125
126                let message_id: MessageId = seq
127                    .next_element()?
128                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
129
130                let action: &str = seq
131                    .next_element()?
132                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
133                if action != T::ACTION {
134                    return Err(serde::de::Error::invalid_value(
135                        serde::de::Unexpected::Str(action),
136                        &T::ACTION,
137                    ));
138                }
139
140                let payload: T = seq
141                    .next_element()?
142                    .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
143
144                Ok(Call {
145                    message_id,
146                    payload,
147                })
148            }
149        }
150
151        deserializer.deserialize_tuple(4, CallVisitor(core::marker::PhantomData))
152    }
153}
154
155/// An OCPP-J `CALLRESULT`: `[3, "<MessageId>", {Payload}]`. Unlike
156/// [`Call`], there's no action name on the wire -- the recipient already
157/// knows which action a result is for by correlating `message_id` against
158/// the `Call` it sent, which is exactly what choosing `T` here represents.
159#[cfg(feature = "serde")]
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct CallResult<T> {
162    pub message_id: MessageId,
163    pub payload: T,
164}
165
166#[cfg(feature = "serde")]
167impl<T: serde::Serialize> serde::Serialize for CallResult<T> {
168    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
169        use serde::ser::SerializeTuple;
170
171        let mut tup = serializer.serialize_tuple(3)?;
172        tup.serialize_element(&3u8)?;
173        tup.serialize_element(&self.message_id)?;
174        tup.serialize_element(&self.payload)?;
175        tup.end()
176    }
177}
178
179#[cfg(feature = "serde")]
180impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for CallResult<T> {
181    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
182        struct CallResultVisitor<T>(core::marker::PhantomData<T>);
183
184        impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for CallResultVisitor<T> {
185            type Value = CallResult<T>;
186
187            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
188                f.write_str("a CALLRESULT array [3, messageId, payload]")
189            }
190
191            fn visit_seq<A: serde::de::SeqAccess<'de>>(
192                self,
193                mut seq: A,
194            ) -> Result<Self::Value, A::Error> {
195                let message_type_id: u8 = seq
196                    .next_element()?
197                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
198                if message_type_id != 3 {
199                    return Err(serde::de::Error::invalid_value(
200                        serde::de::Unexpected::Unsigned(message_type_id as u64),
201                        &"3 (CALLRESULT)",
202                    ));
203                }
204
205                let message_id: MessageId = seq
206                    .next_element()?
207                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
208
209                let payload: T = seq
210                    .next_element()?
211                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
212
213                Ok(CallResult {
214                    message_id,
215                    payload,
216                })
217            }
218        }
219
220        deserializer.deserialize_tuple(3, CallResultVisitor(core::marker::PhantomData))
221    }
222}
223
224/// Defines an error envelope type (`CallError`/`CallResultError`): both
225/// are `[<discriminant>, "<MessageId>", "<errorCode>", "<errorDescription>",
226/// {errorDetails}]`, differing only in which prior message they're a
227/// response to and their `MessageTypeId`. A macro rather than two
228/// hand-written copies, so a fix to one can't drift from the other.
229macro_rules! error_envelope {
230    ($name:ident, $discriminant:literal, $expecting:literal) => {
231        #[cfg(feature = "serde")]
232        #[derive(Debug, Clone, PartialEq, Eq)]
233        pub struct $name<E, D = EmptyPayload, const DESC_CAP: usize = 1024> {
234            pub message_id: MessageId,
235            pub error_code: E,
236            pub error_description: heapless::String<DESC_CAP>,
237            pub error_details: D,
238        }
239
240        #[cfg(feature = "serde")]
241        impl<E: serde::Serialize, D: serde::Serialize, const DESC_CAP: usize> serde::Serialize
242            for $name<E, D, DESC_CAP>
243        {
244            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
245                use serde::ser::SerializeTuple;
246
247                let mut tup = serializer.serialize_tuple(5)?;
248                tup.serialize_element(&$discriminant)?;
249                tup.serialize_element(&self.message_id)?;
250                tup.serialize_element(&self.error_code)?;
251                tup.serialize_element(&self.error_description)?;
252                tup.serialize_element(&self.error_details)?;
253                tup.end()
254            }
255        }
256
257        #[cfg(feature = "serde")]
258        impl<'de, E: serde::Deserialize<'de>, D: serde::Deserialize<'de>, const DESC_CAP: usize>
259            serde::Deserialize<'de> for $name<E, D, DESC_CAP>
260        {
261            fn deserialize<Dz: serde::Deserializer<'de>>(deserializer: Dz) -> Result<Self, Dz::Error> {
262                struct Visitor<E, D, const DESC_CAP: usize>(core::marker::PhantomData<(E, D)>);
263
264                impl<'de, E: serde::Deserialize<'de>, D: serde::Deserialize<'de>, const DESC_CAP: usize>
265                    serde::de::Visitor<'de> for Visitor<E, D, DESC_CAP>
266                {
267                    type Value = $name<E, D, DESC_CAP>;
268
269                    fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
270                        f.write_str($expecting)
271                    }
272
273                    fn visit_seq<A: serde::de::SeqAccess<'de>>(
274                        self,
275                        mut seq: A,
276                    ) -> Result<Self::Value, A::Error> {
277                        let message_type_id: u8 = seq
278                            .next_element()?
279                            .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
280                        if message_type_id != $discriminant {
281                            return Err(serde::de::Error::invalid_value(
282                                serde::de::Unexpected::Unsigned(message_type_id as u64),
283                                &$expecting,
284                            ));
285                        }
286
287                        let message_id = seq
288                            .next_element()?
289                            .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
290                        let error_code = seq
291                            .next_element()?
292                            .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
293                        let error_description = seq
294                            .next_element()?
295                            .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
296                        let error_details = seq
297                            .next_element()?
298                            .ok_or_else(|| serde::de::Error::invalid_length(4, &self))?;
299
300                        Ok($name {
301                            message_id,
302                            error_code,
303                            error_description,
304                            error_details,
305                        })
306                    }
307                }
308
309                deserializer.deserialize_tuple(5, Visitor(core::marker::PhantomData))
310            }
311        }
312    };
313}
314
315// An OCPP-J `CALLERROR`: `[4, "<MessageId>", "<errorCode>",
316// "<errorDescription>", {errorDetails}]`. `E` is the version's own
317// `RpcErrorCode` (e.g. `v16::RpcErrorCode`) -- error codes aren't shared
318// across versions, so nothing here picks one for you. `D` defaults to
319// `EmptyPayload` since the spec leaves `errorDetails` undefined in shape
320// and recommends leaving it empty; supply your own `D` for typed details.
321// `errorDescription` has no spec-given bound either, so it's a
322// caller-choosable const generic (default 1024) rather than a guess.
323error_envelope!(
324    CallError,
325    4u8,
326    "a CALLERROR array [4, messageId, errorCode, errorDescription, errorDetails]"
327);
328
329// An OCPP-J `CALLRESULTERROR` (OCPP 2.1 only): identical shape to
330// `CallError`, sent in response to a `CallResult` that itself contained
331// errors, rather than to a `Call`.
332error_envelope!(
333    CallResultError,
334    5u8,
335    "a CALLRESULTERROR array [5, messageId, errorCode, errorDescription, errorDetails]"
336);
337
338/// An OCPP-J `SEND` (OCPP 2.1 only): `[6, "<MessageId>", "<Action>",
339/// {Payload}]`. Structurally identical to [`Call`], but the receiver
340/// SHALL NOT respond to it -- used for fire-and-forget messages like
341/// frequent periodic monitoring values.
342#[cfg(feature = "serde")]
343#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct SendMessage<T> {
345    pub message_id: MessageId,
346    pub payload: T,
347}
348
349#[cfg(feature = "serde")]
350impl<T: crate::Action + serde::Serialize> serde::Serialize for SendMessage<T> {
351    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
352        use serde::ser::SerializeTuple;
353
354        let mut tup = serializer.serialize_tuple(4)?;
355        tup.serialize_element(&6u8)?;
356        tup.serialize_element(&self.message_id)?;
357        tup.serialize_element(T::ACTION)?;
358        tup.serialize_element(&self.payload)?;
359        tup.end()
360    }
361}
362
363#[cfg(feature = "serde")]
364impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::Deserialize<'de> for SendMessage<T> {
365    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
366        struct SendVisitor<T>(core::marker::PhantomData<T>);
367
368        impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::de::Visitor<'de> for SendVisitor<T> {
369            type Value = SendMessage<T>;
370
371            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
372                write!(f, "a SEND array [6, messageId, \"{}\", payload]", T::ACTION)
373            }
374
375            fn visit_seq<A: serde::de::SeqAccess<'de>>(
376                self,
377                mut seq: A,
378            ) -> Result<Self::Value, A::Error> {
379                let message_type_id: u8 = seq
380                    .next_element()?
381                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
382                if message_type_id != 6 {
383                    return Err(serde::de::Error::invalid_value(
384                        serde::de::Unexpected::Unsigned(message_type_id as u64),
385                        &"6 (SEND)",
386                    ));
387                }
388
389                let message_id: MessageId = seq
390                    .next_element()?
391                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
392
393                let action: &str = seq
394                    .next_element()?
395                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
396                if action != T::ACTION {
397                    return Err(serde::de::Error::invalid_value(
398                        serde::de::Unexpected::Str(action),
399                        &T::ACTION,
400                    ));
401                }
402
403                let payload: T = seq
404                    .next_element()?
405                    .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
406
407                Ok(SendMessage {
408                    message_id,
409                    payload,
410                })
411            }
412        }
413
414        deserializer.deserialize_tuple(4, SendVisitor(core::marker::PhantomData))
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    // The crate is `#![no_std]`, so `std` isn't implicitly in scope even in
423    // test builds -- only needed here to spawn a thread with a larger
424    // stack (see `send_message_serializes_and_round_trips_a_real_send_type_message`).
425    extern crate std;
426
427    #[cfg(feature = "serde")]
428    #[test]
429    fn empty_payload_serializes_as_an_empty_object() {
430        let mut buf = [0u8; 16];
431        let len = serde_json_core::to_slice(&EmptyPayload, &mut buf).unwrap();
432        assert_eq!(&buf[..len], b"{}");
433    }
434
435    #[cfg(feature = "serde")]
436    #[test]
437    fn empty_payload_accepts_and_discards_a_populated_object() {
438        let (parsed, _): (EmptyPayload, usize) =
439            serde_json_core::from_str(r#"{"foo": 1, "bar": [1, 2, 3]}"#).unwrap();
440        assert_eq!(parsed, EmptyPayload);
441    }
442
443    #[cfg(feature = "serde")]
444    #[test]
445    fn call_serializes_and_round_trips_a_real_message_type() {
446        use crate::v16::{AuthorizeRequest, IdTag};
447
448        let call = Call {
449            message_id: MessageId::try_from("19223201").unwrap(),
450            payload: AuthorizeRequest {
451                id_tag: IdTag::try_from("ABC123").unwrap(),
452            },
453        };
454
455        let mut buf = [0u8; 128];
456        let len = serde_json_core::to_slice(&call, &mut buf).unwrap();
457        assert_eq!(
458            core::str::from_utf8(&buf[..len]).unwrap(),
459            r#"[2,"19223201","Authorize",{"idTag":"ABC123"}]"#
460        );
461
462        let (parsed, _): (Call<AuthorizeRequest>, usize) =
463            serde_json_core::from_slice(&buf[..len]).unwrap();
464        assert_eq!(parsed, call);
465    }
466
467    #[cfg(feature = "serde")]
468    #[test]
469    fn call_rejects_the_wrong_message_type_id() {
470        use crate::v16::AuthorizeRequest;
471
472        let json = r#"[3,"1","Authorize",{"idTag":"ABC123"}]"#;
473        let result: Result<(Call<AuthorizeRequest>, usize), _> = serde_json_core::from_str(json);
474        assert!(result.is_err());
475    }
476
477    #[cfg(feature = "serde")]
478    #[test]
479    fn call_rejects_a_mismatched_action() {
480        use crate::v16::AuthorizeRequest;
481
482        let json = r#"[2,"1","Heartbeat",{"idTag":"ABC123"}]"#;
483        let result: Result<(Call<AuthorizeRequest>, usize), _> = serde_json_core::from_str(json);
484        assert!(result.is_err());
485    }
486
487    // `HeartbeatResponse::current_time` is `heapless::String<N>` without
488    // `alloc` and `alloc::string::String` with it -- this test constructs
489    // the no-`alloc` shape specifically.
490    #[cfg(all(feature = "serde", not(feature = "alloc")))]
491    #[test]
492    fn call_result_serializes_and_round_trips_a_real_message_type() {
493        use crate::v16::HeartbeatResponse;
494
495        let result: CallResult<HeartbeatResponse> = CallResult {
496            message_id: MessageId::try_from("19223201").unwrap(),
497            payload: HeartbeatResponse {
498                current_time: heapless::String::try_from("2013-02-01T20:53:32.486Z").unwrap(),
499            },
500        };
501
502        let mut buf = [0u8; 128];
503        let len = serde_json_core::to_slice(&result, &mut buf).unwrap();
504        assert_eq!(
505            core::str::from_utf8(&buf[..len]).unwrap(),
506            r#"[3,"19223201",{"currentTime":"2013-02-01T20:53:32.486Z"}]"#
507        );
508
509        let (parsed, _): (CallResult<HeartbeatResponse>, usize) =
510            serde_json_core::from_slice(&buf[..len]).unwrap();
511        assert_eq!(parsed, result);
512    }
513
514    #[cfg(feature = "serde")]
515    #[test]
516    fn call_result_rejects_the_wrong_message_type_id() {
517        use crate::v16::HeartbeatResponse;
518
519        let json = r#"[2,"1",{"currentTime":"2013-02-01T20:53:32.486Z"}]"#;
520        let result: Result<(CallResult<HeartbeatResponse>, usize), _> =
521            serde_json_core::from_str(json);
522        assert!(result.is_err());
523    }
524
525    #[cfg(feature = "serde")]
526    #[test]
527    fn call_error_serializes_and_round_trips_a_real_error_code() {
528        use crate::v16::RpcErrorCode;
529
530        let error: CallError<RpcErrorCode> = CallError {
531            message_id: MessageId::try_from("19223201").unwrap(),
532            error_code: RpcErrorCode::NotImplemented,
533            error_description: heapless::String::try_from("unrecognized action").unwrap(),
534            error_details: EmptyPayload,
535        };
536
537        let mut buf = [0u8; 128];
538        let len = serde_json_core::to_slice(&error, &mut buf).unwrap();
539        assert_eq!(
540            core::str::from_utf8(&buf[..len]).unwrap(),
541            r#"[4,"19223201","NotImplemented","unrecognized action",{}]"#
542        );
543
544        let (parsed, _): (CallError<RpcErrorCode>, usize) =
545            serde_json_core::from_slice(&buf[..len]).unwrap();
546        assert_eq!(parsed, error);
547    }
548
549    #[cfg(feature = "serde")]
550    #[test]
551    fn call_error_rejects_the_wrong_message_type_id() {
552        use crate::v16::RpcErrorCode;
553
554        let json = r#"[5,"1","NotImplemented","",{}]"#;
555        let result: Result<(CallError<RpcErrorCode>, usize), _> = serde_json_core::from_str(json);
556        assert!(result.is_err());
557    }
558
559    #[cfg(feature = "serde")]
560    #[test]
561    fn call_result_error_uses_discriminant_five_not_four() {
562        use crate::v201::RpcErrorCode;
563
564        let error: CallResultError<RpcErrorCode> = CallResultError {
565            message_id: MessageId::try_from("1").unwrap(),
566            error_code: RpcErrorCode::FormatViolation,
567            error_description: heapless::String::try_from("").unwrap(),
568            error_details: EmptyPayload,
569        };
570
571        let mut buf = [0u8; 128];
572        let len = serde_json_core::to_slice(&error, &mut buf).unwrap();
573        assert!(core::str::from_utf8(&buf[..len]).unwrap().starts_with("[5,"));
574
575        // A CALLERROR-shaped (discriminant 4) frame must NOT parse as a
576        // CallResultError, even though every other field lines up.
577        let call_error_json = r#"[4,"1","FormatViolation","",{}]"#;
578        let result: Result<(CallResultError<RpcErrorCode>, usize), _> =
579            serde_json_core::from_str(call_error_json);
580        assert!(result.is_err());
581    }
582
583    #[cfg(all(feature = "serde", not(feature = "alloc")))]
584    #[test]
585    fn send_message_serializes_and_round_trips_a_real_send_type_message() {
586        // `NotifyPeriodicEventStream` (OCPP 2.1) has no Request/Response
587        // suffix at all in the spec -- it's a genuine SEND-only message,
588        // not a CALL with an unusual name.
589        //
590        // This runs on a manually-spawned thread with a larger stack:
591        // deserializing an envelope wrapping a type with two large
592        // const-generic-defaulted fields (a 1024-byte string, a 16-item
593        // vec) goes through several layers of serde's Visitor pattern,
594        // and in an unoptimized debug build (no inlining) that's enough
595        // to overflow the default test-thread stack. Release builds are
596        // unaffected -- verified separately -- so this is purely a
597        // `cargo test`-in-debug-mode artifact, not a real embedded-target
598        // concern, but the test still needs to not crash in CI.
599        std::thread::Builder::new()
600            .stack_size(16 * 1024 * 1024)
601            .spawn(|| {
602                use crate::v21::NotifyPeriodicEventStream;
603
604                let send: SendMessage<NotifyPeriodicEventStream> = SendMessage {
605                    message_id: MessageId::try_from("19223201").unwrap(),
606                    payload: NotifyPeriodicEventStream {
607                        basetime: heapless::String::try_from("2024-08-27T12:30:40Z").unwrap(),
608                        custom_data: None,
609                        data: heapless::Vec::new(),
610                        id: 123,
611                        pending: 0,
612                    },
613                };
614
615                let mut buf = [0u8; 256];
616                let len = serde_json_core::to_slice(&send, &mut buf).unwrap();
617                let json = core::str::from_utf8(&buf[..len]).unwrap();
618                assert!(json.starts_with(r#"[6,"19223201","NotifyPeriodicEventStream","#));
619
620                let (parsed, _): (SendMessage<NotifyPeriodicEventStream>, usize) =
621                    serde_json_core::from_slice(&buf[..len]).unwrap();
622                assert_eq!(parsed, send);
623            })
624            .unwrap()
625            .join()
626            .unwrap();
627    }
628
629    #[test]
630    fn message_id_round_trips_a_short_string() {
631        let id = MessageId::try_from("19223201").unwrap();
632        assert_eq!(id.as_str(), "19223201");
633    }
634
635    #[test]
636    fn message_id_rejects_strings_over_36_bytes() {
637        let too_long = "a".repeat(37);
638        assert!(MessageId::try_from(too_long.as_str()).is_err());
639    }
640
641    #[test]
642    fn message_id_accepts_exactly_36_bytes() {
643        let exactly_36 = "a".repeat(36);
644        assert!(MessageId::try_from(exactly_36.as_str()).is_ok());
645    }
646}