System Design: Depot Simulation¶
1. Problem¶
bf-depot-sim is BetterFleet's reference simulator for depot behaviour.
It exists so BetterFleet can be tested without relying on real depot hardware.
That hardware includes:
- chargers
- buses and other vehicles
- site sensors and meters
- electrical infrastructure and site state
- external partner systems that interact with BetterFleet through published protocols
Depot-sim must let operators and testers control the simulated world in real time.
That control includes:
- starting and stopping chargers and transactions
- changing charger, vehicle, sensor, and site state
- forcing faults, outages, and mismatches
- applying scripted, random, or manual scenarios
- inspecting simulator state and external protocol outcomes
The reference solution owns:
- saved simulation configurations
- resumable simulation runs
- one shared simulation world
- protocol projections over that world
- control and inspection APIs used by operators and automated tests
Depot-sim integrates with BetterFleet only through external interfaces.
It never reads BetterFleet databases, internal event streams, or in-process objects directly.
This boundary is part of the product philosophy.
If BetterFleet behaves correctly against depot-sim through published interfaces, it should behave the same against real hardware and partner systems.
The reference solution therefore needs stable simulator implementations for these interface families:
- OCPP charger interfaces
- BetterFleet site power and sensor packet interfaces
- BetterFleet telematics-facing interfaces
- OSCP
Capacity Providerinterfaces - simulator control and inspection APIs
All interface families are projections over one simulation world.
No interface family becomes the source of truth for depot state.
2. System Overview¶
Depot-sim remains one deployable service.
The reference solution is decomposed into explicit internal boundaries.
Those boundaries separate:
- simulation configuration
- simulation runtime
- scenario control
- external interface projections
- operator and test control surfaces
It does not become the source of truth for depot state.
It exposes BetterFleet-facing and operator-facing interfaces from the same underlying simulation run.
The external interface projection family includes:
OCPP Charge Point ServiceBetterFleet IoT Packet ServiceTelematics ServiceOSCP Capacity Provider Service
Each projection service translates shared runtime state into one external contract.
Each projection service may keep protocol-local session or delivery state.
It reads simulation runtime state through ports.
It does not write simulation runtime state directly.
Simulation runtime is exposed through SimulationRuntimePort.
Projection services depend on that stable port rather than runtime internals.
ADR alignment:
0018applies. Persistence stays behind repositories.0023is proposed only. Internal domain events are useful, but optional.0024is proposed only. This design does not depend on a shared event bus.
flowchart LR
UI[Depot Sim Control API]
CFG[Simulation Configuration Service]
RUN[Simulation Run Service]
SCN[Scenario Control Service]
OCPP[OCPP Charge Point Service]
IOT[BetterFleet IoT Packet Service]
TEL[Telematics Service]
OSCP[OSCP Capacity Provider Service]
BF[BetterFleet External Interfaces]
UI --> CFG
UI --> RUN
UI --> SCN
CFG --> RUN
RUN --> OSCP
RUN --> OCPP
RUN --> IOT
RUN --> TEL
SCN --> OSCP
SCN --> OCPP
SCN --> IOT
SCN --> TEL
BF --> OCPP
BF --> IOT
BF --> TEL
BF --> OSCP
OCPP --> BF
IOT --> BF
TEL --> BF
OSCP --> BF
3. Core Domain Model¶
SimulationConfiguration
- saved, versioned depot definition
- owns topology, managed scopes, projection bindings, and scenario presets
ManagedScope
- named electrical boundary inside one configuration
- gives protocol projections a stable routing target
ElectricalTopology
- rooted model of the simulated site electrical structure
- defines the nodes a managed scope can reference
ProjectionBinding
- maps one managed scope to one external protocol role
- stores projection defaults, not live transport state
- covers OCPP, BetterFleet packet, telematics, and OSCP projection families
ProjectionRuntimeState
- runtime delivery or session state for one projection family
- tracks health, checkpoints, and protocol-local state without owning depot truth
OscpCapacityProviderBinding
- OSCP-specific specialization of ProjectionBinding for one managed scope
- stores default group_id, measurement mode, and default scenario preset
SimulationRun
- one executing or resumable run pinned to one configuration version
- owns logical time and mutable depot state
VehicleRuntimeState
- runtime state for one simulated vehicle
- tracks depot presence, SoC, and charging state
ChargerRuntimeState
- runtime state for one charger or connector
- tracks availability, network health, and charging session state
SensorRuntimeState
- runtime state for one simulated sensor or meter
- tracks measured values, heartbeat state, and availability
NodeRuntimeState
- runtime state for one electrical node or managed boundary
- tracks load, constraint, and breach-relevant values
OscpCapacityProviderSession
- runtime OSCP relationship for one simulation run and one BetterFleet configuration
- owns lifecycle state, negotiated behaviour, timers, and correlation state
- uses lifecycle states offline, accepting_handshake, awaiting_handshake_acknowledge, online, and degraded
OscpNegotiatedBehaviour
- effective heartbeat and measurement rules for one session
- records what the session will actually honour
OscpScenarioOverride
- explicit forced behaviour for one session
- drives timeout, reject, ignore, and mismatch cases
OscpForecastPlan
- one scheduled or manual outbound forecast from depot-sim to BetterFleet
- lets tests send controlled forecast blocks deterministically
OscpMessageLedgerEntry
- immutable record of one OSCP message attempt or receipt
- stores direction, headers, body snapshot, outcome, and validation result
- validation result is accepted, rejected, ignored, or indeterminate
OscpMessageExpectation
- expected BetterFleet outbound message for one scenario or runtime condition
- supports positive and negative test assertions
4. Domain Aggregates¶
Simulation Configuration Aggregate¶
Root: SimulationConfiguration
Entities:
ManagedScopeElectricalTopologyProjectionBindingOscpCapacityProviderBinding- scenario preset definitions
Invariants:
- topology is rooted and acyclic
- every managed scope references an existing topology node
- every projection binding targets one managed scope
- projection binding defaults are versioned with the configuration
- active runs never adopt a new configuration version implicitly
Simulation Run Aggregate¶
Root: SimulationRun
Entities:
VehicleRuntimeStateChargerRuntimeStateSensorRuntimeStateNodeRuntimeState- generic
ProjectionRuntimeState
Invariants:
- each run references exactly one configuration version
- logical time moves forward only
- one active runner lease owns one run at a time
- protocol projections read runtime state through ports
- protocol projections cannot rewrite saved configuration
OSCP Capacity Provider Session Aggregate¶
Root: OscpCapacityProviderSession
Entities:
OscpNegotiatedBehaviourOscpScenarioOverrideOscpForecastPlanOscpMessageLedgerEntryOscpMessageExpectation
Invariants:
- one active session exists per
simulation_run_id + remote_configuration_id - session state gates which messages may be accepted
- message ledger entries are append-only
- an outcome update may only finalize an existing ledger entry
- overrides are explicit and time-bounded
- every accepted inbound BetterFleet message resolves to one session
- every outbound depot-sim message carries one session identity
5. Service Boundaries¶
All boundaries below are internal to bf-depot-sim.
Simulation Configuration Service¶
Responsibilities:
- persist versioned simulation configurations
- manage managed scopes and projection bindings
- store projection binding defaults and scenario presets
- import or translate legacy circuit-centric definitions
Owned Data:
SimulationConfigurationManagedScopeProjectionBindingOscpCapacityProviderBinding- scenario preset definitions
External Dependencies:
- configuration repositories
- configuration translation adapters for supported input shapes
Simulation Run Service¶
Responsibilities:
- start, stop, pause, resume, and tick simulation runs
- maintain vehicle, charger, sensor, and electrical runtime state
- expose read ports for projection services
- emit runtime events relevant to projections
Owned Data:
SimulationRunVehicleRuntimeStateChargerRuntimeStateSensorRuntimeStateNodeRuntimeState- runner lease and checkpoint data
External Dependencies:
SimulationRuntimePort- configuration service read port
- runtime repositories
OCPP Charge Point Service¶
Responsibilities:
- project charger runtime state through OCPP charger endpoints
- receive BetterFleet charger control requests through OCPP
- keep OCPP session state outside the core run aggregate
Owned Data:
- OCPP session state
- connector protocol state
External Dependencies:
- simulation runtime read and command ports
- OCPP transport adapters
- scheduler and clock abstractions
BetterFleet IoT Packet Service¶
Responsibilities:
- project site power and sensor runtime state into BetterFleet packet contracts
- publish packet sequences at the required cadence
- retain packet delivery checkpoints and failure state
Owned Data:
- packet projection state
- packet delivery checkpoints
External Dependencies:
- simulation runtime read port
- packet transport adapters
- scheduler and clock abstractions
Telematics Service¶
Responsibilities:
- project vehicle runtime and route state into telematics-facing interfaces
- serve or publish vehicle state snapshots without exposing internal simulator objects
- retain telematics projection state and snapshot metadata
Owned Data:
- telematics projection state
- telematics snapshot metadata
External Dependencies:
- simulation runtime read port
- telematics transport or API adapters
OSCP Capacity Provider Service¶
Responsibilities:
- expose CP protocol routes under
/oscp/cp/2.0 - send outbound handshake, heartbeat, and forecast messages to BetterFleet
- receive and validate BetterFleet outbound OSCP messages
- manage lifecycle state, negotiation state, and timeouts
- persist message ledger and expectations
Owned Data:
OscpCapacityProviderSessionOscpNegotiatedBehaviourOscpForecastPlanOscpMessageLedgerEntry- protocol timers and correlation metadata
External Dependencies:
bf-manage-coreOSCP HTTP endpoints- simulation runtime read port
- scheduler and clock abstractions
- HTTP client and HTTP server adapters
Scenario Control Service¶
Responsibilities:
- apply named scenario presets
- apply manual session overrides
- force reject, ignore, timeout, or mismatch behaviour across projection services
- raise and retire message expectations for tests
Owned Data:
OscpScenarioOverrideOscpMessageExpectation- scenario execution history
External Dependencies:
- OSCP Capacity Provider Service
- OCPP Charge Point Service
- BetterFleet IoT Packet Service
- Telematics Service
- Simulation Run Service read port
6. Event Model¶
Depot-sim uses two event classes:
- cross-cutting simulation events that describe the shared simulation world
- projection-specific events that describe protocol behaviour derived from that world
The cross-cutting events define simulator truth.
Projection events never replace that truth.
SimulationRunStarted
Triggered when:
- an operator or API starts a run
Produced by:
- Simulation Run Service
Consumed by:
- OSCP Capacity Provider Service
- OCPP Charge Point Service
- BetterFleet IoT Packet Service
- Telematics Service
- Scenario Control Service
SimulationRunResumed
Triggered when:
- depot-sim restores a resumable run after restart
Produced by:
- Simulation Run Service
Consumed by:
- OSCP Capacity Provider Service
- OCPP Charge Point Service
- BetterFleet IoT Packet Service
- Telematics Service
ProjectionStateChanged
Triggered when:
- any projection service changes health, connectivity, or delivery checkpoint state
Produced by:
- OCPP Charge Point Service
- BetterFleet IoT Packet Service
- Telematics Service
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
- Scenario Control Service
SensorPacketPublished
Triggered when:
- the BetterFleet IoT Packet Service publishes one packet sequence
Produced by:
- BetterFleet IoT Packet Service
Consumed by:
- query and inspection adapters
TelematicsSnapshotServed
Triggered when:
- the Telematics Service serves or publishes one vehicle state snapshot
Produced by:
- Telematics Service
Consumed by:
- query and inspection adapters
OscpProviderSessionAttached
Triggered when:
- a run is bound to one BetterFleet OSCP configuration
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
OscpHandshakeReceived
Triggered when:
- BetterFleet calls
/oscp/cp/2.0/handshake
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- Scenario Control Service
- query and inspection adapters
OscpHandshakeAcknowledgeSent
Triggered when:
- depot-sim posts a handshake acknowledgement to BetterFleet
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
OscpHeartbeatSent
Triggered when:
- depot-sim posts a heartbeat to BetterFleet
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
OscpHeartbeatReceived
Triggered when:
- BetterFleet posts a heartbeat to depot-sim
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
OscpForecastPublished
Triggered when:
- depot-sim sends
UpdateGroupCapacityForecast
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
OscpOutboundMessageReceived
Triggered when:
- BetterFleet sends a measurement, adjustment, or compliance message
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- Scenario Control Service
- query and inspection adapters
OscpOutboundMessageValidated
Triggered when:
- depot-sim accepts, rejects, or ignores a BetterFleet outbound message
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
ManagedScopeBreachDetected
Triggered when:
- simulation runtime detects a breach against an active forecasted scope
Produced by:
- Simulation Run Service
Consumed by:
- Scenario Control Service
OscpMessageExpectationRaised
Triggered when:
- a scenario or runtime condition expects a BetterFleet outbound message
Produced by:
- Scenario Control Service
Consumed by:
- OSCP Capacity Provider Service
- query and inspection adapters
OscpSessionTimedOut
Triggered when:
- handshake or heartbeat timing rules fail
Produced by:
- OSCP Capacity Provider Service
Consumed by:
- query and inspection adapters
7. API Contracts¶
Depot-sim exposes:
- control and inspection APIs for operators and automated tests
- protocol APIs that emulate the external interfaces BetterFleet uses in production
The concrete contracts below show the control surface and the OSCP protocol surface.
OCPP, BetterFleet packet, and telematics contracts are also part of the reference solution, but their wire-level message shapes are defined by those protocol families rather than restated here.
Control API¶
POST /simulation-runs/{simulation_run_id}/oscp-sessions¶
Creates one OSCP Capacity Provider session for one run.
Request:
{
"managed_scope_id": "scope-grid-1",
"betterfleet_base_url": "http://bf-manage-core:5001/oscp",
"remote_configuration_id": "2b5f4a88-d4b4-41ad-a1c8-889f3ee71111",
"group_id": "group-1",
"measurement_publication_mode": "GROUP",
"measurement_configuration": "INTERMITTENT",
"scenario_preset": "connected_baseline"
}
Response:
POST /simulation-runs/{simulation_run_id}/oscp-sessions/{session_id}/commands/send-handshake¶
Makes depot-sim initiate the handshake.
Request:
Response:
{
"ledger_entry_id": "b2f7b8f2-7181-4f9e-a42d-2f08efea1111",
"state": "awaiting_handshake_acknowledge"
}
POST /simulation-runs/{simulation_run_id}/oscp-sessions/{session_id}/commands/send-forecast¶
Sends one controlled forecast to BetterFleet.
Request:
{
"type": "CONSUMPTION",
"forecasted_blocks": [
{
"capacity": 500.0,
"phase": "ALL",
"unit": "KW",
"start_time": "2026-04-02T10:00:00Z",
"end_time": "2026-04-02T10:15:00Z"
}
]
}
Response:
PATCH /simulation-runs/{simulation_run_id}/oscp-sessions/{session_id}/overrides¶
Applies or clears a deterministic failure override.
Request:
Response:
GET /simulation-runs/{simulation_run_id}/oscp-sessions/{session_id}/messages¶
Returns the message ledger for one session.
Response:
{
"messages": [
{
"direction": "inbound",
"message_type": "UpdateAssetMeasurements",
"outcome": "accepted"
}
]
}
Protocol API¶
All protocol routes use the BetterFleet OSCP routing contract.
Required headers:
X-OSCP-Configuration-IDX-Request-IDX-Correlation-IDwhen the message answers an earlier message
Any protocol route may return 503 Service Unavailable.
That response is reserved for stale or unavailable simulator state.
POST /oscp/cp/2.0/handshake¶
Accepts BetterFleet-initiated handshake.
Successful response:
HTTP response:
204 No Content
POST /oscp/cp/2.0/handshake_acknowledge¶
Accepts BetterFleet acknowledgement of a depot-sim initiated handshake.
HTTP response:
204 No Content
POST /oscp/cp/2.0/heartbeat¶
Accepts BetterFleet heartbeat when the session is online.
Request:
HTTP responses:
204 No Contentwhen accepted403 Forbiddenwhen the session is intentionally offline or mismatched
POST /oscp/cp/2.0/update_group_measurements¶
Receives BetterFleet group measurement publication.
HTTP responses:
204 No Contentwhen accepted403 Forbiddenwhen lifecycle or binding state is invalid422 Unprocessable Entitywhen payload validation fails
POST /oscp/cp/2.0/update_asset_measurements¶
Receives BetterFleet asset measurement publication.
HTTP responses:
204 No Contentwhen accepted403 Forbiddenwhen lifecycle or binding state is invalid422 Unprocessable Entitywhen payload validation fails
POST /oscp/cp/2.0/adjust_group_capacity_forecast¶
Receives BetterFleet manual adjustment request.
HTTP responses:
204 No Contentwhen accepted403 Forbiddenwhen no active compatible forecast exists422 Unprocessable Entitywhen payload validation fails
POST /oscp/cp/2.0/group_capacity_compliance_error¶
Receives BetterFleet compliance error publication.
HTTP responses:
204 No Contentwhen accepted403 Forbiddenwhen correlation or lifecycle state is invalid422 Unprocessable Entitywhen payload validation fails
8. Data Ownership¶
SimulationConfiguration¶
- Written by: Simulation Configuration Service
- Read by: Simulation Run Service, OSCP Capacity Provider Service, query adapters
- Not modified by: Simulation Run Service, OSCP Capacity Provider Service
SimulationRun¶
- Written by: Simulation Run Service
- Read by: OSCP Capacity Provider Service, Scenario Control Service, query adapters
- Not modified by: OSCP Capacity Provider Service
OscpCapacityProviderSession¶
- Written by: OSCP Capacity Provider Service
- Read by: Scenario Control Service, query adapters
- Not modified by: Simulation Run Service
OscpMessageLedgerEntry¶
- Written by: OSCP Capacity Provider Service only
- Read by: query adapters and test clients
- Not modified by: external protocol handlers outside the service boundary
OscpMessageExpectation¶
- Written by: Scenario Control Service
- Read by: OSCP Capacity Provider Service and query adapters
- Not modified by: BetterFleet protocol calls
OscpScenarioOverride¶
- Written by: Scenario Control Service
- Read by: OSCP Capacity Provider Service
- Not modified by: BetterFleet protocol calls
Read models are derived only.
They are not authoritative write targets.
9. Operational Workflows¶
BetterFleet-Initiated Handshake¶
- Operator creates an OSCP session for a simulation run.
- BetterFleet calls
/oscp/cp/2.0/handshake. - The OSCP service resolves the session by
X-OSCP-Configuration-ID. - Scenario control decides allow, reject, ignore, or delay.
- The OSCP service records the inbound handshake in the ledger.
- The OSCP service returns
204or403. - When allowed, depot-sim sends
/oscp/fp/2.0/handshake_acknowledge. - If BetterFleet accepts, the session moves to
online. - The heartbeat schedule starts.
Capacity Provider-Initiated Handshake¶
- Operator calls
send-handshake. - The OSCP service posts
/oscp/fp/2.0/handshake. - The service records the outbound handshake in the ledger.
- The session moves to
awaiting_handshake_acknowledge. - BetterFleet calls
/oscp/cp/2.0/handshake_acknowledge. - The OSCP service validates the session state.
- The session moves to
online. - The heartbeat schedule starts.
Heartbeat and Offline Control¶
- When online, depot-sim sends heartbeats on the negotiated interval.
- BetterFleet may also send heartbeats to depot-sim.
- Each heartbeat updates session liveness state.
- An override may stop sending heartbeats.
- An override may reject incoming heartbeats with
403. - The ledger stores each accepted, rejected, or ignored heartbeat.
- Expired liveness moves the session to
offline.
Forecast Publication¶
- Operator or scenario submits an
OscpForecastPlan. - The OSCP service validates session state and
group_id. - The service posts
/oscp/fp/2.0/update_group_capacity_forecast. - The ledger records request headers, payload, and outcome.
- Scenario control may raise future message expectations.
- Query APIs expose the last forecast result.
BetterFleet Outbound Message Validation¶
- BetterFleet posts a measurement, adjustment, or compliance message.
- The OSCP service resolves the session and active binding.
- The service validates headers, lifecycle, and correlation.
- The service validates message-specific rules.
- The service records the message in the ledger.
- The service returns
204,403, or422. - Query APIs expose whether the message was accepted or rejected.
Failure Scenario Injection¶
- Operator applies a scenario preset or manual override.
- Scenario control marks the active session behaviour.
- The next matching OSCP action is delayed, ignored, or rejected.
- The ledger marks the deliberate outcome.
- The override expires or is cleared explicitly.
sequenceDiagram
participant BF as bf-manage-core
participant CP as Depot Sim OSCP CP Service
participant SCN as Scenario Control
BF->>CP: POST /oscp/cp/2.0/handshake
CP->>SCN: Check override
SCN-->>CP: allow / reject / ignore
CP-->>BF: 204 or 403
CP->>BF: POST /oscp/fp/2.0/handshake_acknowledge
BF-->>CP: 204
CP->>BF: POST /oscp/fp/2.0/heartbeat
10. Failure Handling¶
| Failure Type | Detection | Simulator Behaviour |
|---|---|---|
| Service restart | Depot-sim process stops or redeploys | Reload SimulationRun and OscpCapacityProviderSession state from repositories. Resume only sessions whose timers are still valid. |
| Duplicate message delivery | Same request identity appears again | Return the prior outcome when the request is idempotent. Do not append a second final ledger result. |
| BetterFleet unreachable | HTTP timeout or transport failure on outbound handshake, heartbeat, or forecast | Mark the ledger entry as failed. Move the session to degraded or offline by policy. Keep prior session history. |
| BetterFleet stops sending heartbeats | Heartbeat expiry elapses | Mark the session offline. Close any expectation that requires an online session. |
| Depot-sim stops sending heartbeats | Scenario override or scheduler failure | Keep the ledger history. Let BetterFleet detect offline state naturally. |
| Intentional ignore case | Scenario override says ignore | Record intent in the ledger. Return no response until timeout behavior is satisfied. |
| Intentional mismatch case | Scenario override forces wrong lifecycle or binding state | Return deterministic 403. Record the reason in the ledger. |
| Payload shape failure | JSON body misses required fields or invalid enum values | Return 422. Store validation errors with the ledger entry. |
| Stale validation state | Runtime snapshot is older than the allowed validation window | Return 503 or mark the session indeterminate by policy. Do not silently accept stale semantics. |
| Limit or scope violation | Message does not match active group_id, asset, scope, or forecast correlation |
Return 403 for state violations. Return 422 for payload semantics. Record the exact rule failure. |
Failure handling rules:
- projection failure does not delete simulation state
- OSCP session failure does not mutate saved configuration
- every failure must be visible through the message ledger
- scenario-driven failures must be distinguishable from accidental failures
- session recovery must be explicit after offline or timeout unless the scenario says otherwise
11. Future Extensions¶
- add
register,reregister, andunregisterwhen the BetterFleet registration flow is part of the active integration - support multiple active OSCP sessions for different managed scopes in one run
- add segmented message support if the BetterFleet interface needs it
- add stronger numeric validation of measurement payloads against runtime power windows
- add UI editing for OSCP scenario presets and active overrides
- move timer ownership to a durable scheduler when multi-instance execution is required
- support additional partner profiles without changing the core session aggregate
- support future
Capacity Optimizerflows without moving OSCP protocol state intoSimulationRun