ocpp_client/client.rs
1use crate::action::{Action, SendAction};
2use crate::envelope::{
3 MESSAGE_TYPE_CALL, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_RESULT, MESSAGE_TYPE_SEND, RawCall,
4 RawError, RawResult, RawSend,
5};
6use crate::error::{ClientError, ProtocolError};
7use crate::keepalive::{KeepaliveBehavior, KeepalivePolicy};
8use crate::reconnect::{ReconnectPolicy, Reconnector};
9use crate::runtime::{Executor, Timer, with_cancel, with_timeout};
10use crate::sync::{BroadcastRegistry, Chan, Notify, OneShot, SharedMutex};
11use crate::transport::{TransportEvent, TransportSink, TransportStream};
12use alloc::borrow::ToOwned;
13use alloc::boxed::Box;
14use alloc::collections::BTreeMap;
15use alloc::format;
16use alloc::string::{String, ToString};
17use alloc::sync::Arc;
18use alloc::vec::Vec;
19use core::future::Future;
20use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
21use core::time::Duration;
22use serde::Serialize;
23use serde::de::DeserializeOwned;
24use serde_json::Value;
25use uuid::Uuid;
26
27type PendingResponses<E> = Arc<SharedMutex<BTreeMap<Uuid, OneShot<Result<Value, E>>>>>;
28type RequestSenders = Arc<SharedMutex<BTreeMap<String, Chan<(String, Value)>>>>;
29type NotificationSenders = Arc<SharedMutex<BTreeMap<String, Chan<Value>>>>;
30type PongWaiters = Arc<SharedMutex<PongState>>;
31
32/// Outstanding pings, keyed by the correlation token written into each ping's payload. RFC 6455
33/// requires a pong to echo that payload back, so a pong resolves the exact ping that produced it
34/// rather than whichever one happened to be at the front of a queue.
35///
36/// This replaced a `VecDeque<OneShot<()>>` matched positionally, which had two failure modes: a
37/// ping that timed out left its waiter in the queue forever, permanently offsetting every later
38/// ping's pong by one, and an unsolicited pong (which RFC 6455 permits) did the same. Both were
39/// mostly unreachable while pings were only ever sent by hand; a keepalive loop pinging on a
40/// timer makes them routine.
41#[derive(Default)]
42struct PongState {
43 next_token: u64,
44 waiters: BTreeMap<u64, OneShot<()>>,
45}
46
47/// Why the read loop stopped reading the current transport, which decides what happens next.
48enum LoopExit {
49 /// The transport ended or errored on its own. Redial if a reconnector is configured.
50 Eof,
51 /// Keepalive (or `Client::force_reconnect`) gave up on an unresponsive peer. Redial.
52 Forced,
53 /// `Client::disconnect` was called. Stop entirely - do not redial.
54 Shutdown,
55}
56
57/// The keepalive interval, mutable at runtime so a CSMS writing `WebSocketPingInterval` takes
58/// effect on a live connection, plus the fixed policy governing each ping.
59///
60/// Stored as milliseconds in an `AtomicU32` rather than behind the client's mutex so
61/// `Client::ping_interval`/`set_ping_interval` can stay non-`async` - a `GetVariables` handler
62/// reporting the value shouldn't have to await a lock. `u32` milliseconds caps at ~49 days, far
63/// beyond any sane ping interval, and 32-bit atomics exist on every target this crate builds for
64/// (`AtomicU64` does not - notably not on `thumbv7em-none-eabihf`).
65struct KeepaliveState {
66 interval_millis: AtomicU32,
67 changed: Notify,
68 policy: KeepalivePolicy,
69}
70
71impl KeepaliveState {
72 /// The current interval, or `None` when keepalive is off. Zero is the disabled
73 /// representation, matching OCPP's `WebSocketPingInterval` semantics.
74 fn interval(&self) -> Option<Duration> {
75 match self.interval_millis.load(Ordering::Relaxed) {
76 0 => None,
77 millis => Some(Duration::from_millis(millis as u64)),
78 }
79 }
80
81 fn set_interval(&self, interval: Option<Duration>) {
82 let millis = interval
83 .map(|d| d.as_millis().min(u32::MAX as u128) as u32)
84 .unwrap_or(0);
85 self.interval_millis.store(millis, Ordering::Relaxed);
86 self.changed.notify();
87 }
88}
89
90/// Everything `Client::from_transport_with_config` needs beyond the transport halves and the
91/// runtime. Introduced because the option set had outgrown positional parameters -
92/// `from_transport_with_reconnect` already took seven arguments, and keepalive would have made it
93/// eight or forced a third constructor.
94///
95/// Defaults are deliberately inert: no reconnector, no keepalive. `ConnectOptions` (the
96/// WebSocket convenience path) opts into both, but a caller assembling a client from raw
97/// transport halves gets exactly the behavior they asked for and nothing more.
98pub struct ClientConfig {
99 /// How long to wait for a CALLRESULT/CALLERROR, and the default pong deadline.
100 pub timeout: Duration,
101 /// Redials when the transport closes. `None` means the read loop exits on disconnect.
102 pub reconnector: Option<Box<dyn Reconnector>>,
103 /// Backoff between failed reconnect attempts. Ignored when `reconnector` is `None`.
104 pub reconnect_policy: ReconnectPolicy,
105 /// Whether to ping the peer on a schedule, and what to do when it stops answering.
106 pub keepalive: KeepaliveBehavior,
107}
108
109impl ClientConfig {
110 /// A config with `timeout` and nothing else enabled.
111 pub fn new(timeout: Duration) -> Self {
112 Self {
113 timeout,
114 reconnector: None,
115 reconnect_policy: ReconnectPolicy::default(),
116 keepalive: KeepaliveBehavior::Disabled,
117 }
118 }
119
120 /// Redial through `reconnector`, backing off per `policy`, when the transport closes.
121 pub fn with_reconnect(
122 mut self,
123 reconnector: Box<dyn Reconnector>,
124 policy: ReconnectPolicy,
125 ) -> Self {
126 self.reconnector = Some(reconnector);
127 self.reconnect_policy = policy;
128 self
129 }
130
131 /// Ping the peer per `keepalive`.
132 pub fn with_keepalive(mut self, keepalive: KeepaliveBehavior) -> Self {
133 self.keepalive = keepalive;
134 self
135 }
136}
137
138/// The OCPP client engine, generic over one version's protocol error type. `OCPP1_6Client`
139/// and `OCPP2_0_1Client` are just `Client<OCPP1_6Error>` / `Client<OCPP2_0_1Error>` - the
140/// dispatch/timeout/error machinery below is written once and shared by every version.
141pub struct Client<E: ProtocolError> {
142 sink: Arc<SharedMutex<Box<dyn TransportSink>>>,
143 pending_responses: PendingResponses<E>,
144 request_senders: RequestSenders,
145 notification_senders: NotificationSenders,
146 pong_waiters: PongWaiters,
147 ping_registry: Arc<BroadcastRegistry>,
148 reconnect_registry: Arc<BroadcastRegistry>,
149 keepalive: Arc<KeepaliveState>,
150 force_reconnect: Notify,
151 /// Sticky: set by `disconnect()` and never cleared. Distinguishes "the caller shut this
152 /// client down" from "the connection dropped", which the read loop must treat oppositely.
153 closed: Arc<AtomicBool>,
154 executor: Arc<dyn Executor>,
155 timer: Arc<dyn Timer>,
156 timeout: Duration,
157}
158
159impl<E: ProtocolError> Clone for Client<E> {
160 fn clone(&self) -> Self {
161 Self {
162 sink: self.sink.clone(),
163 pending_responses: self.pending_responses.clone(),
164 request_senders: self.request_senders.clone(),
165 notification_senders: self.notification_senders.clone(),
166 pong_waiters: self.pong_waiters.clone(),
167 ping_registry: self.ping_registry.clone(),
168 reconnect_registry: self.reconnect_registry.clone(),
169 keepalive: self.keepalive.clone(),
170 force_reconnect: self.force_reconnect.clone(),
171 closed: self.closed.clone(),
172 executor: self.executor.clone(),
173 timer: self.timer.clone(),
174 timeout: self.timeout,
175 }
176 }
177}
178
179impl<E: ProtocolError> Client<E> {
180 /// Build a client over any transport - the WebSocket adapter used by `connect_1_6` is
181 /// just one implementation of `TransportSink`/`TransportStream`; tests and non-WebSocket
182 /// transports (an embedded framed link, an in-memory fake for unit tests) construct a
183 /// client the same way. `executor`/`timer` are likewise pluggable: the `tokio-runtime`
184 /// feature provides `TokioExecutor`/`TokioTimer`; embedded users supply their own (e.g.
185 /// backed by `embassy-executor`/`embassy-time`).
186 pub fn from_transport(
187 sink: Box<dyn TransportSink>,
188 stream: Box<dyn TransportStream>,
189 timeout: Duration,
190 executor: Box<dyn Executor>,
191 timer: Box<dyn Timer>,
192 ) -> Self {
193 Self::from_transport_with_config(sink, stream, executor, timer, ClientConfig::new(timeout))
194 }
195
196 /// Same as [`Client::from_transport`], but with automatic reconnect: when the transport
197 /// closes (`TransportStream::recv` returns `Ok(None)`/`Err(_)`), the background read loop
198 /// calls `reconnector.connect()` (backing off per `reconnect_policy` between failed
199 /// attempts) instead of exiting, and swaps in the new transport once one succeeds.
200 /// `reconnector: None` reproduces `from_transport`'s behavior - the read loop exits on
201 /// disconnect and the client goes quiet. `connect_1_6`/`connect_2_0_1`/`connect_2_1` use
202 /// this constructor with a WebSocket-backed `Reconnector`.
203 pub fn from_transport_with_reconnect(
204 sink: Box<dyn TransportSink>,
205 stream: Box<dyn TransportStream>,
206 timeout: Duration,
207 executor: Box<dyn Executor>,
208 timer: Box<dyn Timer>,
209 reconnector: Option<Box<dyn Reconnector>>,
210 reconnect_policy: ReconnectPolicy,
211 ) -> Self {
212 let mut config = ClientConfig::new(timeout);
213 config.reconnector = reconnector;
214 config.reconnect_policy = reconnect_policy;
215 Self::from_transport_with_config(sink, stream, executor, timer, config)
216 }
217
218 /// The constructor the other two delegate to: everything optional lives in [`ClientConfig`]
219 /// instead of a growing positional parameter list.
220 ///
221 /// Spawns two background tasks on `executor`: the read loop, and a keepalive task. The
222 /// keepalive task is spawned even when `config.keepalive` is `Disabled`, where it simply
223 /// parks until someone calls [`Client::set_ping_interval`] - otherwise a client built with
224 /// keepalive off could never have it turned on later, which is exactly what a CSMS writing
225 /// `WebSocketPingInterval` needs to do.
226 pub fn from_transport_with_config(
227 sink: Box<dyn TransportSink>,
228 mut stream: Box<dyn TransportStream>,
229 executor: Box<dyn Executor>,
230 timer: Box<dyn Timer>,
231 config: ClientConfig,
232 ) -> Self {
233 let ClientConfig {
234 timeout,
235 reconnector,
236 reconnect_policy,
237 keepalive,
238 } = config;
239
240 let sink = Arc::new(SharedMutex::new(sink));
241 let pending_responses: PendingResponses<E> = Arc::new(SharedMutex::new(BTreeMap::new()));
242 let request_senders: RequestSenders = Arc::new(SharedMutex::new(BTreeMap::new()));
243 let notification_senders: NotificationSenders = Arc::new(SharedMutex::new(BTreeMap::new()));
244 let pong_waiters: PongWaiters = Arc::new(SharedMutex::new(PongState::default()));
245 let ping_registry = Arc::new(BroadcastRegistry::new());
246 let reconnect_registry = Arc::new(BroadcastRegistry::new());
247 let keepalive_state = Arc::new(KeepaliveState {
248 interval_millis: AtomicU32::new(
249 keepalive
250 .initial_interval()
251 .map(|d| d.as_millis().min(u32::MAX as u128) as u32)
252 .unwrap_or(0),
253 ),
254 changed: Notify::new(),
255 policy: keepalive.policy(),
256 });
257 let force_reconnect = Notify::new();
258 let closed = Arc::new(AtomicBool::new(false));
259 let executor: Arc<dyn Executor> = Arc::from(executor);
260 let timer: Arc<dyn Timer> = Arc::from(timer);
261
262 let read_pending_responses = pending_responses.clone();
263 let read_request_senders = request_senders.clone();
264 let read_notification_senders = notification_senders.clone();
265 let read_pong_waiters = pong_waiters.clone();
266 let read_ping_registry = ping_registry.clone();
267 let read_reconnect_registry = reconnect_registry.clone();
268 let read_sink = sink.clone();
269 let read_timer = timer.clone();
270 let read_force_reconnect = force_reconnect.clone();
271 let read_closed = closed.clone();
272
273 // Honoring a forced reconnect means abandoning the current transport. With no
274 // reconnector there is nothing to abandon it *for*, and breaking the read loop would
275 // leave a permanently deaf client - strictly worse than an unanswered ping. So keepalive
276 // can only ever escalate to a redial when redialling is actually configured. An explicit
277 // `disconnect()` is different: it wants the loop gone, reconnector or not.
278 let honor_force_reconnect = reconnector.is_some();
279
280 executor.spawn(Box::pin(async move {
281 // Persists across connections on purpose. Resetting it per connection is what made a
282 // peer that accepts-then-immediately-closes a zero-delay hot loop: every dial
283 // "succeeded", so the backoff never advanced past its first step. It is reset by
284 // evidence that a connection actually works (see the inbound-event arm below), not by
285 // the mere fact that a dial completed.
286 let mut attempt = 0u32;
287
288 'connection: loop {
289 let mut reason = LoopExit::Eof;
290 loop {
291 // `recv` is always raced against the wake signal, even with no reconnector,
292 // so `disconnect()` can pull the loop out of a `recv` that would otherwise
293 // park until the OS TCP timeout. This is why the cancel-safety contract on
294 // `TransportStream::recv` is unconditional.
295 let event = match with_cancel(stream.recv(), read_force_reconnect.wait()).await
296 {
297 Ok(event) => event,
298 Err(_) => {
299 // Woken on purpose. An explicit shutdown outranks everything.
300 if read_closed.load(Ordering::SeqCst) {
301 reason = LoopExit::Shutdown;
302 break;
303 }
304 if honor_force_reconnect {
305 reason = LoopExit::Forced;
306 break;
307 }
308 // Nothing to redial with, so keep reading rather than going deaf.
309 continue;
310 }
311 };
312
313 let event = match event {
314 Ok(Some(event)) => event,
315 Ok(None) | Err(_) => break,
316 };
317
318 // Anything arriving proves this connection is real and not an
319 // accept-then-close, so stop escalating the backoff. Dialling successfully is
320 // deliberately *not* treated as proof - that is exactly what the hot-loop bug
321 // mistook for a healthy connection.
322 attempt = 0;
323
324 match event {
325 TransportEvent::Frame(frame) => {
326 handle_frame::<E>(
327 &frame,
328 &read_pending_responses,
329 &read_request_senders,
330 &read_notification_senders,
331 &read_sink,
332 )
333 .await;
334 }
335 TransportEvent::Ping(payload) => {
336 read_ping_registry.notify_all().await;
337 let mut lock = read_sink.lock().await;
338 // RFC 6455: a pong must echo the triggering ping's payload.
339 let _ = lock.pong(payload).await;
340 }
341 TransportEvent::Pong(payload) => {
342 let token = <[u8; 8]>::try_from(payload.as_slice())
343 .map(u64::from_be_bytes)
344 .ok();
345 let mut lock = read_pong_waiters.lock().await;
346 match token.and_then(|token| lock.waiters.remove(&token)) {
347 Some(waiter) => waiter.send(()),
348 // Either an unsolicited pong, or one whose ping already timed
349 // out. Both are dropped rather than resolving some other
350 // outstanding ping, which is what the old positional matching
351 // did wrong.
352 None => tracing::debug!(
353 "ocpp-client: pong matched no outstanding ping"
354 ),
355 }
356 }
357 }
358 }
359
360 // The EOF path lands here too, and `disconnect()` produces one: it closes the
361 // sink, which on a real transport ends the stream. Without this check that EOF
362 // is indistinguishable from a dropped connection, and the reconnector undoes the
363 // shutdown the caller just asked for.
364 if matches!(reason, LoopExit::Shutdown) || read_closed.load(Ordering::SeqCst) {
365 tracing::info!("ocpp-client: read loop stopped after an explicit disconnect");
366 read_pong_waiters.lock().await.waiters.clear();
367 break 'connection;
368 }
369
370 let Some(reconnector) = reconnector.as_ref() else {
371 break 'connection;
372 };
373
374 if matches!(reason, LoopExit::Forced) {
375 // Courtesy close so a peer that *is* still listening sees a clean shutdown
376 // rather than a vanished socket. Bounded, because the whole reason we got
377 // here is that this socket may be dead - an unbounded close could park the
378 // read loop for as long as the OS TCP timeout, which is precisely what
379 // forcing a reconnect was meant to avoid.
380 let mut lock = read_sink.lock().await;
381 let _ = with_timeout(read_timer.as_ref(), timeout, lock.close()).await;
382 }
383
384 // Outstanding pings belong to the connection that just died; a pong can never
385 // arrive for them now. Leaving them would also mean the keepalive task's first
386 // ping on the new connection competes with corpses from the old one.
387 read_pong_waiters.lock().await.waiters.clear();
388
389 loop {
390 // Wait *before* dialling, not only after a failed dial. The old order meant a
391 // dial that succeeded and then instantly dropped never waited at all.
392 let delay = reconnect_policy.jittered_delay_for(attempt);
393 attempt = attempt.saturating_add(1);
394
395 // Interruptible, so `disconnect()` during a long backoff takes effect now
396 // rather than after up to `max_delay`. A wake that isn't a shutdown (keepalive
397 // giving up on the connection we are already replacing) just shortens this
398 // one wait - it can't recur faster than the keepalive interval, so it can't
399 // reopen the hot loop.
400 if with_cancel(read_timer.delay(delay), read_force_reconnect.wait())
401 .await
402 .is_err()
403 && read_closed.load(Ordering::SeqCst)
404 {
405 tracing::info!("ocpp-client: reconnect abandoned after a disconnect");
406 break 'connection;
407 }
408
409 if read_closed.load(Ordering::SeqCst) {
410 break 'connection;
411 }
412
413 match reconnector.connect().await {
414 Ok((new_sink, new_stream)) => {
415 *read_sink.lock().await = new_sink;
416 stream = new_stream;
417 tracing::info!(attempt, "ocpp-client: reconnected");
418 read_reconnect_registry.notify_all().await;
419 break;
420 }
421 Err(err) => {
422 tracing::warn!(attempt, error = %err, "ocpp-client: reconnect attempt failed");
423 }
424 }
425 }
426 }
427 }));
428
429 let client = Self {
430 sink,
431 pending_responses,
432 request_senders,
433 notification_senders,
434 pong_waiters,
435 ping_registry,
436 reconnect_registry,
437 keepalive: keepalive_state,
438 force_reconnect,
439 closed,
440 executor: executor.clone(),
441 timer,
442 timeout,
443 };
444
445 let keepalive_client = client.clone();
446 executor.spawn(Box::pin(
447 async move { keepalive_loop(keepalive_client).await },
448 ));
449
450 client
451 }
452
453 /// Send a CALL for `A` and wait for the matching CALLRESULT/CALLERROR.
454 pub async fn call<A: Action>(
455 &self,
456 request: A::Request,
457 ) -> Result<A::Response, ClientError<E>> {
458 let response = self.do_send_request(request, A::NAME).await?;
459 Ok(response)
460 }
461
462 /// Register a handler for CALLs the other side sends for action `A`. Replaces any
463 /// previously registered handler for the same action.
464 pub async fn on<A, F, FF>(&self, mut callback: F)
465 where
466 A: Action,
467 F: FnMut(A::Request, Self) -> FF + Send + Sync + 'static,
468 FF: Future<Output = Result<A::Response, E>> + Send,
469 {
470 let chan: Chan<(String, Value)> = Chan::new();
471 {
472 let mut lock = self.request_senders.lock().await;
473 // Retire the handler being replaced. Overwriting the map entry alone only made the
474 // old task unreachable, not finished - it stayed parked on a channel nothing could
475 // ever deliver to, leaking one task per re-registration.
476 if let Some(previous) = lock.insert(A::NAME.to_string(), chan.clone()) {
477 previous.close();
478 }
479 }
480
481 let client = self.clone();
482 self.executor.spawn(Box::pin(async move {
483 while let Some((message_id, payload)) = chan.recv().await {
484 match serde_json::from_value::<A::Request>(payload) {
485 Ok(request) => {
486 let response = callback(request, client.clone()).await;
487 client.do_send_response(response, &message_id).await;
488 }
489 Err(_) => {
490 let error =
491 E::not_implemented(&format!("Failed to parse payload for {}", A::NAME));
492 client
493 .do_send_response::<A::Response>(Err(error), &message_id)
494 .await;
495 }
496 }
497 }
498 }));
499 }
500
501 /// Wait for exactly one CALL for action `A` (bounded by the client's timeout), answer
502 /// it with `callback`, and return the parsed request. Only useful in tests.
503 ///
504 /// The registration is removed again on the way out, whichever way that is. Leaving it in
505 /// place left the action bound to a channel with no reader, so any *later* CALL for it was
506 /// queued and silently forgotten - the peer got no CALLRESULT and no CALLERROR either, which
507 /// looks exactly like the client having hung.
508 ///
509 /// Note this does not restore a handler that [`Client::on`] had registered for the same
510 /// action beforehand; registering replaces, as `on`'s own docs say.
511 #[cfg(feature = "test")]
512 pub async fn wait_for<A, F, FF>(&self, mut callback: F) -> Result<A::Request, ClientError<E>>
513 where
514 A: Action,
515 F: FnMut(A::Request, Self) -> FF + Send + Sync + 'static,
516 FF: Future<Output = Result<A::Response, E>> + Send,
517 {
518 let chan: Chan<(String, Value)> = Chan::new();
519 {
520 let mut lock = self.request_senders.lock().await;
521 if let Some(previous) = lock.insert(A::NAME.to_string(), chan.clone()) {
522 previous.close();
523 }
524 }
525
526 let outcome = match with_timeout(self.timer.as_ref(), self.timeout, chan.recv()).await {
527 Ok(Some((message_id, payload))) => {
528 match serde_json::from_value::<A::Request>(payload.clone()) {
529 Ok(for_callback) => {
530 let response = callback(for_callback, self.clone()).await;
531 self.do_send_response(response, &message_id).await;
532 serde_json::from_value(payload).map_err(ClientError::Decode)
533 }
534 Err(err) => Err(ClientError::Decode(err)),
535 }
536 }
537 // The channel was closed out from under us - another registration for the same
538 // action superseded this one.
539 Ok(None) => Err(ClientError::Closed),
540 Err(_) => Err(ClientError::Timeout),
541 };
542
543 {
544 let mut lock = self.request_senders.lock().await;
545 // Only remove our own registration: something else may have replaced it while we
546 // were waiting, and tearing that out would break whoever installed it.
547 if lock
548 .get(A::NAME)
549 .is_some_and(|current| current.is_same(&chan))
550 {
551 lock.remove(A::NAME);
552 }
553 }
554
555 outcome
556 }
557
558 /// Send a `SEND` (OCPP-J 2.1 only) fire-and-forget message: writes the frame and returns as
559 /// soon as the transport accepts it - no waiter, no timeout, since the spec forbids the
560 /// receiver from ever replying to a `SEND`.
561 pub async fn send_notification<A: SendAction>(
562 &self,
563 payload: A::Payload,
564 ) -> Result<(), ClientError<E>> {
565 if self.is_closed() {
566 return Err(ClientError::Closed);
567 }
568 let message_id = Uuid::new_v4();
569 let payload = serde_json::to_value(&payload).map_err(ClientError::Decode)?;
570 let send = RawSend(
571 MESSAGE_TYPE_SEND,
572 message_id.to_string(),
573 A::NAME.to_string(),
574 payload,
575 );
576 let frame = serde_json::to_string(&send).map_err(ClientError::Decode)?;
577
578 let mut lock = self.sink.lock().await;
579 lock.send(frame).await.map_err(ClientError::Transport)
580 }
581
582 /// Register a handler for `SEND` (OCPP-J 2.1 only) messages of action `A`. Unlike
583 /// [`Client::on`], `callback` returns nothing - the spec forbids replying to a `SEND`, so
584 /// there's no response to send back. Replaces any previously registered handler for the
585 /// same action.
586 pub async fn on_notification<A, F, FF>(&self, mut callback: F)
587 where
588 A: SendAction,
589 F: FnMut(A::Payload, Self) -> FF + Send + Sync + 'static,
590 FF: Future<Output = ()> + Send,
591 {
592 let chan: Chan<Value> = Chan::new();
593 {
594 let mut lock = self.notification_senders.lock().await;
595 if let Some(previous) = lock.insert(A::NAME.to_string(), chan.clone()) {
596 previous.close();
597 }
598 }
599
600 let client = self.clone();
601 self.executor.spawn(Box::pin(async move {
602 while let Some(payload) = chan.recv().await {
603 match serde_json::from_value::<A::Payload>(payload) {
604 Ok(payload) => callback(payload, client.clone()).await,
605 Err(err) => {
606 tracing::warn!(error = %err, action = A::NAME, "ocpp-client: failed to parse SEND payload");
607 }
608 }
609 }
610 }));
611 }
612
613 /// Send one ping and wait for the matching pong, bounded by the client's timeout.
614 ///
615 /// The pong is matched by correlation token, not arrival order: the ping carries an
616 /// 8-byte token as its payload and only a pong echoing that exact payload resolves this
617 /// call. RFC 6455 requires peers to echo ping payloads, so this is exact against any
618 /// compliant server; a pong that echoes something else is ignored, and this call times out.
619 ///
620 /// This is the manual, one-shot ping. For scheduled keepalive - including detecting a peer
621 /// that has stopped answering and forcing a redial - see [`Client::set_ping_interval`] and
622 /// `KeepaliveBehavior`.
623 pub async fn send_ping(&self) -> Result<(), ClientError<E>> {
624 self.send_ping_with_timeout(self.timeout).await
625 }
626
627 /// [`Client::send_ping`] with an explicit pong deadline, so keepalive can use
628 /// `KeepalivePolicy::timeout` instead of the client's request timeout.
629 async fn send_ping_with_timeout(&self, timeout: Duration) -> Result<(), ClientError<E>> {
630 if self.is_closed() {
631 return Err(ClientError::Closed);
632 }
633 let waiter = OneShot::new();
634 let token = {
635 let mut lock = self.pong_waiters.lock().await;
636 let token = lock.next_token;
637 lock.next_token = lock.next_token.wrapping_add(1);
638 lock.waiters.insert(token, waiter.clone());
639 token
640 };
641
642 let sent = {
643 let mut lock = self.sink.lock().await;
644 lock.ping(Vec::from(token.to_be_bytes())).await
645 };
646 if let Err(err) = sent {
647 self.forget_ping(token).await;
648 return Err(ClientError::Transport(err));
649 }
650
651 match with_timeout(self.timer.as_ref(), timeout, waiter.wait()).await {
652 Ok(()) => Ok(()),
653 Err(_) => {
654 // Drop our own waiter. Skipping this is what used to poison the client: an
655 // abandoned waiter sat in the table forever, and (under the old positional
656 // matching) stole the next ping's pong.
657 self.forget_ping(token).await;
658 Err(ClientError::Timeout)
659 }
660 }
661 }
662
663 async fn forget_ping(&self, token: u64) {
664 self.pong_waiters.lock().await.waiters.remove(&token);
665 }
666
667 /// How many requests are still waiting for a CALLRESULT/CALLERROR.
668 ///
669 /// Test-only instrumentation: this table is bookkeeping that should return to zero once every
670 /// request has either been answered or given up, and a leak in it is otherwise invisible from
671 /// outside - it shows up only as memory growth on a charge point that has been running for
672 /// weeks. `tests/ocpp_1_6_bookkeeping.rs` asserts on it.
673 #[cfg(feature = "test")]
674 pub async fn pending_request_count(&self) -> usize {
675 self.pending_responses.lock().await.len()
676 }
677
678 /// How many pings are still waiting for a pong. Test-only, same rationale as
679 /// [`Client::pending_request_count`].
680 #[cfg(feature = "test")]
681 pub async fn pending_ping_count(&self) -> usize {
682 self.pong_waiters.lock().await.waiters.len()
683 }
684
685 /// The keepalive ping interval currently in force, or `None` when keepalive is off.
686 ///
687 /// This is the value to report for `OCPPCommCtrlr.WebSocketPingInterval` (2.0.1/2.1) or the
688 /// `WebSocketPingInterval` configuration key (1.6) - `None` maps to the spec's `0`. Cheap
689 /// and non-blocking, so a `GetVariables`/`GetConfiguration` handler can call it directly.
690 pub fn ping_interval(&self) -> Option<Duration> {
691 self.keepalive.interval()
692 }
693
694 /// Change the keepalive ping interval on a live connection, for a CSMS writing
695 /// `WebSocketPingInterval` via `SetVariables`/`ChangeConfiguration`.
696 ///
697 /// `None` - or `Some(Duration::ZERO)`, matching the spec's `0` - disables pinging. Takes
698 /// effect immediately: the keepalive task is woken rather than finishing the interval it was
699 /// already waiting out, so shortening a 1-hour interval doesn't take up to an hour to apply.
700 /// Enabling works even on a client built with `KeepaliveBehavior::Disabled`.
701 pub fn set_ping_interval(&self, interval: Option<Duration>) {
702 let interval = interval.filter(|d| !d.is_zero());
703 self.keepalive.set_interval(interval);
704 tracing::info!(
705 interval_millis = interval.map(|d| d.as_millis() as u64).unwrap_or(0),
706 "ocpp-client: keepalive ping interval updated"
707 );
708 }
709
710 /// Abandon the current transport and redial, without waiting for it to notice it is dead.
711 ///
712 /// This is what keepalive escalates to after `KeepalivePolicy::max_missed` unanswered pings,
713 /// exposed because a caller with its own liveness signal (an application-level heartbeat
714 /// going unanswered, say) has the same problem. A half-open TCP connection can otherwise
715 /// keep the read loop parked until the OS timeout, which no amount of protocol-level
716 /// bookkeeping can shorten.
717 ///
718 /// No-op when the client was built without a reconnector: there would be nothing to redial
719 /// with, and dropping the current connection anyway would just make the client deaf.
720 pub fn force_reconnect(&self) {
721 if self.is_closed() {
722 return;
723 }
724 self.force_reconnect.notify();
725 }
726
727 pub async fn on_ping<
728 F: FnMut(Self) -> FF + Send + Sync + 'static,
729 FF: Future<Output = ()> + Send,
730 >(
731 &self,
732 mut callback: F,
733 ) {
734 let signal = self.ping_registry.subscribe().await;
735 let client = self.clone();
736 self.executor.spawn(Box::pin(async move {
737 loop {
738 signal.wait().await;
739 callback(client.clone()).await;
740 }
741 }));
742 }
743
744 /// Register a callback that fires every time the background read loop redials
745 /// successfully after a disconnect (see [`Client::from_transport_with_reconnect`]). Never
746 /// fires for the initial connection, only for later reconnects - the initial `Client` is
747 /// already handed back post-connect, so callers run their own post-connect setup (e.g.
748 /// `BootNotification`) right after `connect_1_6`/`from_transport_with_reconnect` returns.
749 /// This is the hook for redoing that setup (or resyncing any other session state) after a
750 /// dropped-and-restored connection; this crate does not re-run `BootNotification` or replay
751 /// any state on its own.
752 pub async fn on_reconnect<
753 F: FnMut(Self) -> FF + Send + Sync + 'static,
754 FF: Future<Output = ()> + Send,
755 >(
756 &self,
757 mut callback: F,
758 ) {
759 let signal = self.reconnect_registry.subscribe().await;
760 let client = self.clone();
761 self.executor.spawn(Box::pin(async move {
762 loop {
763 signal.wait().await;
764 callback(client.clone()).await;
765 }
766 }));
767 }
768
769 /// Shut this client down for good: close the transport, stop the read loop, stop keepalive,
770 /// and do **not** redial.
771 ///
772 /// The shutdown is sticky and takes precedence over every automatic recovery path. That
773 /// matters because closing the transport looks exactly like a dropped connection from the read
774 /// loop's side - it previously produced an EOF the reconnector dutifully redialled, so on the
775 /// default [`crate::ConnectOptions`] (reconnect enabled) there was no way to stop a client at
776 /// all. After this returns:
777 ///
778 /// - the read loop has been told to exit rather than redial, whether it was parked in `recv`
779 /// or sees the EOF from the close;
780 /// - the keepalive task stops pinging, and [`Client::set_ping_interval`] cannot restart it;
781 /// - [`Client::force_reconnect`] is a no-op;
782 /// - further `call`/`send_*`/`send_ping` return [`ClientError::Closed`] instead of writing to
783 /// a dead transport and waiting out the timeout.
784 ///
785 /// Idempotent: calling it again is a no-op returning `Ok(())`. Reconnecting afterwards means
786 /// building a new `Client`.
787 ///
788 /// This only covers *deliberate* shutdown. An unrequested drop is still redialled as before.
789 pub async fn disconnect(&self) -> Result<(), ClientError<E>> {
790 if self.closed.swap(true, Ordering::SeqCst) {
791 return Ok(());
792 }
793
794 // Both loops re-read `closed` as soon as they wake, so the flag has to be set first.
795 self.force_reconnect.notify();
796 self.keepalive.changed.notify();
797
798 let mut lock = self.sink.lock().await;
799 lock.close().await.map_err(ClientError::Transport)
800 }
801
802 /// Whether [`Client::disconnect`] has been called.
803 ///
804 /// This reflects deliberate shutdown only - it stays `false` while a connection is dropped and
805 /// being redialled, because such a client is still live and will resume on its own. There is
806 /// deliberately no "is the socket up right now" accessor: it would be stale the moment it
807 /// returned, and [`Client::on_reconnect`] is the reliable way to observe reconnection.
808 pub fn is_closed(&self) -> bool {
809 self.closed.load(Ordering::SeqCst)
810 }
811
812 async fn do_send_request<P: Serialize, R: DeserializeOwned>(
813 &self,
814 request: P,
815 action: &str,
816 ) -> Result<R, ClientError<E>> {
817 // Fail fast rather than writing to a closed transport and then waiting out the full
818 // request timeout for a response that cannot arrive.
819 if self.is_closed() {
820 return Err(ClientError::Closed);
821 }
822 let message_id = Uuid::new_v4();
823 let payload = serde_json::to_value(&request).map_err(ClientError::Decode)?;
824 let call = RawCall(
825 MESSAGE_TYPE_CALL,
826 message_id.to_string(),
827 action.to_string(),
828 payload,
829 );
830 let frame = serde_json::to_string(&call).map_err(ClientError::Decode)?;
831
832 let waiter = OneShot::new();
833 {
834 let mut lock = self.pending_responses.lock().await;
835 lock.insert(message_id, waiter.clone());
836 }
837
838 let sent = {
839 let mut lock = self.sink.lock().await;
840 lock.send(frame).await
841 };
842 if let Err(err) = sent {
843 // Never reached the wire, so no response can ever arrive to clear this entry.
844 self.forget_pending(message_id).await;
845 return Err(ClientError::Transport(err));
846 }
847
848 let result = match with_timeout(self.timer.as_ref(), self.timeout, waiter.wait()).await {
849 Ok(result) => result,
850 Err(_) => {
851 // Drop our own waiter. `handle_frame` only removes entries when a response
852 // actually arrives, so without this every timed-out request left one behind
853 // permanently - unbounded growth on a charge point that has been up for weeks
854 // with an intermittent CSMS. Same failure the pong table used to have.
855 self.forget_pending(message_id).await;
856 return Err(ClientError::Timeout);
857 }
858 };
859
860 match result {
861 Ok(value) => serde_json::from_value(value).map_err(ClientError::Decode),
862 Err(e) => Err(ClientError::Protocol(e)),
863 }
864 }
865
866 async fn forget_pending(&self, message_id: Uuid) {
867 self.pending_responses.lock().await.remove(&message_id);
868 }
869
870 async fn do_send_response<R: Serialize>(&self, response: Result<R, E>, message_id: &str) {
871 let frame = match response {
872 Ok(r) => match serde_json::to_value(r) {
873 Ok(value) => serde_json::to_string(&RawResult(
874 MESSAGE_TYPE_RESULT,
875 message_id.to_string(),
876 value,
877 )),
878 Err(e) => return log_send_error(e),
879 },
880 Err(e) => serde_json::to_string(&RawError(
881 MESSAGE_TYPE_ERROR,
882 message_id.to_string(),
883 e.code().to_string(),
884 e.description().to_string(),
885 e.details().to_owned(),
886 )),
887 };
888
889 match frame {
890 Ok(frame) => {
891 let mut lock = self.sink.lock().await;
892 if let Err(err) = lock.send(frame).await {
893 tracing::warn!(error = %err, "ocpp-client: failed to send response");
894 }
895 }
896 Err(err) => {
897 tracing::error!(error = %err, "ocpp-client: failed to encode response");
898 }
899 }
900 }
901}
902
903/// The scheduled-ping task spawned by [`Client::from_transport_with_config`].
904///
905/// Runs for the client's whole life, including across reconnects - `send_ping` writes through
906/// `Client`'s shared sink handle, which the read loop swaps in place on redial, so nothing here
907/// has to know a reconnect happened.
908///
909/// Sleeping is `with_timeout(timer, interval, changed.wait())` rather than a plain delay: it is
910/// already exactly "wait out the interval, but wake early if reconfigured", so `set_ping_interval`
911/// applies immediately without a second timer or a polling granularity.
912async fn keepalive_loop<E: ProtocolError>(client: Client<E>) {
913 let state = client.keepalive.clone();
914 let policy = state.policy;
915 let misses_allowed = policy.misses_allowed();
916 let ping_timeout = policy.timeout.unwrap_or(client.timeout);
917 let mut missed = 0u32;
918
919 loop {
920 // `disconnect()` sets this and then notifies `changed`, so both waits below wake up here.
921 if client.is_closed() {
922 return;
923 }
924
925 let Some(interval) = state.interval() else {
926 // Keepalive off: park until someone turns it on (or the client shuts down).
927 state.changed.wait().await;
928 missed = 0;
929 continue;
930 };
931
932 if with_timeout(client.timer.as_ref(), interval, state.changed.wait())
933 .await
934 .is_ok()
935 {
936 // Reconfigured mid-wait; re-read the interval rather than pinging on the old one.
937 missed = 0;
938 continue;
939 }
940
941 // The interval could have been zeroed - or the client shut down - between the wait
942 // ending and here.
943 if client.is_closed() {
944 return;
945 }
946 if state.interval().is_none() {
947 continue;
948 }
949
950 match client.send_ping_with_timeout(ping_timeout).await {
951 Ok(()) => missed = 0,
952 Err(err) => {
953 missed = missed.saturating_add(1);
954 tracing::warn!(
955 missed,
956 misses_allowed,
957 error = %err,
958 "ocpp-client: keepalive ping went unanswered"
959 );
960 if missed >= misses_allowed {
961 missed = 0;
962 tracing::error!(
963 misses_allowed,
964 "ocpp-client: peer stopped answering pings, forcing a redial"
965 );
966 client.force_reconnect();
967 }
968 }
969 }
970 }
971}
972
973fn log_send_error(err: serde_json::Error) {
974 tracing::error!(error = %err, "ocpp-client: failed to encode response payload");
975}
976
977async fn handle_frame<E: ProtocolError>(
978 frame: &str,
979 pending_responses: &PendingResponses<E>,
980 request_senders: &RequestSenders,
981 notification_senders: &NotificationSenders,
982 sink: &Arc<SharedMutex<Box<dyn TransportSink>>>,
983) {
984 let value: Value = match serde_json::from_str(frame) {
985 Ok(v) => v,
986 Err(err) => {
987 tracing::warn!(error = %err, "ocpp-client: received malformed frame");
988 return;
989 }
990 };
991
992 let Value::Array(items) = value else {
993 tracing::warn!("ocpp-client: a message should be a JSON array");
994 return;
995 };
996 let Some(Value::Number(message_type)) = items.first() else {
997 tracing::warn!("ocpp-client: missing message type id");
998 return;
999 };
1000 let Some(message_type) = message_type.as_u64() else {
1001 tracing::warn!("ocpp-client: message type id must be an integer");
1002 return;
1003 };
1004
1005 match message_type {
1006 MESSAGE_TYPE_CALL => {
1007 let call: RawCall = match serde_json::from_str(frame) {
1008 Ok(c) => c,
1009 Err(err) => {
1010 tracing::warn!(error = %err, "ocpp-client: failed to parse CALL");
1011 return;
1012 }
1013 };
1014 let action = &call.2;
1015 let sender = {
1016 let lock = request_senders.lock().await;
1017 lock.get(action).cloned()
1018 };
1019 match sender {
1020 Some(sender) => {
1021 sender.send((call.1, call.3)).await;
1022 }
1023 None => {
1024 let error =
1025 E::not_implemented(&format!("Action '{action}' is not implemented"));
1026 let payload = RawError(
1027 MESSAGE_TYPE_ERROR,
1028 call.1,
1029 error.code().to_string(),
1030 error.description().to_string(),
1031 error.details().to_owned(),
1032 );
1033 if let Ok(frame) = serde_json::to_string(&payload) {
1034 let mut lock = sink.lock().await;
1035 let _ = lock.send(frame).await;
1036 }
1037 }
1038 }
1039 }
1040 MESSAGE_TYPE_RESULT => {
1041 let result: RawResult = match serde_json::from_str(frame) {
1042 Ok(r) => r,
1043 Err(err) => {
1044 tracing::warn!(error = %err, "ocpp-client: failed to parse CALLRESULT");
1045 return;
1046 }
1047 };
1048 let Ok(id) = Uuid::parse_str(&result.1) else {
1049 return;
1050 };
1051 let mut lock = pending_responses.lock().await;
1052 if let Some(sender) = lock.remove(&id) {
1053 sender.send(Ok(result.2));
1054 }
1055 }
1056 MESSAGE_TYPE_ERROR => {
1057 let error: RawError = match serde_json::from_str(frame) {
1058 Ok(e) => e,
1059 Err(err) => {
1060 tracing::warn!(error = %err, "ocpp-client: failed to parse CALLERROR");
1061 return;
1062 }
1063 };
1064 let Ok(id) = Uuid::parse_str(&error.1) else {
1065 return;
1066 };
1067 let mut lock = pending_responses.lock().await;
1068 if let Some(sender) = lock.remove(&id) {
1069 sender.send(Err(E::from_wire(&error.2, &error.3, error.4)));
1070 }
1071 }
1072 MESSAGE_TYPE_SEND => {
1073 let send: RawSend = match serde_json::from_str(frame) {
1074 Ok(s) => s,
1075 Err(err) => {
1076 tracing::warn!(error = %err, "ocpp-client: failed to parse SEND");
1077 return;
1078 }
1079 };
1080 let action = &send.2;
1081 let sender = {
1082 let lock = notification_senders.lock().await;
1083 lock.get(action).cloned()
1084 };
1085 match sender {
1086 Some(sender) => sender.send(send.3).await,
1087 None => {
1088 tracing::warn!(action = %action, "ocpp-client: SEND for unhandled action");
1089 }
1090 }
1091 }
1092 other => {
1093 tracing::warn!(message_type = other, "ocpp-client: unknown message type id");
1094 }
1095 }
1096}