Skip to main content

codetics_ocpp_core/
rpc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! OCPP-J RPC framing (OCPP-J 1.6 §4, OCPP 2.0.1/2.1 Part 4 §4).
5//!
6//! ```text
7//! CALL            [2, "<MessageId>", "<Action>", {<Payload>}]
8//! CALLRESULT      [3, "<MessageId>", {<Payload>}]
9//! CALLERROR       [4, "<MessageId>", "<errorCode>", "<errorDescription>", {<errorDetails>}]
10//! CALLRESULTERROR [5, "<MessageId>", "<errorCode>", "<errorDescription>", {<errorDetails>}]   (2.1)
11//! SEND            [6, "<MessageId>", "<Action>", {<Payload>}]                                 (2.1)
12//! ```
13
14use serde::Serialize;
15use serde_json::value::RawValue;
16
17use crate::{ErrorCode, ErrorDialect, RpcError};
18
19/// Maximum message id length, to allow for UUIDs/GUIDs.
20pub const MAX_MESSAGE_ID_LEN: usize = 36;
21/// Maximum CALLERROR description length (2.0.1 Part 4 §4.2.3).
22pub const MAX_ERROR_DESCRIPTION_LEN: usize = 255;
23/// Message id to use in a CALLERROR when the id of the offending message cannot be read.
24pub const UNKNOWN_MESSAGE_ID: &str = "-1";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[repr(u8)]
28pub enum MessageTypeId {
29    Call = 2,
30    CallResult = 3,
31    CallError = 4,
32    CallResultError = 5,
33    Send = 6,
34}
35
36#[derive(Debug)]
37pub enum Frame {
38    Call {
39        id: String,
40        action: String,
41        payload: Box<RawValue>,
42    },
43    CallResult {
44        id: String,
45        payload: Box<RawValue>,
46    },
47    CallError {
48        id: String,
49        code: String,
50        description: String,
51        details: Box<RawValue>,
52    },
53    CallResultError {
54        id: String,
55        code: String,
56        description: String,
57        details: Box<RawValue>,
58    },
59    Send {
60        id: String,
61        action: String,
62        payload: Box<RawValue>,
63    },
64}
65
66/// A frame that could not be parsed.
67#[derive(Debug, Clone, PartialEq)]
68pub enum FrameError {
69    /// The text is not valid JSON, not an array, or the message id is unreadable.
70    Malformed { id: Option<String>, reason: String },
71    /// Valid envelope but a message type number this implementation does not know.
72    UnknownMessageType { id: Option<String>, type_id: u64 },
73}
74
75impl FrameError {
76    /// Message id to echo in the CALLERROR: the original id if readable, otherwise `"-1"`.
77    pub fn reply_id(&self) -> &str {
78        match self {
79            FrameError::Malformed { id, .. } | FrameError::UnknownMessageType { id, .. } => {
80                id.as_deref().unwrap_or(UNKNOWN_MESSAGE_ID)
81            }
82        }
83    }
84}
85
86impl std::fmt::Display for FrameError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            FrameError::Malformed { reason, .. } => write!(f, "malformed OCPP-J frame: {reason}"),
90            FrameError::UnknownMessageType { type_id, .. } => {
91                write!(f, "unknown message type id {type_id}")
92            }
93        }
94    }
95}
96
97impl std::error::Error for FrameError {}
98
99fn empty_object() -> Box<RawValue> {
100    RawValue::from_string("{}".to_owned()).expect("valid json")
101}
102
103/// `null` payloads are tolerated and normalized to `{}` (OCPP-J 1.6 §4.2.1).
104fn payload(raw: &RawValue) -> Box<RawValue> {
105    if raw.get() == "null" {
106        empty_object()
107    } else {
108        raw.to_owned()
109    }
110}
111
112fn string(raw: &RawValue) -> Option<String> {
113    serde_json::from_str::<String>(raw.get()).ok()
114}
115
116impl Frame {
117    pub fn parse(text: &str) -> Result<Frame, FrameError> {
118        let parts: Vec<&RawValue> =
119            serde_json::from_str(text).map_err(|e| FrameError::Malformed {
120                id: None,
121                reason: format!("not a JSON array: {e}"),
122            })?;
123        let type_id: u64 = parts
124            .first()
125            .and_then(|r| serde_json::from_str(r.get()).ok())
126            .ok_or_else(|| FrameError::Malformed {
127                id: None,
128                reason: "missing or invalid MessageTypeId".into(),
129            })?;
130        let id = parts.get(1).and_then(|r| string(r));
131        let Some(id_str) = id.clone() else {
132            return Err(FrameError::Malformed {
133                id: None,
134                reason: "missing or invalid MessageId".into(),
135            });
136        };
137        let bad = |reason: &str| FrameError::Malformed {
138            id: id.clone(),
139            reason: reason.into(),
140        };
141        let check_object = |r: &RawValue| -> Result<Box<RawValue>, FrameError> {
142            let p = payload(r);
143            if p.get().trim_start().starts_with('{') {
144                Ok(p)
145            } else {
146                Err(bad("payload is not a JSON object"))
147            }
148        };
149        match type_id {
150            2 | 6 => {
151                if parts.len() != 4 {
152                    return Err(bad("CALL/SEND must have 4 elements"));
153                }
154                let action = string(parts[2]).ok_or_else(|| bad("Action is not a string"))?;
155                let payload = check_object(parts[3])?;
156                Ok(if type_id == 2 {
157                    Frame::Call {
158                        id: id_str,
159                        action,
160                        payload,
161                    }
162                } else {
163                    Frame::Send {
164                        id: id_str,
165                        action,
166                        payload,
167                    }
168                })
169            }
170            3 => {
171                if parts.len() != 3 {
172                    return Err(bad("CALLRESULT must have 3 elements"));
173                }
174                Ok(Frame::CallResult {
175                    id: id_str,
176                    payload: check_object(parts[2])?,
177                })
178            }
179            4 | 5 => {
180                // Be lenient with peers that omit errorDetails.
181                if !(4..=5).contains(&parts.len()) {
182                    return Err(bad("CALLERROR must have 5 elements"));
183                }
184                let code = string(parts[2]).ok_or_else(|| bad("errorCode is not a string"))?;
185                let description = string(parts[3]).unwrap_or_default();
186                let details = parts
187                    .get(4)
188                    .map(|r| payload(r))
189                    .unwrap_or_else(empty_object);
190                Ok(if type_id == 4 {
191                    Frame::CallError {
192                        id: id_str,
193                        code,
194                        description,
195                        details,
196                    }
197                } else {
198                    Frame::CallResultError {
199                        id: id_str,
200                        code,
201                        description,
202                        details,
203                    }
204                })
205            }
206            other => Err(FrameError::UnknownMessageType { id, type_id: other }),
207        }
208    }
209
210    pub fn id(&self) -> &str {
211        match self {
212            Frame::Call { id, .. }
213            | Frame::CallResult { id, .. }
214            | Frame::CallError { id, .. }
215            | Frame::CallResultError { id, .. }
216            | Frame::Send { id, .. } => id,
217        }
218    }
219}
220
221/// Serializes `[2, id, action, payload]`.
222pub fn encode_call<P: Serialize + ?Sized>(
223    id: &str,
224    action: &str,
225    payload: &P,
226) -> Result<String, serde_json::Error> {
227    serde_json::to_string(&(MessageTypeId::Call as u8, id, action, payload))
228}
229
230/// Serializes `[6, id, action, payload]` (2.1 SEND).
231pub fn encode_send<P: Serialize + ?Sized>(
232    id: &str,
233    action: &str,
234    payload: &P,
235) -> Result<String, serde_json::Error> {
236    serde_json::to_string(&(MessageTypeId::Send as u8, id, action, payload))
237}
238
239/// Serializes `[3, id, payload]`.
240pub fn encode_result<P: Serialize + ?Sized>(
241    id: &str,
242    payload: &P,
243) -> Result<String, serde_json::Error> {
244    serde_json::to_string(&(MessageTypeId::CallResult as u8, id, payload))
245}
246
247/// Serializes a CALLERROR (`[4, …]`) or, with `result_error`, a CALLRESULTERROR (`[5, …]`).
248pub fn encode_error(id: &str, err: &RpcError, dialect: ErrorDialect, result_error: bool) -> String {
249    let type_id = if result_error {
250        MessageTypeId::CallResultError
251    } else {
252        MessageTypeId::CallError
253    } as u8;
254    let mut description = err.description.as_str();
255    if description.chars().count() > MAX_ERROR_DESCRIPTION_LEN {
256        let cut = description
257            .char_indices()
258            .nth(MAX_ERROR_DESCRIPTION_LEN)
259            .map(|(i, _)| i)
260            .unwrap_or(description.len());
261        description = &description[..cut];
262    }
263    // errorDetails MUST be an object; `{}` when there are none.
264    let empty = serde_json::Value::Object(Default::default());
265    let details = if err.details.is_object() {
266        &err.details
267    } else {
268        &empty
269    };
270    serde_json::to_string(&(type_id, id, err.code.as_wire(dialect), description, details))
271        .expect("error frame serializes")
272}
273
274/// Converts a received CALLERROR into an [`RpcError`].
275pub fn error_from_frame(code: &str, description: String, details: &RawValue) -> RpcError {
276    RpcError {
277        code: ErrorCode::from_wire(code),
278        description,
279        details: serde_json::from_str(details.get()).unwrap_or_default(),
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn parse_call() {
289        let f = Frame::parse(r#"[2, "19223201", "BootNotification", {"chargePointVendor": "VendorX", "chargePointModel": "X"}]"#).unwrap();
290        match f {
291            Frame::Call {
292                id,
293                action,
294                payload,
295            } => {
296                assert_eq!(id, "19223201");
297                assert_eq!(action, "BootNotification");
298                assert!(payload.get().contains("VendorX"));
299            }
300            _ => panic!(),
301        }
302        let f = Frame::parse(r#"[2,"1","Heartbeat",null]"#).unwrap();
303        assert!(matches!(f, Frame::Call { ref payload, .. } if payload.get() == "{}"));
304    }
305
306    #[test]
307    fn parse_result_and_error() {
308        assert!(matches!(
309            Frame::parse(r#"[3,"a",{"status":"Accepted"}]"#).unwrap(),
310            Frame::CallResult { .. }
311        ));
312        match Frame::parse(
313            r#"[4,"162376037","NotSupported","SetDisplayMessageRequest not supported",{}]"#,
314        )
315        .unwrap()
316        {
317            Frame::CallError {
318                code, description, ..
319            } => {
320                assert_eq!(code, "NotSupported");
321                assert_eq!(description, "SetDisplayMessageRequest not supported");
322            }
323            _ => panic!(),
324        }
325        assert!(matches!(
326            Frame::parse(r#"[5,"a","FormatViolation","",{}]"#).unwrap(),
327            Frame::CallResultError { .. }
328        ));
329        assert!(matches!(
330            Frame::parse(r#"[6,"a","NotifyPeriodicEventStream",{}]"#).unwrap(),
331            Frame::Send { .. }
332        ));
333    }
334
335    #[test]
336    fn parse_errors() {
337        assert_eq!(Frame::parse("garbage").unwrap_err().reply_id(), "-1");
338        assert_eq!(Frame::parse("[2]").unwrap_err().reply_id(), "-1");
339        assert_eq!(
340            Frame::parse(r#"[2,"abc","X"]"#).unwrap_err().reply_id(),
341            "abc"
342        );
343        assert_eq!(
344            Frame::parse(r#"[2,"abc","X",[]]"#).unwrap_err().reply_id(),
345            "abc"
346        );
347        assert_eq!(
348            Frame::parse(r#"[9,"abc"]"#).unwrap_err(),
349            FrameError::UnknownMessageType {
350                id: Some("abc".into()),
351                type_id: 9
352            }
353        );
354    }
355
356    #[test]
357    fn encode() {
358        assert_eq!(
359            encode_call("1", "Heartbeat", &serde_json::json!({})).unwrap(),
360            r#"[2,"1","Heartbeat",{}]"#
361        );
362        assert_eq!(
363            encode_result("1", &serde_json::json!({"a":1})).unwrap(),
364            r#"[3,"1",{"a":1}]"#
365        );
366        let e = RpcError::new(ErrorCode::FormatViolation, "bad");
367        assert_eq!(
368            encode_error("1", &e, ErrorDialect::Ocpp16, false),
369            r#"[4,"1","FormationViolation","bad",{}]"#
370        );
371        assert_eq!(
372            encode_error("1", &e, ErrorDialect::Ocpp2, true),
373            r#"[5,"1","FormatViolation","bad",{}]"#
374        );
375        let long = RpcError::new(ErrorCode::GenericError, "x".repeat(400));
376        let s = encode_error("1", &long, ErrorDialect::Ocpp2, false);
377        assert!(s.len() < 300);
378    }
379}