Context
A code-verified gap in the OCPP 2.0.1 CP simulator's TriggerMessage support. The v201 handler classifies six requested-message types as NotImplemented (crates/ocpp-cp/src/v201_command.rs, v201_trigger_message_status):
LogStatusNotification | FirmwareStatusNotification | SignChargingStationCertificate
| SignV2GCertificate | SignCombinedCertificate | PublishFirmwareStatusNotification
=> TriggerMessageStatusEnumType::NotImplemented
But the simulator does model firmware updates: UpdateFirmware drives an async FirmwareStatusNotification(Downloading → Downloaded → Installing → Installed) progress stream (run_v201_firmware_update, Issue #532/#534). So a CSMS asking TriggerMessage(requestedMessage = FirmwareStatusNotification) should get the station's current firmware status — exactly as the 1.6J side already does: the 1.6J CP holds a firmware_status: Arc<RwLock<FirmwareStatus>> and TriggerMessage(FirmwareStatusNotification) re-reports it "without re-running the update" (crates/ocpp-cp/src/lib.rs).
The v201 side can't do this yet because nothing persists the latest status: V201FirmwareUpdateStore (crates/ocpp-cp/src/v201_firmware_update.rs) tracks only the in-flight requestId, not a FirmwareStatusEnumType. This issue closes that gap for the FirmwareStatusNotification trigger (the direct v201 twin of the existing 1.6J behavior). The sibling triggers are tracked separately (LogStatusNotification, PublishFirmwareStatusNotification, and the certificate-signing triggers).
Scope (small slice)
- Persist the latest reported v201 firmware status + correlating
requestId (mirroring the 1.6J firmware_status field), colocated in V201FirmwareUpdateStore. Default { status: Idle, requestId: None }. Recorded at the single emit choke point (send_v201_firmware_status), so the full lifecycle (interim + terminal, happy path and injected failures) is captured.
- Reclassify
FirmwareStatusNotification from NotImplemented to Accepted in v201_trigger_message_status.
- Add the dispatch arm in
send_v201_triggered_message: read the latest status and emit one FirmwareStatusNotification(status, requestId) — a pure snapshot re-report, no new update kicked off, in-flight store untouched.
requestId is Option<i32> on the wire (skip_serializing_if): omitted for a never-ran Idle report, present for any status that came from an UpdateFirmware.
Failure modes / trust boundary
- Never ran an update → report
Idle, requestId omitted (Idle is a valid FirmwareStatusEnumType; the spec omits requestId for a status not tied to a specific request).
- Update completed (
Installed) → in-flight store is cleared, but the last-reported status is retained → re-report Installed + its requestId.
- Injected failure (
DownloadFailed / InstallationFailed) → re-report that terminal status.
- Mid-flight trigger → reports the most recent interim status (a snapshot), never restarts the rollout.
- The re-report burns no
requestId, allocates nothing, and never re-enters the receive loop (runs on the command-consumer task, like the other triggers).
- A 1.6J charge point is unaffected (separate classifier +
firmware_status field).
Test matrix
- Store: a new store's last-reported is
{ Idle, None }; recording a status updates it.
- Pure builder: an
Idle report omits requestId and is schema-valid; a Some(id) report carries it and is schema-valid.
- Policy:
v201_trigger_message_status(FirmwareStatusNotification) == Accepted.
- Handler (mock CSMS):
TriggerMessage(FirmwareStatusNotification) with no prior update → response Accepted, then one FirmwareStatusNotification(Idle) with no requestId.
- Handler: after an
UpdateFirmware runs to Installed, the trigger re-reports Installed + that requestId.
Acceptance criteria
Reference
mobilityhouse/ocpp is protocol-only, so the behavior ported is the Charging Station side; the wire tokens are pinned by the already-ported types:
ocpp/v201/enums.py — FirmwareStatusEnumType, MessageTriggerEnumType.firmware_status_notification.
ocpp/v201/call.py — class FirmwareStatusNotification, class TriggerMessage.
- Existing Rust:
crates/ocpp-cp/src/v201_firmware_update.rs, crates/ocpp-cp/src/v201_command.rs (v201_trigger_message_status, v201_firmware_status_notification), crates/ocpp-cp/src/lib.rs (send_v201_triggered_message, send_v201_firmware_status, run_v201_firmware_update). 1.6J precedent: the firmware_status field + TriggerMessage(FirmwareStatusNotification) arm in lib.rs.
Milestone / labels
M7 — OCPP 2.0.1. Labels: port, m7. Small (one store field + methods, one policy line, one dispatch arm, one builder variant, tests — well under the ~500-LOC target).
🌙 Filed by the nightly dev during Sunday grooming (2026-09-27): the M7 earliest-incomplete queue had dropped below 3 unblocked, well-scoped port issues (#579 is in-flight as PR #581; #582 is blocked on it). This is a code-verified gap — the v201 TriggerMessage handler reports FirmwareStatusNotification as NotImplemented despite the simulator modeling firmware updates — and the direct twin of behavior the 1.6J CP already has. Broader M7→M8 direction remains tracked in #256.
Generated by Claude Code
Context
A code-verified gap in the OCPP 2.0.1 CP simulator's
TriggerMessagesupport. The v201 handler classifies six requested-message types asNotImplemented(crates/ocpp-cp/src/v201_command.rs,v201_trigger_message_status):But the simulator does model firmware updates:
UpdateFirmwaredrives an asyncFirmwareStatusNotification(Downloading → Downloaded → Installing → Installed)progress stream (run_v201_firmware_update, Issue #532/#534). So a CSMS askingTriggerMessage(requestedMessage = FirmwareStatusNotification)should get the station's current firmware status — exactly as the 1.6J side already does: the 1.6J CP holds afirmware_status: Arc<RwLock<FirmwareStatus>>andTriggerMessage(FirmwareStatusNotification)re-reports it "without re-running the update" (crates/ocpp-cp/src/lib.rs).The v201 side can't do this yet because nothing persists the latest status:
V201FirmwareUpdateStore(crates/ocpp-cp/src/v201_firmware_update.rs) tracks only the in-flightrequestId, not aFirmwareStatusEnumType. This issue closes that gap for the FirmwareStatusNotification trigger (the direct v201 twin of the existing 1.6J behavior). The sibling triggers are tracked separately (LogStatusNotification, PublishFirmwareStatusNotification, and the certificate-signing triggers).Scope (small slice)
requestId(mirroring the 1.6Jfirmware_statusfield), colocated inV201FirmwareUpdateStore. Default{ status: Idle, requestId: None }. Recorded at the single emit choke point (send_v201_firmware_status), so the full lifecycle (interim + terminal, happy path and injected failures) is captured.FirmwareStatusNotificationfromNotImplementedtoAcceptedinv201_trigger_message_status.send_v201_triggered_message: read the latest status and emit oneFirmwareStatusNotification(status, requestId)— a pure snapshot re-report, no new update kicked off, in-flight store untouched.requestIdisOption<i32>on the wire (skip_serializing_if): omitted for a never-ranIdlereport, present for any status that came from anUpdateFirmware.Failure modes / trust boundary
Idle,requestIdomitted (Idleis a validFirmwareStatusEnumType; the spec omitsrequestIdfor a status not tied to a specific request).Installed) → in-flight store is cleared, but the last-reported status is retained → re-reportInstalled+ itsrequestId.DownloadFailed/InstallationFailed) → re-report that terminal status.requestId, allocates nothing, and never re-enters the receive loop (runs on the command-consumer task, like the other triggers).firmware_statusfield).Test matrix
{ Idle, None }; recording a status updates it.Idlereport omitsrequestIdand is schema-valid; aSome(id)report carries it and is schema-valid.v201_trigger_message_status(FirmwareStatusNotification) == Accepted.TriggerMessage(FirmwareStatusNotification)with no prior update → responseAccepted, then oneFirmwareStatusNotification(Idle)with norequestId.UpdateFirmwareruns toInstalled, the trigger re-reportsInstalled+ thatrequestId.Acceptance criteria
TriggerMessage(FirmwareStatusNotification)is classifiedAcceptedand originates exactly one schema-validFirmwareStatusNotificationcarrying the station's latest firmware status.requestId) is tracked across the full update lifecycle; a never-ran station reportsIdlewithrequestIdomitted.UpdateFirmware → FirmwareStatusNotificationprogress stream or the 1.6J trigger behavior.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Reference
mobilityhouse/ocpp is protocol-only, so the behavior ported is the Charging Station side; the wire tokens are pinned by the already-ported types:
ocpp/v201/enums.py—FirmwareStatusEnumType,MessageTriggerEnumType.firmware_status_notification.ocpp/v201/call.py—class FirmwareStatusNotification,class TriggerMessage.crates/ocpp-cp/src/v201_firmware_update.rs,crates/ocpp-cp/src/v201_command.rs(v201_trigger_message_status,v201_firmware_status_notification),crates/ocpp-cp/src/lib.rs(send_v201_triggered_message,send_v201_firmware_status,run_v201_firmware_update). 1.6J precedent: thefirmware_statusfield +TriggerMessage(FirmwareStatusNotification)arm inlib.rs.Milestone / labels
M7 — OCPP 2.0.1. Labels:
port,m7. Small (one store field + methods, one policy line, one dispatch arm, one builder variant, tests — well under the ~500-LOC target).🌙 Filed by the nightly dev during Sunday grooming (2026-09-27): the M7 earliest-incomplete queue had dropped below 3 unblocked, well-scoped
portissues (#579 is in-flight as PR #581; #582 is blocked on it). This is a code-verified gap — the v201TriggerMessagehandler reportsFirmwareStatusNotificationasNotImplementeddespite the simulator modeling firmware updates — and the direct twin of behavior the 1.6J CP already has. Broader M7→M8 direction remains tracked in #256.Generated by Claude Code