# OCPP-RS
Rust protocol library for the [Open Charge Point Protocol](https://www.openchargealliance.org/) (OCPP).
Supports **OCPP 1.6** (JSON) and **OCPP 2.1** (additive / 2.0.1-compatible schemas).
[Documentation](https://docs.rs/ocpp_rs/latest/ocpp_rs/) · [Changelog](CHANGELOG.md) · [Guides](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](https://github.com/mobilityhouse/ocpp); 2.1 layout inspired by [rust-ocpp](https://github.com/tommymalmqvist/rust-ocpp)
## Install
```toml
[dependencies]
ocpp-rs = "0.4"
# Optional:
# features = ["schema_validate", "device_model_catalog", "datetime_serialize_rfc3339"]
```
| `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](guides/migration-0.4.md).
## `no_std` / firmware
```toml
[dependencies]
ocpp_rs = { version = "0.4", default-features = false }
# plus your allocator / #[global_allocator]
```
```bash
cargo build --lib
cargo build --lib --target thumbv7em-none-eabi # optional freestanding check
## OCPP 1.6 CallResult
CALLRESULT has no action on the wire. Blind parse yields `CallResultRaw`. Correlate with `PendingCalls`:
```rust
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(_))));
```
- Sticky sessions or Redis `messageId → action name` + `resolve_with_action_name`
- Datetime: RFC3339 parse; serialize defaults to `%.3fZ` — [guides/datetime-features.md](guides/datetime-features.md)
- Details: [`v16::pending`](src/v16/pending.rs), [migration guide](guides/migration-0.4.md)
## OCPP 2.1
Same correlation model under `v21::pending`. Framing types **2–6** (CALL, CALLRESULT, CALLERROR, CALLRESULTERROR, SEND).
```rust
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](guides/ocpp-2.0.1.md).
### Load-balanced resolve
```rust
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)
```rust
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):
```bash
cargo install cargo-fuzz
cargo +nightly fuzz run v21_deserialize --fuzz-dir fuzz
```
License: MIT.