Repository navigation
Releases
Releases · archhaze24/kirby-ocpp
v0.5.0
The polish release: documentation as a first-class deliverable, a stronger scenario engine, a local HTTP control API for kirby-csms, prepared distribution channels and a long list of hardening and usability fixes.
0.5.0 in numbers
| Metric | Since 0.4.0 |
|---|---|
| 🧾 Commits | 91 in three days (5 to 7 November 2026) |
| ✏️ Lines of code | +21.6k / −1.6k across 144 files; Rust grew from 70.6k to 86.9k lines |
| 🧪 Tests | +283 new test functions: 1,047 → 1,330 (all run in the nextest gate) |
| 📦 Crates | 7 → 7 (new dependencies: axum, hyper, hyper-util, clap_complete) |
| 🔎 Reviews | 11 review rounds, including product reviews that played the real binaries and both terminal UIs |
| ✨ New features | 39 |
| 🔧 Changes | 25 |
| 🐛 Fixes and hardening | 35 |
✨ Added
Scenarios and scripts
- Report vocabulary is consistent across the run, suite and fleet reports. Fleet reports and their station entries carry
scenario,source,started_atandduration_ms; suite entries addscenario(next toname) andstarted_at. ocpp_versioncomes from each run's own report, and fleetcounts.skippedis computed.failed_namesanderror_nameslist the two kinds of failure whilefailedkeeps both.- Emulator scenarios have variables: an optional
[vars]table (strings, numbers, booleans) and${name}substitution in the string fields and JSON payloads of a step. An exact"${name}"keeps the JSON type,$${is a literal${, and${station_id}and${index}are built in. capture = { name = "/json/pointer" }onsend_raw,data_transferandexpect_callsteps stores a value of the answer for later steps; a missing pointer fails the step.kirby-ocpp run --var NAME=VALUE(repeatable) overrides scenario variables, also in fleet runs. A name the scenario never uses is a usage error inrun(exit 2) and a warning insuite.- Emulator scenarios have loops:
action = "repeat"withcount(1..=1000) and nested[[steps.steps]], up to 3 levels, counters${i}/${i0}(or a customvar) and at most 10000 expanded steps (checked at load time). Reports and JUnit list every executed step asname [n](name [2.3]when nested). - Five new built-in scenarios:
heartbeat-and-status,metering-burst,data-transfer,security-eventandreconnect(seekirby-ocpp scenarios list). kirby-ocpp suite [SOURCE...]runs many scenarios (files, directories of*.toml, built-in names,--all-builtins) one after another, each on a fresh station. It prints a PASS/FAIL line per scenario and a summary, and supports--report(suite JSON), one--junitfile with a test suite per scenario,--fail-fast,--varand a per-scenario--timeout.kirby-csmsscripts have variables too: a top-level[vars]table,${name}substitution instation,expect_statusand the JSON payloads of steps,captureoncall(CALLRESULT payload) andwait_call(matched CALL payload), andkirby-csms --script FILE --var NAME=VALUE.callsteps inkirby-csmsscripts takeexpect_payload = { ... }, a JSON subset the CALLRESULT payload must contain (used withexpect_status, not withexpect_error).kirby_ocpp_core::scenario::varsis a public helper (Vars,substitute,substitute_str,capture) for other tools.- Scenario and script files accept the optional top-level keys
ocpp_version(only"1.6"),schema(only1) andtags(a list of strings, kept but not used for filtering). Other values are load errors.evseandid_tokenare reserved step keys. - Every JSON report carries
schema,ocpp_version,tool_version,station(not for CSMS scripts),source,started_atand astatusstring (passed,failed,error,skipped); suite and fleet reports addcounts. Failed steps carryaction,params(secrets redacted) and, for failed assertions,expectedandactual. Existing fields are unchanged. - Load errors name the file and, where the parser knows it,
path:line:column, and suggest the closest name for an unknown action or field.
Control API
kirby-csms --api-listen ADDRstarts a local HTTP control API (JSON, plain HTTP, off by default) withGET /v1/health,GET /v1/stations,GET /v1/stations/{id},GET /v1/stations/{id}/history?limit=N,POST /v1/stations/{id}/calls(same result object as the stdin commands, shared 256-command limit, 429 when full) andDELETE /v1/stations/{id}(forget a disconnected station).GET /v1/events?station=IDstreams Server-Sent Events: one--jsonevent object per message, redacted like--json, keep-alive every 15 s, anoteevent when the client lags, at most 16 streams (503 beyond).GET /v1/rules,PUT/DELETE /v1/rules/{action}(fixed CALLRESULT, like therespondscript step) andPOST/GET/DELETE /v1/faults(likeinject_fault). Fault action names (1 to 128 bytes, no control characters),timesand the stored-fault caps are checked identically for the API andinject_faultscript steps.--api-token/--api-token-file(envKIRBY_CSMS_API_TOKEN,KIRBY_CSMS_API_TOKEN_FILE) require a bearer token. A non-loopback address needs one. Without a token, requests with anOriginheader,Sec-Fetch-Site: cross-siteor a non-loopbackHost(DNS rebinding) are refused. Seedocs/api.md.- The API caps connections (512) and the request data held by calls in flight (32 MiB). It times out slow request heads and bodies (10 s) and limits the writing of a response (60 s total, 30 s without progress; event streams end after 1 hour with a note).
- The API enables TCP keepalive, closes every connection after one response, bounds injected faults (1000 faults,
timesup to 1000000, 8 MiB) and ends event streams after the shutdown events. - New direct dependencies:
axum(HTTP/1 only),hyperandhyper-util. - The API shapes are a versioned contract (
api_version1, inGET /v1andGET /v1/health; breaking changes will ship as/v2).GET /v1lists every route,OPTIONSon a known path answers204withAllow(no CORS headers by design), and every SSE message has anid:line (Last-Event-IDis ignored). Station objects andconnectedevents carryprotocol(the negotiated subprotocol,nullif none) andconnectedevents carryremote_addr; the same fields appear inkirby-csms --jsonand{"cmd":"stations"}output, and the text line readsCP-1 connected from 10.0.0.5:4242 (ocpp1.6, profile 2). - API errors have the body
{"error":{"code":"...","message":"..."}}with stable snake_case codes (table indocs/api.md). A call to a station that was never seen is404unknown_station; to a known one that is disconnected,409station_not_connected. POST /v1/stations/{id}/callsanswers{"outcome":"result"|"call_error"|"timeout","ok":...,...}withouttype,stationandaction; CALLERROR and timeout stay HTTP200. The stdin answers are unchanged.- Transaction ids (
active_transactions[].id,state.transactions[].id) are strings in the API (OCPP 2.x ids are strings); the stdin answers and--jsonlines keep numbers.
Whitepaper and offline behavior
- Transaction-related
MeterValuessamples taken while the station is offline (or while the transaction still has a local id) are queued instead of dropped: bounded (1000, oldest first), persisted, and sent with the realtransactionIdafter the start was synchronized and before the stop. State file format 8 (offline_samples; older files load). - Security profile fallback (
A05.FR.08): after a change ofSecurityProfilethe station goes back to the previous profile (andAuthorizationKey) after 3 connection failures the profile can explain, queues aReconfigurationOfSecurityParametersevent and reconnects. Only HTTP 401/403 and client-certificate TLS alerts count, never server-certificate errors, closes, timeouts, refused TCP or DNS, so a man in the middle cannot force a downgrade. New flag--security-profile-fallback <BOOL>(OCPP_SECURITY_PROFILE_FALLBACK, defaulttrue). State file format 9 (pending_profile_switch, previous key in the secrets file; older files load). - Client certificate alternation (
A02.FR.28) no longer depends on a TLS alert: an HTTP 401/403 upgrade answer or a close/reset right after the TLS handshake switches between the new and the previous certificate at once, and any other failure does so after 3 in a row. A bad renewed certificate cannot strand the station.
Distribution
- New release target
aarch64-unknown-linux-musl(static, built onubuntu-24.04-arm) and the npm platform package@archhaze24/kirby-ocpp-linux-arm64-musl. The npm launcher prefers it on musl arm64 systems and falls back to it when the glibc package is missing. A release has 14 files (7 archives + 7.sha256). - Multi-arch container image
ghcr.io/archhaze24/kirby-ocpp(linux/amd64,linux/arm64) built by the release workflow from the static musl binaries: non-root user, state volume/data, port 9000, both binaries (kirby-csmsas the first argument starts the CSMS).Dockerfile.devbuilds it from a source checkout. - Homebrew formula and Scoop manifest templates under
packaging/plus.github/scripts/render-packaging.sh; the release workflow renders them with the real checksums into the workflow artifactpackaging.
Emulator CLI
kirby-ocpp versionprints the tool version, git commit, build date, target triple and supported protocols (--jsongives one JSON object);--versionstays one line. Abuild.rsembeds the commit and the date (SOURCE_DATE_EPOCHis honored).kirby-ocpp completions <bash|zsh|fish|powershell|elvish>prints a shell completion script (new dependencyclap_complete).version --help,completions --helpandscenarios --helpno longer repeat the station flags (they still parse there);run --helpandsuite --helppoint tokirby-ocpp --helpfor them...
v0.4.0
kirby-csms joins kirby-ocpp: a full central system for your terminal, the OCPP 1.6 Security
Whitepaper on both sides, and a much harder, faster, better tested code base.
0.4.0 in numbers
| Metric | Since 0.3.0 |
|---|---|
| 🧾 Commits | 39 in four days (2 to 5 November 2026) |
| ✏️ Lines of code | +29,821 / −851 across 197 files; Rust grew from 43.0k to 70.6k lines |
| 🧪 Tests | +324 new test functions: 723 → 1,047 (1,049 tests in the full gate) |
| 📦 Crates | 4 → 7 (kirby-csms, kirby-csms-core, kirby-csms-tui are new) |
| 🔎 Reviews | 2 deep adversarial reviews and 4 focused ones: over 60 verified findings fixed |
| ✨ New features | 24 |
| 🔧 Changes | 9 |
| 🐛 Fixes and hardening | 30 |
✨ Added
kirby-csms: a standalone OCPP 1.6J central system
New binary kirby-csms (default listen 0.0.0.0:9000) and the new crates kirby-csms-core
(the engine, usable as a library) and kirby-csms-tui.
- Multi-station engine: stations connect to
ws(s)://host:port/<any prefix>/<station id>, with a
bounded per-station frame history (entry and byte caps) and an event stream. - Per-station state read model: boot information, connectors, transactions with meter readings,
security events, log and firmware status, installed certificates. - Typed, schema-checked commands for every central-system action, the Security Whitepaper ones
included, plus a rawcallfor anything else. - Scripted replies: response rules, fault injection and custom responders, to test how firmware
copes with a misbehaving central system. - Validation of inbound CALLs against the bundled OCPP schemas, answered with the right
CALLERROR code (--validate-inbound, on by default). - Server-side WebSocket ping/pong watchdog (
--ping-interval,--pong-timeout). - Connection security profiles 1 to 3: Basic Auth (
--auth-key,--auth-keys-file), TLS and
mutual TLS with the certificate CN equal to the station id. - Headless mode:
--json,--log-file, JSON-lines commands on stdin, TOML--scriptwith
--reportand--junit, documented exit codes. - Terminal UI: station list and details, merged or per-station log, command forms for every
action, response rules and security forms. - Shipped next to
kirby-ocppin every release archive and as the npm package
kirby-csms.
OCPP 1.6 Security Whitepaper (edition 4) on the emulator
- 11 new messages with the 22 official schemas:
CertificateSigned,DeleteCertificate,
ExtendedTriggerMessage,GetInstalledCertificateIds,GetLog,InstallCertificate,
LogStatusNotification,SecurityEventNotification,SignCertificate,
SignedFirmwareStatusNotification,SignedUpdateFirmware. - Certificate store with OCSP-style certificate hashes and
CertificateStoreMaxLength. - Security profiles 0 to 3 on the station: Basic Auth with
AuthorizationKey, TLS, mTLS, upgrade
rules, optional downgrade (AllowSecurityProfileDowngrade) and a reconnect after every change
of security parameters. - Client certificates:
SignCertificate/CertificateSignedwith chain checks, repeat with
back-off (CertSigningWaitMinimum,CertSigningRepeatTimes) and alternation between the new
and the previous certificate. - Security events: queued, persisted and reported, including TLS version, cipher suite and
certificate failures, reported once per series of failed attempts. AdditionalRootCertificateCheck: central system root rotation signed by the old root, with a
fallback until the first successful connection (--additional-root-certificate-check).AllowCentralSystemTLSWildcards: wildcard server certificates are rejected unless allowed.GetLogandSignedUpdateFirmwaresimulated with their status notifications.- Emulator TUI actions and the scenario steps
request_certificateandsecurity_event.
Reports and secrets
- JUnit XML for scenario runs:
kirby-ocpp run --junit, single station and fleet. - The
AuthorizationKeyis redacted in recorded frames (logs, TUI,--json, history);
--show-secretsturns it off.
Release engineering
- Release notes are taken from this file by the release workflow; a release without notes fails
early, also in dry runs. - Tag and Cargo version must match; prerelease tags (
v1.2.3-rc.1) publish as GitHub prereleases. - Every crate ships a
LICENSE; the library crates have their own README and crates.io metadata.
🔧 Changed
- State file format 3 → 7 (older files still load).
StopTransactionOnInvalidIdandMaxEnergyOnInvalidIdnow follow the spec: they apply to the
StartTransactionanswer, not toAuthorize.StopTransactionOnInvalidIddefaults totrue.- The central system answers an unknown action with
NotImplementedinstead ofNotSupported. kirby-ocpp --jsonimplies headless mode, and--log-filealso works with the TUI.- Bounded memory everywhere it was unbounded: TUI logs (byte budget, large frames truncated),
CSMS history, script backlog, scenario frames, stdout and log-file queues. GetCompositeScheduleis bounded to 7 days and 1,000 periods.- OCPP schema attribution now points to the official Open Charge Alliance downloads
(CC BY-ND 4.0); the bundled files are byte-identical to them. - Faster builds and tests: line-table debuginfo in the dev profile and one integration-test
binary per crate (cold test build 3:07 → 1:23, warm test run 64 s → 33 s on the maintainer's
VM;cargo nextestruns the suite in about 15 s). - Windows CI test made independent of the OS send buffer size.
🐛 Fixed
Station
- Energy was credited with a stale "charging" flag after status changes.
- A
Stoppingtransaction restored from disk was reported with the date 1970 or lost when two
connectors shared one transaction id. - Failed queued
StartTransaction/StopTransactionmessages were never retried; the offline
queue is now retried and capped at 1,000 entries. GetDiagnosticsandUpdateFirmwareshared one status queue, so firmware updates never
reportedInstalled.- Clock-aligned
MeterValuesstarved connectors on stations with more than 16 connectors. - A pending
ChangeAvailabilitycould stay stuck after a start that never happened. RemoteStartTransactionignored a parent id tag that only the central system'sAuthorize
answer carried.- Periodic samples stopped while a transaction was suspended; intervals above 24 h sampled every
24 h; an online sync forced a resend of all status notifications. - Stale firmware and diagnostics status after a disconnect.
- The
WebSocketPingIntervalkey was stored but unused. - A write that never completed could hang the connection: writes now time out.
- Messages from the central system are limited to 4 MiB; a huge
--call-timeout-msno longer
panics. - A05.FR.01: setting the current security profile again is rejected as the whitepaper demands.
- Overlapping
SignCertificaterequests could restore a stale private key.
Central system
- A dropped
callfuture freed the outstanding-CALL slot early. max_stationscould be exceeded by parallel handshakes.- An empty configured key accepted an empty password.
- The
--timeoutof--scriptcould overflow and panic.
🔒 Security hardening
- Unbounded memory growth from unauthenticated stations closed (byte caps, action key length,
station id limits). - Text sent by the other side (station ids, payloads, reasons, certificate subjects) is
sanitised before it reaches terminals, logs and reports; C1 controls and bidi controls
included. A station can no longer forge lines in a script report. - Secrets stay out of output: URL passwords (
--url,--help, environment echo),--auth-key
in clap errors,AuthorizationKeyin schema error messages andDebugoutput. - Certificate checks: a self-signed root needs a valid signature, a
CertificateSignedchain
must be signed link by link, wildcard server certificates are rejected by default. - The secrets file is written before the state file and a corrupt one is moved aside to
.bak
instead of being overwritten. - Release workflow: least-privilege token permissions, third-party actions pinned to commit
SHAs, no expression injection into shell steps, tag/version check. - The npm launcher forwards signals to the native binary, and
npm/build.mjsrefuses to wipe
directories that are not its own output.
🛟 Reliability
- A closed stdout (
| head) no longer kills a run before its JUnit report is written. - An unread stdout or a stalled log file no longer blocks the process or the log file.
- A clipboard helper that never exits no longer blocks the exit of the TUI.
- SIGTERM during
kirby-csms --scriptstill writes the JUnit report. - A scenario
laggedfailure no longer sticks for the rest of the run.
v0.3.0
v0.2.0
What's Changed
- rewrite in in rust by @archhaze24 in #1
New Contributors
- @archhaze24 made their first contribution in #1
Full Changelog: https://github.com/archhaze24/kirby-ocpp/commits/v0.2.0