Skip to content

Releases: flowionab/ocpp-client

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 08 Aug 20:49

Tracks ocpp-types 0.3.0. A dependency-only release: no source file in src/ changed, and the
whole test suite passed against the new version unedited.

Changed

  • BREAKING (dependency): ocpp-types updated to 0.3.0. Upstream's own release is purely
    additive - no message type, field type or action changed shape, and all 39 / 64 / 91 actions
    are the same ones - so no code in this crate needed editing and no call site here does
    either
    . It is called out as breaking only because ocpp-types is a public dependency
    (pub use ocpp_types;): a consumer that also names ocpp-types in its own Cargo.toml must
    move from 0.2 to 0.3 in the same step, or the two copies are distinct types to the
    compiler. A consumer that reaches the types only through ocpp_client::ocpp_types::.. - the
    spelling src/lib.rs recommends precisely so this stays a non-event - needs to change nothing.

Added

  • A validate feature, forwarding ocpp-types' own. That adds a Validate trait to every
    message type covering the spec constraints the types cannot carry: maxLength on fields too
    large to inline as a heapless::String (certificates, CSRs, OCSP results), plus every
    minItems, minimum, maximum and multipleOf in the schemas. Off by default.

  • From<ValidationError> for OCPP1_6Error, OCPP2_0_1Error and OCPP2_1Error (same
    feature). ocpp-types classifies a violation as a property- or occurrence-class breach but
    leaves the wire code to the caller's version, and the versions disagree: OCPP 1.6J's RPC error
    table spells it OccurenceConstraintViolation, missing an r that 2.0.1 and 2.1 restored.
    These impls pick the right one, use upstream's rendering (which names the failing field) as the
    CALLERROR description, and put the JSON path in errorDetails under "path" so a peer can
    match on it without parsing prose. A handler can now reject a bad payload with
    request.validate()? and get the correct code on the wire.
    See tests/validation_error_mapping.rs.

    Validation is never automatic. It is deliberately not wired into Client::call: that would
    require a Validate bound on Action::Request, and a trait bound that appears only when a
    feature is enabled is not additive - one crate in the graph enabling validate would break an
    unrelated crate's custom Action impl, which src/action.rs documents as a supported
    extension point. Callers validate explicitly, which is one line and composes with their own
    error type. The rationale is recorded in PRODUCTION_READINESS.md item 5.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 08 Aug 18:54

Tracks ocpp-types 0.2.0. Everything below follows from that release; the engine itself
(src/client.rs, envelope.rs, transport.rs) needed no changes at all.

Added

  • The eleven OCPP 1.6 security-whitepaper actions, which ocpp-types 0.2.0 was the first
    release to define: CertificateSigned, DeleteCertificate, ExtendedTriggerMessage,
    GetInstalledCertificateIds, GetLog, InstallCertificate, LogStatusNotification,
    SecurityEventNotification, SignCertificate, SignedFirmwareStatusNotification and
    SignedUpdateFirmware. 1.6 now wires up 39 actions rather than 28; 2.0.1 (64) and 2.1 (91) are
    unchanged. Each gets the usual send_*/on_*/wait_for_* trio, and
    tests/ocpp_1_6_security_actions.rs covers one round trip apiece.
  • 2.x action markers take the customData type as a parameter: Reset<AcmeExtension>,
    Heartbeat<NoCustomData>, and so on, defaulting to the specification's CustomData. A
    deployment with a vendor extension richer than a bare vendorId can now read and write it
    through Client::call/Client::on without hand-writing an Action impl. See
    tests/custom_data_generics.rs.
  • A chrono feature, forwarding to ocpp-types' own: From/Into between OcppTimestamp
    and chrono::DateTime for callers that already keep time in chrono. Off by default, and chrono
    never touches the wire path.

Changed

  • BREAKING: every dateTime field is now ocpp_types::OcppTimestamp instead of String.
    Construct them with OcppTimestamp::parse_rfc3339(..) (or From<chrono::DateTime> under the
    new chrono feature) and compare against a parsed value rather than a string literal. Beyond
    the type, this changes behaviour twice over: the client now validates what the CSMS sends, so
    a malformed dateTime surfaces as ClientError::Decode where it used to be handed through as a
    string; and the value is an instant, so two timestamps naming the same moment in different UTC
    offsets compare equal. Fractional seconds and non-UTC offsets both survive the round trip -
    tests/ocpp_1_6_timestamps.rs pins all of it.
  • BREAKING: 2.0.1 and 2.1 message types carry a customData type parameter. The generated
    send_*/on_*/wait_for_* methods stay concrete at CustomData, so ordinary call sites are
    unaffected, but a 2.x request or response built in a let binding with no expected type now
    needs an annotation (let request: ResetRequest = ResetRequest { .. }) - a defaulted type
    parameter does not participate in inference. The methods were deliberately left non-generic for
    exactly this reason; the marker types carry the parameter instead.
  • BREAKING: ocpp_types::v16::common::RequestedMessage is now
    TriggerMessageRequestRequestedMessage
    , renamed upstream to make room for
    ExtendedTriggerMessageRequestRequestedMessage. A pure rename, no variants changed.
  • BREAKING: 48 2.x string fields whose length the specification leaves to a configuration
    variable are now String instead of heapless::String<N>
    - certificates, certificate
    chains, CSRs, OCSP results, MessageContent.content and the like. Construction drops from
    "...".try_into().unwrap() to "...".into(). In the other direction, 89 dateTime fields and
    8 more 2.1 date/time-of-day fields (TariffConditions.start_time_of_day and friends) left
    String for OcppTimestamp/OcppDate/OcppTimeOfDay.
  • ocpp-types updated to 0.2.0. Its alloc feature is now upstream's default; this crate still
    names its features explicitly, so nothing about the build changes.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 08 Aug 16:41

Added

  • WebSocket keepalive. The client can now ping the CSMS on a schedule and, when it stops
    answering, force the connection to be redialled. Previously the client only ever replied to
    pings the server sent, so a half-open connection - a dropped NAT entry, a mobile link that went
    away without a FIN - was invisible: the read loop stayed parked in recv until the OS TCP
    timeout, and the reconnect logic added in 0.2.0 never got a chance to fire.
    • KeepalivePolicy { interval, timeout, max_missed } and KeepaliveBehavior::{Enabled, Disabled}, shaped after the existing ReconnectPolicy/ReconnectBehavior pair.
    • ConnectOptions::keepalive, defaulting to enabled at a 60-second interval tolerating one
      missed pong. See "Changed" below.
    • Client::ping_interval() and Client::set_ping_interval() - the read/write path for OCPP's
      OCPPCommCtrlr.WebSocketPingInterval (2.0.1/2.1) and the 1.6 security whitepaper's
      WebSocketPingInterval configuration key. Both are non-async so a GetVariables handler can
      call them directly, and set_ping_interval takes effect immediately rather than after the
      interval already being waited out expires. This is what previously forced consumers to report a
      hardcoded 0: the library owned no interval to report.
    • Client::force_reconnect() - abandon the current transport and redial now, for callers with
      their own liveness signal. This is what keepalive escalates to after max_missed unanswered
      pings. No-op without a configured reconnector.
    • ClientConfig and Client::from_transport_with_config, so options live in a struct instead of
      a positional parameter list. from_transport_with_reconnect had already reached seven
      arguments; keepalive would have made it eight.
  • Client::pending_request_count() / Client::pending_ping_count() behind the test feature -
    instrumentation for the bookkeeping tables, since a leak in either is otherwise invisible from
    outside.
  • Client::is_closed() - whether disconnect() has been called. Deliberately does not report
    transient connection state: that would be stale the moment it returned, and on_reconnect is the
    reliable way to observe reconnection.
  • tests/action_coverage.rs fails the build if ocpp-types defines an action that no
    ocpp_*_action! invocation wires up. This is the class of gap that shipped in 0.2.0 (see below)
    and was invisible at compile time.

Fixed

  • Every timed-out request leaked an entry in the pending-response table. do_send_request
    registered a waiter keyed by message id, and the only code that removed it was the read loop on an
    arriving CALLRESULT/CALLERROR - so a request that timed out, or that never made it onto the wire,
    left its entry behind permanently. On a charge point running for weeks against an intermittent
    CSMS the map grew without bound. Each request now removes its own waiter on every exit path, the
    same fix the ping table already got.

    In-flight requests are still not failed early by disconnect(); they wait out their timeout. That
    is a latency wart rather than a leak, and is left as-is.

  • Replacing a handler leaked its task. Client::on (and on_notification) overwrote the map
    entry for an action, which made the previous task unreachable but not finished - it stayed parked
    forever on a channel nothing could deliver to, one leaked task per re-registration. The internal
    channel is now closable, and the superseded handler is retired: it drains whatever was already
    dispatched to it, answers those calls, and then exits.

  • wait_for left the action registered (test feature). After it returned, the action stayed
    bound to a channel with no reader, so any later CALL for it was queued and silently forgotten -
    the peer got no CALLRESULT and no CALLERROR, which is indistinguishable from the client hanging.
    It now unregisters on the way out, and only if the registration is still its own.

  • A peer that accepted the connection and then closed it immediately was redialled in a hot loop
    with no delay at all.
    ReconnectPolicy only delayed failed dials, and the attempt counter
    reset on every successful one - so a dial that completed and then instantly dropped never waited,
    unboundedly. Measured against a local server that completes the WebSocket handshake and drops:
    ~9,900 connections in 2 seconds, roughly 5k/s from a single charge point. The triggers are
    ordinary - a CSMS rejecting the charge point at the application layer, an overloaded endpoint, a
    load balancer with no live backend - and a fleet doing this at once is a self-inflicted DoS.

    Two changes fix it. The backoff delay is now applied before every dial rather than only after a
    failed one, and the attempt counter is reset by evidence that a connection actually works -
    any inbound traffic on it - instead of by the mere fact that a dial completed. So a genuine
    transient drop still reconnects after one initial_delay, while a connection that never carries
    anything escalates to max_delay as intended.

    disconnect() during a long backoff now also takes effect immediately instead of waiting out the
    remaining delay (up to max_delay, 60s by default).

  • Reconnect delays are now jittered (ReconnectPolicy::jitter, default true): each delay is
    drawn uniformly from [delay / 2, delay]. Without it, every charge point that lost the same CSMS
    retried in lockstep, so the endpoint coming back got the whole fleet at once, repeatedly. Half the
    delay is left un-jittered so a randomly tiny value can't defeat the rate bound. Randomness comes
    from uuid, already a dependency and already working on this crate's bare-metal target, so no new
    RNG dependency or embedded plumbing is involved.

    One behavior change worth noting: the first redial after a drop now waits initial_delay
    (jittered) where it previously went out immediately.

  • disconnect() was undone by the reconnector. Closing the transport is indistinguishable from
    a dropped connection from the read loop's side, so with reconnect enabled - the default on
    ConnectOptions - an explicit disconnect() produced an EOF that got dutifully redialled. On
    default options there was no way to stop a client at all.

    disconnect() now marks the client closed before closing the transport, and that flag is sticky
    and outranks every automatic recovery path: the read loop exits instead of redialling (whether it
    was parked in recv or saw the EOF), the keepalive task stops, set_ping_interval cannot restart
    it, force_reconnect() becomes a no-op, and further call/send_*/send_ping return
    ClientError::Closed immediately instead of writing to a dead transport and waiting out the
    request timeout. disconnect() is also idempotent now. An unrequested drop is still redialled
    exactly as before.

    This also gives ClientError::Closed its first construction site - the variant existed but was
    never produced by anything.

Changed

  • Ping/pong now carry payloads, and pongs are matched by correlation token. send_ping writes
    an 8-byte token as the ping's application data and only a pong echoing that exact payload
    resolves it, per RFC 6455 §5.5.2-3. Matching was previously positional, which had two failure
    modes that a scheduled keepalive would have hit constantly:

    • A ping that timed out left its waiter queued forever, permanently offsetting every later
      ping's pong by one - so every subsequent send_ping on that client timed out. Reachable
      before this release via reconnect, since the waiter table was not cleared on redial and stale
      waiters ate the first pong of the new connection.
    • An unsolicited pong (which RFC 6455 permits) caused the same one-off desync.

    Outstanding pings are now also cleared when the read loop swaps in a reconnected transport.

  • ConnectOptions::default() enables keepalive, where the previous behavior sent no
    unsolicited traffic. Same reasoning as reconnect defaulting to enabled: without keepalive a
    charge point cannot detect a half-open link at all. Set keepalive: KeepaliveBehavior::Disabled to restore the old behavior. The lower-level
    Client::from_transport* constructors still default to disabled - a caller assembling a
    client from raw transport halves has said nothing about wanting background traffic on it.

Breaking

  • rust-version = "1.87" is now declared. Not a change in what the crate needs, but if you
    were building it on an older toolchain it will now fail with a clear MSRV error rather than a
    confusing parse error. Contributors need 1.88 for the test suite (dev-dependencies).
  • ReconnectPolicy gained a jitter: bool field, which breaks struct-literal construction
    (ReconnectPolicy { initial_delay, max_delay, multiplier }). Add jitter: true for the new
    default behavior, jitter: false for exact delays, or switch to
    ..ReconnectPolicy::default().

The remaining changes affect implementors of the transport traits only - not callers of
connect_*/Client. The in-tree ocpp-transport-embassy-net is updated accordingly.

  • TransportSink::ping and TransportSink::pong now take a Vec<u8> payload, which must be
    transmitted verbatim (pong echoes the triggering ping's payload).
  • TransportEvent::Ping and TransportEvent::Pong are now Ping(Vec<u8>) / Pong(Vec<u8>).
  • TransportStream::recv is now documented as having to be cancel-safe: its future may be
    dropped mid-poll without losing an event. The read loop races it against the force-reconnect
    signal, which is how a stalled recv gets abandoned instead of parking the loop. Both in-tree
    implementations already satisfied this (futures::StreamExt::next, an embassy-net socket
    read); a third-party transport buffering partial state across an .await in a local needs to
    move that state into `sel...
Read more