ocpp/protocol/csms

A pure, sans-IO CSMS-side machine for one station connection.

Wraps the generic RPC endpoint (ocpp/protocol/endpoint) and adds the CSMS half of the provisioning rules. It is deliberately much thinner than the station machine: the CSMS answers boot notifications rather than sending them, so there is no registration lifecycle of its own to drive — it only tracks what the application’s replies have told the peer, and (optionally) enforces the peer’s obligations. It initiates no calls of its own either, so application refs pass through to the endpoint untranslated, and it adds no deadlines of its own — next_deadline is the wrapped endpoint’s.

Peer registration

The peer starts PeerUnregistered. Inbound CALLs recognised by the config’s is_boot_request are remembered by id; when the application answers one, read_boot_reply reads the decision out of the response: Accepted makes the peer PeerRegistered, Pending/Rejected makes it PeerUnregistered — the tracked state always follows the latest answered boot request, so a re-boot answered Rejected unregisters a registered peer. A reply that is not a readable boot reply (None) and a CALLERROR answer both leave the tracked state unchanged.

Enforcement

A station SHALL NOT send messages other than boot notifications before it has been accepted, and the 2.x provisioning rules (B02.FR.09) let the CSMS answer offending CALLs with a CALLERROR SecurityError. With RejectUnregistered this machine enforces exactly that: a non-boot inbound CALL while the peer is unregistered is answered on the wire with a CALLERROR SecurityError and surfaced as an UnregisteredCallRejected output instead of being delivered to the application. Boot requests always deliver, under either policy. Inbound SENDs (2.1) always deliver too — they are unconfirmed, so no error reply is possible, and whether to act on one from an unregistered peer is application policy. With DeliverAll everything is delivered and registration is merely tracked.

Time and termination

Time follows the endpoint’s convention: it enters exclusively as the now arguments to init and update (absolute monotonic milliseconds); the machine schedules nothing at rest, so init’s clock is accepted purely for uniformity across the layer. SocketClosed is the endpoint’s terminal transition: the in-flight and queued outbound calls fail with Disconnected; afterwards a new CallRequested fails immediately and every other input is a silent no-op.

Types

The CSMS side of one station connection. Construct with init, advance with update.

pub opaque type Csms(req, res)

Version-specific bindings for the CSMS machine, on top of the wrapped endpoint’s codec Config.

pub type CsmsConfig(req, res) {
  CsmsConfig(
    endpoint: endpoint.Config(req, res),
    is_boot_request: fn(req) -> Bool,
    read_boot_reply: fn(res) -> option.Option(protocol.BootStatus),
    enforcement: RegistrationEnforcement,
  )
}

Constructors

  • CsmsConfig(
      endpoint: endpoint.Config(req, res),
      is_boot_request: fn(req) -> Bool,
      read_boot_reply: fn(res) -> option.Option(protocol.BootStatus),
      enforcement: RegistrationEnforcement,
    )

    Arguments

    endpoint

    Codecs and call timeout for the wrapped RPC endpoint.

    is_boot_request

    Is this inbound request this version’s boot notification (1.6/2.x BootNotification)?

    read_boot_reply

    Read the registration decision out of the application’s reply to a boot request. None means the response is not a readable boot reply; the tracked registration state is then left unchanged.

    enforcement

    What to do with a non-boot inbound CALL from an unregistered station: reject it on the wire or deliver it anyway (see RegistrationEnforcement).

Inputs to the machine — the same vocabulary as the endpoint’s Msg. ref values belong to the application and pass through unchanged.

pub type Msg(req, res) {
  WireIn(text: String)
  CallRequested(ref: Int, request: req)
  SendRequested(request: req)
  ReplyProvided(
    id: String,
    reply: Result(res, #(rpc.ErrorCode, String)),
  )
  Tick
  SocketClosed
}

Constructors

  • WireIn(text: String)

    A text frame arrived from the peer station.

  • CallRequested(ref: Int, request: req)

    The application wants to make a CALL to the station. Subject to the endpoint’s one-in-flight rule (queued FIFO behind our own calls).

  • SendRequested(request: req)

    The application wants to emit a SEND (2.1): unconfirmed, exempt from the one-in-flight rule, never queued.

  • ReplyProvided(
      id: String,
      reply: Result(res, #(rpc.ErrorCode, String)),
    )

    The application answers the inbound CALL with this id: Ok becomes a CALLRESULT, Error a CALLERROR. Replies to boot requests also update the tracked peer registration (see the module doc).

  • Tick

    Time passed; sent by the adapter when the next_deadline timer fires.

  • SocketClosed

    The transport closed. Fails every pending and queued call with Disconnected and makes the machine terminal.

Outputs emitted by the machine: the endpoint’s outputs passed through unchanged, plus the RejectUnregistered rejection.

pub type Output(req, res) {
  WireOut(text: String)
  CallSucceeded(ref: Int, response: res)
  CallFailed(ref: Int, failure: endpoint.CallFailure)
  CallReceived(id: String, request: req)
  SendReceived(request: req)
  ReplyRejected(id: String, code: String, description: String)
  ViolationDetected(violation: endpoint.Violation)
  UnregisteredCallRejected(id: String, request: req)
}

Constructors

  • WireOut(text: String)

    Write this text frame to the socket.

  • CallSucceeded(ref: Int, response: res)

    The application’s CALL identified by ref succeeded.

  • CallFailed(ref: Int, failure: endpoint.CallFailure)

    The application’s CALL identified by ref failed.

  • CallReceived(id: String, request: req)

    The station initiated a CALL. Answer it with ReplyProvided(id, ...).

  • SendReceived(request: req)

    The station emitted a SEND. No reply is possible.

  • ReplyRejected(id: String, code: String, description: String)

    The station rejected a CALLRESULT we previously sent (2.1).

  • ViolationDetected(violation: endpoint.Violation)

    A protocol irregularity, reported by the wrapped endpoint.

  • UnregisteredCallRejected(id: String, request: req)

    RejectUnregistered: the unregistered peer sent a non-boot CALL, violating the provisioning rules. A CALLERROR SecurityError was already emitted alongside this output, and the request was NOT delivered as a CallReceived — do not answer it.

What this connection knows about the peer station’s registration: the status the application granted in its reply to the latest answered boot request.

pub type PeerRegistration {
  PeerUnregistered
  PeerRegistered
}

Constructors

  • PeerUnregistered

    No boot request on this connection has been answered yet, or the latest answered one was Pending/Rejected.

  • PeerRegistered

    The application answered the latest boot request Accepted.

What to do with a non-boot inbound CALL from a station whose boot notification has not been accepted (see the module doc’s Enforcement section). Boot requests and inbound SENDs always deliver, under either policy.

pub type RegistrationEnforcement {
  RejectUnregistered
  DeliverAll
}

Constructors

  • RejectUnregistered

    Answer the offending CALL with a CALLERROR SecurityError (B02.FR.09) and surface it as UnregisteredCallRejected instead of delivering it.

  • DeliverAll

    Deliver everything; registration is only tracked.

Values

pub fn init(
  config: CsmsConfig(req, res),
  now now: Int,
) -> #(Csms(req, res), effect.Effect(Output(req, res)))

Create the CSMS side of a fresh station connection at time now. Emits no effects — the CSMS speaks only when spoken to (or when the application makes a call) — and schedules nothing, so now is currently unused. It is accepted anyway so that every machine in this layer shares the init(config, now) shape (the station genuinely needs the clock at init) and adapters can treat them uniformly.

pub fn next_deadline(csms: Csms(req, res)) -> option.Option(Int)

The earliest instant at which this machine needs a Tick to make progress: the wrapped endpoint’s call timeout, if any — the CSMS layer adds no deadlines of its own.

pub fn peer_registration(
  csms: Csms(req, res),
) -> PeerRegistration

What this connection currently knows about the peer station’s registration.

pub fn update(
  csms: Csms(req, res),
  msg: Msg(req, res),
  now: Int,
) -> #(Csms(req, res), effect.Effect(Output(req, res)))

Advance the machine with one input at time now (absolute milliseconds, monotonic). The wrapped endpoint applies its call-timeout rule; the CSMS layer has no deadlines of its own.

Search Document