ocpp_rs 0.4.0

no_std + alloc OCPP 1.6 and 2.1 protocol library (OCPP-J parse/serialize, CallResult correlation).
Documentation

OCPP-RS

Rust protocol library for the Open Charge Point Protocol (OCPP).

Supports OCPP 1.6 (JSON) and OCPP 2.1 (additive / 2.0.1-compatible schemas).

Documentation · Changelog · Guides

Features

  • Full OCPP 1.6 and 2.1 message payloads + OCPP-J parse/serialize
  • CallResult typing via PendingCalls / action-name correlation (no untagged guessing)
  • Typed RPC framework error codes; MessageId length ≤ 36
  • Optional Part 3 schema bounds (schema_validate) and device-model catalogs
  • #![no_std] + alloc — zero std in library code (global allocator required on baremetal)
  • Inspired by mobilityhouse/ocpp; 2.1 layout inspired by rust-ocpp

Install

[dependencies]
ocpp-rs = "0.4"

# Optional:
# features = ["schema_validate", "device_model_catalog", "datetime_serialize_rfc3339"]
Feature Purpose
schema_validate Enforce Part 3 / 1.6 string/array/numeric bounds after parse
device_model_catalog Standard 2.1 component/variable name tables
datetime_serialize_rfc3339 Emit RFC3339 millis instead of %.3fZ

MSRV: 1.85 (edition 2024).

Upgrading from 0.2.x: guides/migration-0.4.md.

no_std / firmware

[dependencies]
ocpp_rs = { version = "0.4", default-features = false }
# plus your allocator / #[global_allocator]
cargo build --lib
cargo build --lib --target thumbv7em-none-eabi   # optional freestanding check
cargo tree -p chrono -f '{p} {f}' | head -5       # expect: alloc,serde (not std/clock)

OCPP 1.6 CallResult

CALLRESULT has no action on the wire. Blind parse yields CallResultRaw. Correlate with PendingCalls:

use ocpp_rs::v16::call::{Action, Call, Heartbeat};
use ocpp_rs::v16::parse::TypedMessage;
use ocpp_rs::v16::pending::PendingCalls;
use ocpp_rs::v16::typed_call_result::TypedCallResult;

let mut pending = PendingCalls::new();
let _wire = pending.send_call(Call::new("1".into(), Action::Heartbeat(Heartbeat {})))?;
let typed = pending.deserialize_typed(r#"[3, "1", {"currentTime": "2024-01-01T00:00:00.000Z"}]"#)?;
assert!(matches!(typed, TypedMessage::CallResult(TypedCallResult::Heartbeat(_))));

OCPP 2.1

Same correlation model under v21::pending. Framing types 2–6 (CALL, CALLRESULT, CALLERROR, CALLRESULTERROR, SEND).

use ocpp_rs::v21::call::{Action, Call};
use ocpp_rs::v21::messages::heartbeat::HeartbeatRequest;
use ocpp_rs::v21::parse::TypedMessage;
use ocpp_rs::v21::pending::PendingCalls;
use ocpp_rs::v21::typed_call_result::TypedCallResult;

let mut pending = PendingCalls::new();
let call = Call::new(
    "1".into(),
    Action::Heartbeat(HeartbeatRequest { custom_data: None }),
);
assert_eq!(call.action_kind(), "Heartbeat");
let _wire = pending.send_call(call)?;
let typed = pending.deserialize_typed(
    r#"[3, "1", {"currentTime": "2024-01-01T00:00:00.000Z"}]"#,
)?;
assert!(matches!(
    typed,
    TypedMessage::CallResult(TypedCallResult::Heartbeat(_))
));

2.0.1 subprotocol gating: guides/ocpp-2.0.1.md.

Load-balanced resolve

use ocpp_rs::v21::pending::resolve_with_action_name;
use ocpp_rs::v21::parse::{deserialize_to_message, Message};

let Message::CallResult(raw) = deserialize_to_message(wire)? else { /**/ };
let typed = resolve_with_action_name(raw, &redis_getdel(&raw.unique_id)?)?;

Example (1.6 CALL)

use ocpp_rs::v16::parse::{self, Message};
use ocpp_rs::v16::call::Action;

let incoming_text = r#"[2, "19223201", "BootNotification", {"chargePointVendor":"VendorX","chargePointModel":"SingleSocketCharger"}]"#;
let incoming_message = parse::deserialize_to_message(incoming_text)?;
if let Message::Call(call) = incoming_message {
    match call.payload {
        Action::BootNotification(_boot) => { /**/ }
        _ => {}
    }
}

Out of scope

This is a protocol wire crate, not a full stack:

  • No SOAP / HTTP binding for 1.6
  • No WebSocket, TLS, or Security Profile handshake
  • No Charge Point / CSMS state machines or OCA Part 6 runner

Contributing

Add or update tests with behavior changes. Fuzz (optional, nightly):

cargo install cargo-fuzz
cargo +nightly fuzz run v21_deserialize --fuzz-dir fuzz

License: MIT.