Skip to main content

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}