Repository navigation
port(m7): CP simulator 2.0.1 — chunk NotifyReport / NotifyMonitoringReport across multiple pages (tbc paging) #574
Description
Context
The OCPP 2.0.1 device-model report streams are wired end-to-end, but they emit a single page only. Both emitters hard-code tbc: None / seq_no: 0 regardless of how large the reported inventory is:
ChargePoint::send_v201_notify_report(GetBaseReport→NotifyReport) —crates/ocpp-cp/src/lib.rs(~L7924):"A single page carries the whole report (
seqNo0,tbcomitted =false) — correct for the simulator's small seeded device model. Multi-page chunking (tbcpaging) is a later slice once the inventory grows beyond one frame."ChargePoint::send_v201_notify_monitoring_report(GetMonitoringReport→NotifyMonitoringReport) — same file (~L7963), identical single-page stance.
This issue closes that explicitly-deferred "later slice": chunk both report streams into multiple correlated pages with toBeContinued (tbc) set on every page but the last, so a large FullInventory GetBaseReport (or a large monitor set) reports faithfully instead of overflowing a single frame.
Why it matters
GetBaseReport(FullInventory) on a real station returns the entire device model — dozens to hundreds of ReportDataType entries — which routinely exceeds one practical WebSocket message. OCPP 2.0.1 defines the paging contract precisely for this: each NotifyReport / NotifyMonitoringReport carries a monotonic seqNo and tbc: true until the final page (tbc omitted/false), all correlated by requestId. Emitting one page silently caps the report — a conformance and scaling gap.
The pattern already exists
NotifyCustomerInformation already does this correctly and is the template to mirror — a pure pager plus a command-consumer driver:
crates/ocpp-cp/src/v201_command.rs—v201_notify_customer_information_pages(request_id, generated_at, data_pages)splits into pages, setstbc: (i < last).then_some(true), and assigns monotonicseq_no.- The report is driven off the command-consumer task via
RemoteCommand::V201CustomerInformationReport(off the inbound-CALL path — no receive-loop re-entrancy), exactly asNotifyReport/NotifyMonitoringReportalready are.
So this is a faithful port of an in-repo idiom onto two more emitters, not new machinery.
Scope (small slice)
- Add pure page-splitters for the two report bodies (mirroring
v201_notify_customer_information_pages): given the fullVec<ReportDataType>/Vec<MonitoringDataType>and a bounded page size (entries-per-page constant), produceVec<…Request>with monotonicseqNofrom 0 andtbc: Some(true)on all but the last page,tbc: Noneon the last. - Rewire
send_v201_notify_report/send_v201_notify_monitoring_reportto emit each page in order viaself.call(..), preserving the existing correlation (requestId) and the command-consumer ordering guarantee (pages sent only after the triggering CALLRESULT is flushed). - Empty report → still exactly one final page (
seqNo0,tbcomitted), matching today's behavior and the customer-infoempty_inputcase; no zero-page emission. - Page size is a named constant, not configurable in this slice (keep the diff small); a real byte-budget/frame-size negotiation is out of scope (see notes).
Failure modes / trust boundary
seqNoisi32on the wire; reuse the customer-info pager'si32::try_from(i).unwrap_or(i32::MAX)guard so a pathologically long report can never panic on the cast.- Report contents are the station's own computed snapshot (not attacker-supplied), but oversized individual entries still surface as an outbound-schema
Errfromcall()— never a panic or silent truncation, same guarantee as the single-page path today. - Ordering: all pages of one report are emitted in
seqNoorder on the command-consumer task; a dropped consumer is logged (as today) and burns no partial stream.
Test matrix
- Single small report → one page,
tbcomitted,seqNo0 (unchanged behavior). - Report larger than one page → N pages;
tbc == Some(true)on pages0..N-1,tbcomitted on the last;seqNoruns0..Nmonotonically; every page carries the samerequestIdand ageneratedAt. - Empty report → exactly one final page, no
tbc. - Every built page is schema-valid across shapes (mirror the
built_notify_customer_information_pages_are_schema_validtest). - Integration (capturing mock CSMS): an
AcceptedGetBaseReport(FullInventory)over a seeded model that exceeds one page yields the correct ordered multi-pageNotifyReportstream; symmetricGetMonitoringReport→NotifyMonitoringReport. seqNocast guard: a synthetic over-i32::MAXpage index does not panic (unit test on the pure splitter).cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.
Acceptance criteria
-
send_v201_notify_reportandsend_v201_notify_monitoring_reportemit multi-page streams:tbc: trueon every page but the last, monotonicseqNofrom 0, correlated byrequestId, in order, off the command-consumer task. - Pure page-splitters with schema-validity +
tbc/seqNounit tests, mirroringv201_notify_customer_information_pages. - Small and empty reports keep today's exact single-page behavior; no zero-page emission.
-
seqNocast cannot panic; oversized entries surface as an outbound-schemaErr, never a panic/truncation. -
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.
Python reference
mobilityhouse/ocpp is protocol-only (message dataclasses + @on routing); the paging behavior is the Charging Station's, not a Python module. The wire types being paged are already ported:
ocpp/v201/call.py—class NotifyReport,class NotifyMonitoringReport(tbc,seq_no,request_id,generated_at).ocpp/v201/datatypes.py—ReportDataType,MonitoringDataType.
Known gaps / notes
- Real frame-size/byte-budget negotiation (choosing page size from an actual transport MTU rather than a fixed entry count) is out of scope — a fixed entries-per-page constant is sufficient for the simulator; note the remainder here if a byte-accurate budget is ever wanted.
NotifyEvent(a thirdtbc-capable report stream, single-page today per port(m7): CP simulator 2.0.1 — emit NotifyEvent when a CSMS-installed variable monitor trips #545/feat(ocpp-cp): emit v201 NotifyEvent when a variable monitor trips (M7) #570's known gaps) is intentionally not in scope — it trips on a different trigger and can be a separate follow-up if a per-trip event set ever outgrows a frame.- No new dependencies. Direction for the broader M7→M8 track remains tracked in Direction: v201 CALL/CALLRESULT message coverage is complete — what's the next M7→M8 track? #256.
🌙 Filed by the nightly dev during issue grooming (2026-09-10). Both open nightly PRs (#570, #572) are green and awaiting review, so tonight was a grooming night at the 2-open-PR cap; this is a genuine, code-verified follow-up (the tbc-paging "later slice" the two report emitters explicitly defer, with an in-repo pattern to mirror) — not queue padding — replenishing the M7 backlog for once #570/#572 merge.
Activity
duyhuynh-vn commentedon Sep 11, 2026
🌙 Nightly dev picking this up — 2026-09-11
Generated by Claude Code
duyhuynh-vn commentedon Sep 11, 2026
Opened PR #575 with
tbcpaging for both device-model report streams (NotifyReport/NotifyMonitoringReport): pure page-splitters + rewired command-consumer emitters, mirroring theNotifyCustomerInformationpager idiom. All acceptance criteria addressed;cargo fmt --check,clippy -D warnings, andcargo test --workspacegreen locally. Awaiting your review — I won't self-merge.Generated by Claude Code