kirby-csms 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +382 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/kirby-csms.js +95 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kirill Belolipetsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,383 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kirby-ocpp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Terminal OCPP 1.6J charge point emulator for local and staging CSMS testing, written in Rust.
|
|
4
|
+
|
|
5
|
+
It speaks OCPP 1.6 JSON over WebSocket (`ws://` and `wss://`, including mutual TLS), validates every payload against the bundled official OCPP 1.6J JSON schemas, persists station state per charge point id, and can be used three ways:
|
|
6
|
+
|
|
7
|
+
- an interactive terminal UI (default when stdout is a terminal);
|
|
8
|
+
- a headless event logger (`--no-tui`, `--json`);
|
|
9
|
+
- a scripted scenario runner for CI (`kirby-ocpp run --scenario ...`).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
With Node.js (no Rust needed; installs the prebuilt binary for your platform):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
|
|
17
|
+
npm install -g kirby-ocpp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
With Cargo (Rust 1.88 or newer):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cargo install kirby-ocpp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The central system is a separate binary: `cargo install kirby-csms`, or `npm install -g kirby-csms` (`npx kirby-csms --listen 0.0.0.0:9000`). See [kirby-csms](#kirby-csms).
|
|
27
|
+
|
|
28
|
+
Or download a prebuilt binary for Linux (x86_64 glibc, aarch64 glibc, x86_64 static musl for Alpine and minimal containers), macOS or Windows from the [GitHub releases](https://github.com/archhaze24/kirby-ocpp/releases). Each archive holds both binaries (`kirby-ocpp` and `kirby-csms`), the licence, the third-party notices (`THIRD_PARTY_NOTICES.md` and `THIRD_PARTY_NOTICES-kirby-csms.md`) and this README. To build from a checkout: `cargo install --path crates/kirby-ocpp` (and `--path crates/kirby-csms`).
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001 # TUI
|
|
34
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --no-tui --json # headless, JSON lines
|
|
35
|
+
kirby-ocpp --url ws://localhost:9000/ocpp run --scenario charge-session
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`--url` (or `OCPP_URL`) is the only required option. Precedence is flag, then environment variable, then default. Booleans accept `true/false/t/f/yes/no/y/n/on/off/1/0`, case-insensitive; anything else is an error (exit 2).
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
| Flag | Env | Default | Meaning |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `-u, --url` | `OCPP_URL` | required | CSMS WebSocket URL (`ws://` or `wss://`) |
|
|
45
|
+
| `-i, --id` | `OCPP_CHARGE_POINT_ID` | `CP-001` | Charge point id |
|
|
46
|
+
| `--vendor` | `OCPP_VENDOR` | `archhaze24` | BootNotification vendor |
|
|
47
|
+
| `--model` | `OCPP_MODEL` | `KIRBY` | BootNotification model |
|
|
48
|
+
| `--serial` | `OCPP_SERIAL` | none | Serial number (max 25 chars) |
|
|
49
|
+
| `--firmware-version` | `OCPP_FIRMWARE_VERSION` | none | Firmware version (max 50 chars) |
|
|
50
|
+
| `-c, --connector` | | `1` | Initial connector |
|
|
51
|
+
| `--connectors` | | `1` | Number of connectors |
|
|
52
|
+
| `--heartbeat` | | `30` | Fallback heartbeat interval (s) |
|
|
53
|
+
| `--nominal-power-w` | | `7200` | Nominal charging power per connector (W); charging profiles can only lower it |
|
|
54
|
+
| `--voltage-v` | | `230` | Voltage per phase (V), used for A to W conversion and the `Voltage` reading |
|
|
55
|
+
| `--phases` | | `1` | Number of phases (1-3) |
|
|
56
|
+
| `--ws-subprotocol` | `OCPP_WS_SUBPROTOCOL` | `ocpp1.6` | Requested subprotocol |
|
|
57
|
+
| `--ws-ping` | `OCPP_WS_PING_INTERVAL` | `30` | WebSocket ping interval (s), `0` disables |
|
|
58
|
+
| `--reconnect <BOOL>` | `OCPP_RECONNECT` | `true` | Auto-reconnect after unexpected disconnect |
|
|
59
|
+
| `--no-reconnect` | | | Disable auto-reconnect (wins over `--reconnect`) |
|
|
60
|
+
| `--reconnect-initial-ms` | `OCPP_RECONNECT_INITIAL_MS` | `1000` | Initial backoff; `0` is rejected (exit 2) unless `--reconnect-max-attempts` is set or reconnect is off |
|
|
61
|
+
| `--reconnect-max-ms` | `OCPP_RECONNECT_MAX_MS` | `30000` | Maximum backoff |
|
|
62
|
+
| `--reconnect-max-attempts` | `OCPP_RECONNECT_MAX_ATTEMPTS` | `0` | `0` = unlimited |
|
|
63
|
+
| `--call-timeout-ms` | `OCPP_CALL_TIMEOUT_MS` | `30000` | CALL response timeout (1 to 86 400 000) |
|
|
64
|
+
| `--tls-ca` | `OCPP_TLS_CA_FILE` | system roots | CA bundle (PEM) |
|
|
65
|
+
| `--tls-cert`, `--tls-key` | `OCPP_TLS_CERT_FILE`, `OCPP_TLS_KEY_FILE` | | Client certificate and key for mTLS (both or neither) |
|
|
66
|
+
| `--tls-server-name` | `OCPP_TLS_SERVER_NAME` | URL host | SNI / verification name override |
|
|
67
|
+
| `--tls-skip-verify` | `OCPP_TLS_SKIP_VERIFY` | off | Skip certificate verification (testing only) |
|
|
68
|
+
| `--show-secrets` | `OCPP_SHOW_SECRETS` | off | Show `AuthorizationKey` values in frame logs (TUI, `--json`, `--log-file`); by default they are replaced by `***` |
|
|
69
|
+
| `--additional-root-certificate-check` | `OCPP_ADDITIONAL_ROOT_CERTIFICATE_CHECK` | off | Value of the read-only `AdditionalRootCertificateCheck` key: a new Central System root installed with `InstallCertificate` must be signed by the installed one (the old one stays as a fallback until the first TLS connection with the new one) |
|
|
70
|
+
| `--no-persist` | `KIRBY_OCPP_NO_PERSIST` | off | Do not load or save station state |
|
|
71
|
+
| `--state-dir` | `KIRBY_OCPP_STATE_DIR` | `~/.kirby-ocpp/stations` | State directory (`%USERPROFILE%` on Windows) |
|
|
72
|
+
| `--count` | `OCPP_COUNT` | `1` | Number of stations (fleet mode, 1-1000), see [Fleet mode](#fleet-mode) |
|
|
73
|
+
| `--id-template` | `OCPP_ID_TEMPLATE` | the `--id` value | Charge point id template with `{n}` / `{n:03}` |
|
|
74
|
+
| `--first-index` | `OCPP_FIRST_INDEX` | `1` | Index of the first station |
|
|
75
|
+
| `--stagger-ms` | `OCPP_STAGGER_MS` | `0` | Delay between starting two stations (ms) |
|
|
76
|
+
| `--rule ACTION=BEHAVIOR[*N]` | `KIRBY_OCPP_RULES` (`;` separates rules) | none | Response rule for a CSMS command, repeatable; see [Response rules](#response-rules) |
|
|
77
|
+
| `--print-config` | | | Print the effective configuration as JSON and exit |
|
|
78
|
+
| `--no-tui` | | | Never start the TUI; log to stdout |
|
|
79
|
+
| `--json` | | | JSON lines instead of text (events and the `run` report). Implies headless: the terminal UI never opens |
|
|
80
|
+
| `--log-file` | | | Also append the event log to a file; works under the terminal UI too (the file then gets the events, stdout stays the UI's). A helper thread writes it; if the file cannot keep up (stalled mount, unread FIFO) lines are dropped and a `note` line reports the count |
|
|
81
|
+
| `--licenses` | | | Print licence notices and exit |
|
|
82
|
+
| `-V, --version`, `-h, --help` | | | |
|
|
83
|
+
|
|
84
|
+
Options are global and may be placed before or after a subcommand, except `--licenses`, which must come before it (after a subcommand it is a usage error, exit 2).
|
|
85
|
+
|
|
86
|
+
## TLS
|
|
87
|
+
|
|
88
|
+
`wss://` uses rustls. Without `--tls-ca` the operating system trust store is used. Use `--tls-ca` for a private CA, `--tls-cert`/`--tls-key` for client authentication, `--tls-server-name` when the URL host differs from the certificate name, and `--tls-skip-verify` only against local self-signed servers.
|
|
89
|
+
|
|
90
|
+
## State persistence
|
|
91
|
+
|
|
92
|
+
Per charge point id, one JSON file `<state dir>/<id>.station.json` holds the local authorization list and its version, authorization cache, charging profiles, writable configuration values, availability (including a `Scheduled` change waiting for a transaction to end), connector count and per-connector state (status, meter, reservation, transaction) and the offline transaction queue. Ids whose escaped name would exceed 100 bytes are shortened and get a hash suffix (`...~<16 hex>.station.json`) so the name always fits the filesystem limit. Files are versioned and written atomically (temp file plus rename, then the directory is fsynced); a corrupt file or one belonging to another charge point is moved to `<file>.bak` and the station starts clean. `--no-persist` disables both loading and saving.
|
|
93
|
+
|
|
94
|
+
## Terminal UI
|
|
95
|
+
|
|
96
|
+
Seven tabs, switched with `1`-`7` or `Tab`/`Shift-Tab`: Station, Connector, Transaction, Maintenance, Data, Logs, Scenarios. Panes: connectors, actions, log (move focus with Left/Right).
|
|
97
|
+
|
|
98
|
+
| Key | Action |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `Up/Down`, `j/k`, `PageUp/PageDown` | Move in the focused pane |
|
|
101
|
+
| `[` `]` | Previous / next connector |
|
|
102
|
+
| `Enter` | Run the selected action / open the selected log row |
|
|
103
|
+
| letters | Accelerators of the current tab's actions (shown in the list) |
|
|
104
|
+
| `/` or `:` or `Ctrl-P` | Command palette (fuzzy search over all actions); `/` searches the log when the log pane is focused on the Logs tab |
|
|
105
|
+
| `Esc` | Close dialog / clear log search |
|
|
106
|
+
| `?` | Help |
|
|
107
|
+
| `q`, `Ctrl-C` | Quit |
|
|
108
|
+
|
|
109
|
+
Actions: connect/disconnect/reconnect, BootNotification, Heartbeat, add connector, response rules (`m` adds one, `Action=off` removes one, `M` clears all; active rules are listed in the Station pane); plug/unplug, set status, StatusNotification, fault toggle; Authorize, StartTransaction, StopTransaction (with reason), MeterValues; set diagnostics/firmware outcome and send their status notifications; DataTransfer, vendor ping and raw CALLs. Security Whitepaper (Station tab): `C` requests a certificate (key pair, CSR, `SignCertificate`), `S` reports a security event (pick one of the 18 types, optional techInfo), `Y` opens the certificate browser; the Station pane shows the security profile, certificate count, queued security events, a pending CSR and the last log-upload and signed-firmware status with their request ids.
|
|
110
|
+
|
|
111
|
+
Data browser (Station tab: `K` config keys, `L` local authorization list, `P` charging profiles, `Y` certificates (read-only: type, subject, expiry; install/delete come from the CSMS); also in the palette): one popup with four pages, switched with `Tab`/`Shift-Tab`, `Left`/`Right` or `1`-`4`. `/` filters the rows fuzzily (`Enter` keeps the filter, `Esc` clears it), `Up/Down`/`j/k`/`PageUp/PageDown`/`g`/`G` move, `Esc`/`q` closes. Forms and confirmations opened from the browser return to it. Edits are local commands that behave like the CSMS-triggered ones, without a CSMS round trip:
|
|
112
|
+
|
|
113
|
+
| Page | Keys |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| Config keys | `Enter`/`e` edits the selected key (read-only keys are marked `(ro)` and refuse); the value is checked live with the station's own rules, as `ChangeConfiguration` would, and applied with the same side effects (heartbeat/metering timers) and persistence |
|
|
116
|
+
| Local auth list | `a` adds, `Enter`/`e` edits (idTag, status, expiry, parent), `d` removes, `c` clears (asks first). Each add/edit/remove is a differential update that bumps the list version by one; clear empties the list and resets the version to 0 |
|
|
117
|
+
| Charging profiles | `i` installs a profile from JSON (a template is prefilled; paste your own: `csChargingProfiles` object plus connector, validated against the `SetChargingProfile` schema and the station's profile rules), `e` edits the selected one (same id replaces it), `Enter` shows its JSON (`c` there copies), `d` deletes it, `c` clears all (asks first) |
|
|
118
|
+
|
|
119
|
+
The snapshot behind it (`Snapshot::config`, `local_list`, `charging_profiles`) is also available to API users; the local list view holds at most 500 entries (`total` has the real count).
|
|
120
|
+
|
|
121
|
+
Log view (Logs tab): CALL/RESULT/ERROR correlation, filters (all, calls, errors, station, csms; `f`), search (`s`), pause/follow (`p`), jump to newest (`G`), detail view with scroll (`Enter`; `c`/`y` copies), copy row (`y`) or visible rows (`Y`), export as JSONL (`e`, default `./kirby-ocpp-<id>-<timestamp>.jsonl`), clear with confirmation (`c`). The buffer keeps the last 2000 entries and at most about 64 MiB (fleet: less per station). A frame or line longer than 16 KiB is stored cut, ending in `…[truncated N bytes]`; copy and the detail view then show the cut text, JSONL export writes it as a string, and search looks at the first 4 KiB only. Copying tries external clipboard tools first (a tool that has not finished after 500 ms is killed) and falls back to the terminal's OSC 52 sequence (up to about 55 kB of text).
|
|
122
|
+
|
|
123
|
+
Scenarios tab: one `Run: <name>` action per built-in scenario (keys `o`, `w`, `s`, `i` for the first four) and `l` to load a scenario file, `x` to cancel the running one.
|
|
124
|
+
|
|
125
|
+
## Headless mode and exit codes
|
|
126
|
+
|
|
127
|
+
Without a TTY (or with `--no-tui`) the tool connects and prints events until SIGINT/SIGTERM, then closes the WebSocket cleanly. With `--json` each line is one JSON object; frames carry a `payload` field.
|
|
128
|
+
|
|
129
|
+
`kirby-ocpp run --scenario <name|file.toml> [--report report.json] [--junit junit.xml] [--timeout 60] [--id-tag TAG]` connects, waits for an accepted BootNotification, runs the scenario and prints a PASS/FAIL/SKIP report (a JSON object with `"type": "report"` under `--json`). `--junit junit.xml` also writes a JUnit XML report for CI: one `<testsuite>` per run, one `<testcase>` per step (failed step = `<failure>`, skipped = `<skipped/>`); a run that could not connect or boot is a suite with one errored `setup` case.
|
|
130
|
+
|
|
131
|
+
| Code | Meaning |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| 0 | Success (scenario passed, normal shutdown) |
|
|
134
|
+
| 1 | Scenario failed or timed out, or a runtime error |
|
|
135
|
+
| 2 | Usage or configuration error |
|
|
136
|
+
| 130 | `run` interrupted by SIGINT (Ctrl-C); SIGTERM still ends it with 1 |
|
|
137
|
+
|
|
138
|
+
## Fleet mode
|
|
139
|
+
|
|
140
|
+
`--count N` runs N independent stations in one process against the same CSMS, for load and multi-station tests (terminal UI on a TTY, headless with `--no-tui` or without one).
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id 'CP-{n:03}' --count 50 --stagger-ms 100 --no-tui
|
|
144
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id 'CP-{n}' --count 20 run --scenario charge-session
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- The id comes from `--id-template`, or from `--id` when no template is given. `{n}` is the station index, `{n:03}` the index zero-padded to 3 digits; `{{` and `}}` are literal braces. With `--count` above 1 the template needs a placeholder (otherwise exit 2). `--first-index` (default 1) is the index of the first station, so `--count 3 --first-index 10` gives `CP-10`, `CP-11`, `CP-12`. `--count 1` without `--id-template` uses `--id` as is; with an explicit template it is expanded too.
|
|
148
|
+
- Every station is a normal station (own connection, transactions, state file named after its id, so state files never collide). `--stagger-ms` waits that long between starting two stations.
|
|
149
|
+
- The terminal UI shows a selector row on top (`1 CP-001+* 2 CP-002- ...`: index, id, connection glyph `+` connected, `~` connecting/retrying, `-` offline, `*` a transaction is running) and the selected station below. Everything else (status, log, actions, `K`/`L`/`P` browsers, forms, scenarios) works on the selected station only; each station keeps its own log (last 500 rows), dialogs and scenario progress, and events of the other stations keep being recorded. `Alt` combinations may need "Option as Meta" in macOS terminals. With `--count 1` there is no selector and no extra keys. Quit stops the whole fleet.
|
|
150
|
+
- Output is tagged with the station id: `[CP-001] ...` in front of every human line, a `"station"` field in every JSON line, stdout and `--log-file` alike. With `--count 1` nothing is tagged.
|
|
151
|
+
- `run --scenario` runs the scenario on every station at the same time and prints one report per station (tagged), then one aggregate line: `fleet: 3/3 stations PASSED`, or `fleet: 2/3 stations passed, FAILED: CP-2`. With `--json` the aggregate is `{"type":"fleet_report","passed":bool,"total":n,"passed_count":n,"failed":[ids],"stations":[...]}`. `--timeout` covers the whole fleet, stagger included. `--report out.json` writes one file per station: `out.CP-001.json` (characters of the id other than letters, digits, `-` and `_` become `_`). `--junit` writes one file for the whole fleet, with a suite `scenario [CP-001]` per station.
|
|
152
|
+
|
|
153
|
+
| Fleet key | Action |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `<` `>` | Previous / next station |
|
|
156
|
+
| `Alt-1` .. `Alt-9` | Jump to station 1..9 |
|
|
157
|
+
| `Alt-c` / `Alt-d` | Connect / disconnect all stations |
|
|
158
|
+
|
|
159
|
+
Selector keys work in normal mode only (inside a dialog they are plain input).
|
|
160
|
+
- Exit codes: 0 only if every station passes, 1 if any fails or times out, 2 usage error, 130 SIGINT.
|
|
161
|
+
- `--print-config` with `--count > 1` prints `{"fleet": {...}, "config": {...}}` where `config` is the first station.
|
|
162
|
+
|
|
163
|
+
## Scenarios
|
|
164
|
+
|
|
165
|
+
`kirby-ocpp scenarios list` lists the built-in scenarios (`charge-session`, `fault-recovery`, `finish-session`, `offline-sync`); `kirby-ocpp scenarios show <name>` prints one as TOML. Built-ins target connector 1 unless `--connector` is given. `offline-sync` works on a stock station: `LocalAuthorizeOffline` and `AllowOfflineTxForUnknownId` default to `true` (switch them off with `ChangeConfiguration` to test the CSMS-side rejection path).
|
|
166
|
+
|
|
167
|
+
```toml
|
|
168
|
+
name = "my-session"
|
|
169
|
+
description = "Plug in, start, meter, stop"
|
|
170
|
+
|
|
171
|
+
[[steps]]
|
|
172
|
+
action = "plug"
|
|
173
|
+
connector = 1
|
|
174
|
+
|
|
175
|
+
[[steps]]
|
|
176
|
+
action = "start_tx"
|
|
177
|
+
connector = 1
|
|
178
|
+
id_tag = "KIRBY-TAG" # implicitly expects status Accepted
|
|
179
|
+
|
|
180
|
+
[[steps]]
|
|
181
|
+
action = "meter"
|
|
182
|
+
connector = 1
|
|
183
|
+
wh_delta = 1000
|
|
184
|
+
|
|
185
|
+
[[steps]]
|
|
186
|
+
action = "expect_call" # a CALL seen since the last matched expect_call (CALLs of other names are not kept; a payload only if some step uses `match`)
|
|
187
|
+
call = "MeterValues"
|
|
188
|
+
timeout_ms = 3000
|
|
189
|
+
|
|
190
|
+
[[steps]]
|
|
191
|
+
action = "stop_tx"
|
|
192
|
+
connector = 1
|
|
193
|
+
reason = "Local"
|
|
194
|
+
expect = { status = "Accepted" }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Actions: `connect`, `disconnect`, `reconnect`, `boot`, `heartbeat`, `plug`, `unplug`, `authorize`, `start_tx`, `stop_tx`, `meter`, `status`, `fault`, `clear_fault`, `sleep`, `wait_for`, `expect_call` (`call`, optional `direction`, `match` payload subset), `send_raw` (`call`, `payload`, `validate`), `data_transfer`, `set_rule` (`rule`), `clear_rules` (optional `call`), `request_certificate` (Security Whitepaper: key pair + CSR + `SignCertificate`), `security_event` (`event_type`, optional `tech_info`; the event type is checked when the scenario loads). Every step may have `name` and `expect` (`status`, `payload` subset, `error = true`). `wait_for` takes `condition = { kind = ... }` with kinds `connected`, `disconnected`, `registration`, `connector_status`, `tx_active`, `tx_idle`, `no_pending_messages`, and `timeout_ms` (default 5000). Unknown fields are rejected.
|
|
198
|
+
|
|
199
|
+
`expect_call` fails with "the scenario runner fell behind" only when events were lost and the wanted CALL was not among the frames seen before the loss; the failure reports the loss once, so it does not poison later `expect_call` steps. CALLs already matched (or skipped by a match) are forgotten, and at most 10 000 unmatched CALLs (about 16 MiB) are kept.
|
|
200
|
+
|
|
201
|
+
## Response rules
|
|
202
|
+
|
|
203
|
+
Response rules make the station misbehave on purpose, to test how a CSMS reacts. A rule overrides how one CSMS command (any of the 26 the station handles: the 19 of OCPP 1.6 and the Security Whitepaper commands, e.g. `Reset`, `RemoteStartTransaction`, `InstallCertificate`, `GetLog`, `ExtendedTriggerMessage`, `SignedUpdateFirmware`) is answered. Form: `ACTION=BEHAVIOR[*COUNT]`, action names case-insensitive.
|
|
204
|
+
|
|
205
|
+
| Behavior | Effect |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `reject` | The action's own rejection (`Rejected`; `NotSupported` for `SendLocalList` and `UnlockConnector`; `Failed` for `DeleteCertificate`; `NotFound` for `GetInstalledCertificateIds`). `GetConfiguration`, `GetLocalListVersion`, `ClearChargingProfile`, `GetDiagnostics` and `UpdateFirmware` have no rejection status, so they get CALLERROR `NotSupported`. |
|
|
208
|
+
| `delay:MS` | The command is handled normally; only the reply is sent `MS` ms later (0 to 3600000). The station keeps working meanwhile. |
|
|
209
|
+
| `error:CODE[:TEXT]` | CALLERROR with an OCPP 1.6 code (`NotImplemented`, `NotSupported`, `InternalError`, `ProtocolError`, `SecurityError`, `FormationViolation`, `PropertyConstraintViolation`, `OccurenceConstraintViolation`, `TypeConstraintViolation`, `GenericError`). |
|
|
210
|
+
| `drop` | Close the WebSocket without replying; the normal reconnect lifecycle follows (disabled by `--no-reconnect`). |
|
|
211
|
+
| `ignore` | Never reply, so the CSMS call timeout fires. |
|
|
212
|
+
|
|
213
|
+
`*COUNT` applies the rule that many times and then removes it (default: unlimited). There is one rule per action; setting another replaces it. Except for `delay`, the command is **not executed** (no side effects). Rules are runtime only, never persisted. Set them with `--rule` (repeatable) or `KIRBY_OCPP_RULES`; in both, `;` separates several rules, so a rule (e.g. an `error:` text) must not contain `;`, in the TUI (Station tab, `m`), as scenario steps, or through `Command::SetResponseRule` / `ClearResponseRules` in the core API. Each application is logged (`response rule: Reset -> reject, 2 more`).
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --rule Reset=reject --rule GetConfiguration=delay:5000 \
|
|
217
|
+
--rule ChangeConfiguration=error:InternalError*3 --rule RemoteStartTransaction=ignore*1
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
In scenarios: `action = "set_rule"` with `rule = "Reset=reject*2"`, and `action = "clear_rules"` with an optional `call = "Reset"` (all rules when omitted).
|
|
221
|
+
|
|
222
|
+
## kirby-csms
|
|
223
|
+
|
|
224
|
+
`kirby-csms` is a terminal OCPP 1.6J central system (CSMS) for charge point firmware developers and load tests. It accepts any number of stations on `ws(s)://HOST:PORT/<any prefix>/<station id>` (the station id is the last path segment), answers their CALLs with canned replies, prints everything that happens and sends commands to stations. On an interactive terminal it opens a terminal UI to observe stations and send them commands; otherwise it runs headless (commands then come from stdin JSON lines or scripts).
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
kirby-csms --listen 0.0.0.0:9000
|
|
228
|
+
kirby-csms --listen 127.0.0.1:0 --json --subprotocol omit
|
|
229
|
+
kirby-csms --tls-cert cert.pem --tls-key key.pem
|
|
230
|
+
kirby-csms --script suite.toml --junit junit.xml --timeout 120
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The first output line is `listening on ws://ADDR` (JSON: `{"type":"listening","addr":"127.0.0.1:41234","url":"ws://127.0.0.1:41234"}`), so `--listen 127.0.0.1:0` works for tests. Then one line per event: station connected / disconnected (with reason), every frame in and out, and a compact line when connector statuses or active transactions of a station change. SIGINT or SIGTERM shuts down gracefully. During `--script`, SIGTERM still writes the `--junit` file (one failed suite, `interrupted`) and exits 1; SIGINT exits 130 without a report. Text supplied by stations is shown with control characters replaced by `·`. Stdout is written by a helper thread: if nobody reads it, the process keeps running, `--log-file` keeps being written, and the lines that did not fit are dropped with a `note` line saying how many.
|
|
234
|
+
|
|
235
|
+
| Flag | Env (`KIRBY_CSMS_`) | Default | Meaning |
|
|
236
|
+
| --- | --- | --- | --- |
|
|
237
|
+
| `--listen ADDR` | `LISTEN` | `0.0.0.0:9000` | address, port 0 picks a free one |
|
|
238
|
+
| `--tls-cert FILE`, `--tls-key FILE` | `TLS_CERT`, `TLS_KEY` | none | PEM, both or none, serves `wss://` |
|
|
239
|
+
| `--tls-client-ca FILE` | `TLS_CLIENT_CA` | none | CA certificate(s) (PEM) clients must be signed by (mutual TLS); needs `--tls-cert` |
|
|
240
|
+
| `--security-profile 0..3` | `SECURITY_PROFILE` | 0 | Security Whitepaper profile, see below |
|
|
241
|
+
| `--auth-key ID=KEY` | `AUTH_KEY` (`ID=KEY;ID=KEY`) | none | Basic Auth key of one station (repeatable); the key is visible in `ps` and shell history, so prefer `KIRBY_CSMS_AUTH_KEY` or `--auth-keys-file` (keep it `0600`) |
|
|
242
|
+
| `--auth-keys-file FILE` | `AUTH_KEYS_FILE` | none | lines `ID=KEY` (`#` comments); keep it private, its permissions are not checked |
|
|
243
|
+
| `--default-auth-key KEY` | `DEFAULT_AUTH_KEY` | none | key for stations without an entry |
|
|
244
|
+
| `--subprotocol POLICY` | `SUBPROTOCOL` | `require:ocpp1.6` | `require:NAME`, `select:NAME`, `omit`, `reject` |
|
|
245
|
+
| `--transaction-ids POLICY` | `TRANSACTION_IDS` | `fixed:123` | `fixed:ID` or `incrementing:FIRST` |
|
|
246
|
+
| `--heartbeat-interval SECONDS` | `HEARTBEAT_INTERVAL` | 300 | interval in the BootNotification answer |
|
|
247
|
+
| `--history-cap N` | `HISTORY_CAP` | 1000 | frames kept per station |
|
|
248
|
+
| `--show-secrets` | `SHOW_SECRETS` | off | keep `AuthorizationKey` values in recorded frames (history, `--json`, `--log-file`, script matching); by default they are replaced by `***` |
|
|
249
|
+
| `--validate-inbound BOOL` | `VALIDATE_INBOUND` | true | check every inbound CALL against the OCPP 1.6J schema; an invalid payload gets a CALLERROR (`OccurenceConstraintViolation`, `TypeConstraintViolation`, `PropertyConstraintViolation` or `FormationViolation`) instead of the canned reply and is not tracked. A scripted fault on the same action still wins; unknown actions are not checked |
|
|
250
|
+
| `--ping-interval SECONDS` | `PING_INTERVAL` | 30 | WebSocket ping to each station after this much silence, `0` disables the watchdog |
|
|
251
|
+
| `--pong-timeout SECONDS` | `PONG_TIMEOUT` | 10 | a station that sends nothing this long after a ping is dropped (disconnect reason `ping_timeout`); a non-zero `--ping-interval` below this is a usage error (exit 2) |
|
|
252
|
+
| `--transaction-history-cap N` | `TRANSACTION_HISTORY_CAP` | 100 | stopped transactions kept per station |
|
|
253
|
+
| `--call-timeout SECONDS` | `CALL_TIMEOUT` | 30 | wait for a station's answer to a command (1 to 86 400) |
|
|
254
|
+
| `--json` | `JSON` | off | JSON lines instead of text |
|
|
255
|
+
| `--log-file PATH` | `LOG_FILE` | none | append every event as a JSON line (written by a helper thread; lines that cannot be written in time are dropped and a `note` line counts them) |
|
|
256
|
+
| `--no-tui` | `NO_TUI` | off | never open the terminal UI, log to stdout |
|
|
257
|
+
| `--stdin` | `STDIN` | auto | read JSON-line commands even on a terminal |
|
|
258
|
+
| `--script FILE` | `SCRIPT` | none | run a TOML script, print the report, exit |
|
|
259
|
+
| `--report PATH`, `--junit PATH` | | none | script report as JSON / JUnit XML |
|
|
260
|
+
| `--timeout SECONDS` | | 60 | overall limit of a script run (1 to 86 400) |
|
|
261
|
+
| `--licenses` | | | print third-party notices |
|
|
262
|
+
|
|
263
|
+
Command line beats environment beats default; booleans accept `true/false/t/f/yes/no/y/n/on/off/1/0`, case-insensitive, and nothing else.
|
|
264
|
+
|
|
265
|
+
**Security profiles** (OCPP 1.6 Security Whitepaper): `0` accepts everybody. `1` requires HTTP Basic Auth (user name = station id from the URL, password = that station's key) over `ws://` or `wss://`; `2` is the same over TLS (`--tls-cert`/`--tls-key` required); `3` requires TLS with a client certificate signed by `--tls-client-ca` whose Common Name equals the station id. Profiles 1 and 2 need at least one key (`--auth-key`, `--auth-keys-file` or `--default-auth-key`); a station without a key is refused. The key is compared byte for byte with the password the station sends (the station's `AuthorizationKey` configuration value). Refusals answer HTTP 401 (403 for certificates) and print an `auth_failed` event (JSON: `{"type":"auth_failed","station":..,"remote_addr":..,"reason":..}`); keys are never printed. A failed TLS handshake under profile 3 is reported with `"station":null`. Invalid flag combinations exit with code 2. The CSMS also tracks what stations report (security events, `LogStatusNotification`, `SignedFirmwareStatusNotification`, the last `SignCertificate` CSR, the last answered `GetInstalledCertificateIds` per certificate type) in each station's state, and the engine has typed commands for `CertificateSigned`, `InstallCertificate`, `DeleteCertificate`, `GetInstalledCertificateIds`, `ExtendedTriggerMessage`, `GetLog` and `SignedUpdateFirmware` (headless: raw `action`/`payload` commands).
|
|
266
|
+
|
|
267
|
+
### Terminal UI
|
|
268
|
+
|
|
269
|
+
With a terminal on both stdin and stdout, and none of `--no-tui`, `--json`, `--script`, `--stdin`, `kirby-csms` opens a terminal UI (`--no-tui` or `KIRBY_CSMS_NO_TUI=1` keeps it headless). The `listening on ...` line goes to stderr before the screen opens; if the terminal cannot be set up, the server runs headless and says so. `--log-file` still receives every event. Quitting the UI (`q`, Ctrl-C) shuts the server down.
|
|
270
|
+
|
|
271
|
+
Left: the stations (dot: green connected, dim disconnected, yellow replaced by a newer connection; vendor model; `txN` active transactions), header with the listen address, connected/total stations and frame counters. Right: details of the selected station (boot info, remote address, subprotocol, connectors, transactions with energy and power, last heartbeat, firmware/diagnostics status, counters) above the frame log of that station, with CALL/RESULT correlation and round-trip times.
|
|
272
|
+
|
|
273
|
+
| Key | Action |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `Up` `Down` `j` `k`, `PgUp` `PgDn`, `Home` `End` | move in the focused pane |
|
|
276
|
+
| `Left` `Right` `Tab` `Shift-Tab` | focus the station list / the log |
|
|
277
|
+
| `/` | filter stations by id (fuzzy); in the log pane: search the log |
|
|
278
|
+
| `Esc` | clear the log search, then the station filter |
|
|
279
|
+
| `a` | log of the selected station / merged live log of all stations |
|
|
280
|
+
| `Enter` (log) | frame detail with pretty JSON and its request or answer (`c` copies) |
|
|
281
|
+
| `y` `Y` | copy the row / all visible rows (clipboard tool, else OSC 52) |
|
|
282
|
+
| `f` `s` `p` `G` | log filter (all, calls, errors, from stations), search, pause/follow, newest |
|
|
283
|
+
| `:` | command palette: fuzzy search over every action below; unavailable ones are dimmed with the reason |
|
|
284
|
+
| `?` | help |
|
|
285
|
+
| `q`, `Ctrl-C` | quit |
|
|
286
|
+
|
|
287
|
+
Connections refused by the security profile show as a status-line notice (at most one per second) and a `N auth failures` chip in the header, next to a `security profile N` chip. Actions on the selected station open a form (`Tab` next field, `Left` `Right` or a letter picks an option, `Enter` sends, `Esc` cancels). Fields are checked as you type, and the request is checked against the OCPP 1.6J schema before anything is sent; errors show under the field. Commands run in the background (several can be in flight, the header shows `N pending`); the answer appears in the status line (`Reset (Soft) on CP-1: Accepted`, or the CALLERROR code and text, or a timeout) and is kept in a history of the last 20 results per station. Answers that carry content (GetConfiguration as a readable key list with `(readonly)` marks and unknown keys, raw calls, DataTransfer with data) open in a popup (`c` copies).
|
|
288
|
+
|
|
289
|
+
| Key | Action |
|
|
290
|
+
| --- | --- |
|
|
291
|
+
| `t` | RemoteStartTransaction (idTag, optional connector) |
|
|
292
|
+
| `x` | RemoteStopTransaction (pick one of the active transactions) |
|
|
293
|
+
| `R` | Reset, soft or hard (asks first) |
|
|
294
|
+
| `c` `g` | ChangeConfiguration / GetConfiguration (keys optional) |
|
|
295
|
+
| `T` | TriggerMessage (message, optional connector) |
|
|
296
|
+
| `u` `A` `C` | UnlockConnector / ChangeAvailability / ClearCache |
|
|
297
|
+
| `P` | SetChargingProfile (connector and profile JSON, validated against the schema) |
|
|
298
|
+
| `D` | DataTransfer |
|
|
299
|
+
| `l` | GetLog (log type, request id, upload URL, optional retries and time range) |
|
|
300
|
+
| `I` `d` | InstallCertificate (paste the PEM; line breaks become spaces in the field and are rebuilt on submit) / DeleteCertificate (hash algorithm and the three hashes) |
|
|
301
|
+
| `L` | GetInstalledCertificateIds (the hash data opens as a readable list) |
|
|
302
|
+
| `E` | ExtendedTriggerMessage (`SignChargePointCertificate` preselected) |
|
|
303
|
+
| `S` `X` | SignedUpdateFirmware (URL, retrieve time, request id, signing certificate PEM, signature) / CertificateSigned (PEM chain) |
|
|
304
|
+
| `e` | the last 100 security events the station reported (scrollable); the Station pane shows the last three, the last log and signed firmware status and a CSR marker |
|
|
305
|
+
| `K` `H` | ClearChargingProfile (optional id, connector, purpose, stack level) / GetCompositeSchedule (connector, duration, optional rate unit; the answer opens in a popup) |
|
|
306
|
+
| `V` | ReserveNow (connector, expiry, idTag, optional parentIdTag, reservation id) |
|
|
307
|
+
| `U` `O` | UpdateFirmware (URL, retrieve time, optional retries) / GetDiagnostics (upload URL, optional retries and time range) |
|
|
308
|
+
| `B` | SendLocalList (Differential or Full, list version, entries as comma separated `idTag[:status]`, status defaults to Accepted, `idTag:-` deletes in a differential update, at most 100 entries) |
|
|
309
|
+
| `w` | raw call: any of the 26 central system actions with a JSON payload checked against its request schema |
|
|
310
|
+
| `r` | the last results of the selected station (`Enter` opens one) |
|
|
311
|
+
| `m` | response rule: answer every station's CALLs of one action with a fixed payload |
|
|
312
|
+
| `i` | inject a fault for the next N occurrences of an action: `call_error`, `silence`, `close`, `invalid_payload` |
|
|
313
|
+
| `M` | clear every response rule and pending fault (asks first); one rule can be removed from the palette |
|
|
314
|
+
| `F` | forget a disconnected station and its state (asks first) |
|
|
315
|
+
| `n` | copy the station id |
|
|
316
|
+
|
|
317
|
+
Rules and faults apply to all stations, not only the selected one; while any is active the header shows the pending fault count and the rule actions.
|
|
318
|
+
|
|
319
|
+
### Commands on stdin
|
|
320
|
+
|
|
321
|
+
Without `--script`, when stdin is not a terminal (or with `--stdin`), each input line is a JSON object and gets exactly one answer line. End of input does not stop the server. Answers are always JSON, also without `--json`.
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
{"station":"CP-1","action":"ChangeConfiguration","payload":{"key":"HeartbeatInterval","value":"60"},"timeout_ms":5000,"id":"my-tag"}
|
|
325
|
+
-> {"type":"result","station":"CP-1","action":"ChangeConfiguration","ok":true,"result":{"status":"Accepted"},"id":"my-tag"}
|
|
326
|
+
-> {"type":"result","station":"CP-1","action":"Reset","ok":false,"error":"CALLERROR NotSupported: ..."}
|
|
327
|
+
{"cmd":"stations"}
|
|
328
|
+
-> {"type":"stations","stations":[{"id":"CP-1","connected":true,"vendor":"...","model":"...","connectors":{"1":"Available"},"active_transactions":[]}]}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Answers can arrive out of order (a slow command does not hold back later ones): match them to commands by `id`, not by position. `payload` defaults to `{}`, `timeout_ms` to `--call-timeout`, `id` is optional and echoed. Payloads are sent as given (not schema-checked), so malformed requests can be tested. A CALLERROR, a timeout, an unknown station or a bad line is an `"ok":false` result with an `error` text, never a crash.
|
|
332
|
+
|
|
333
|
+
### Scripts
|
|
334
|
+
|
|
335
|
+
A script is TOML with sequential steps; the first failing step stops the run (later steps are `SKIP`). The stations are external processes (for example `kirby-ocpp`) that connect while the script waits.
|
|
336
|
+
|
|
337
|
+
```toml
|
|
338
|
+
name = "smoke"
|
|
339
|
+
|
|
340
|
+
[[steps]]
|
|
341
|
+
action = "wait_station" # station = "CP-1" and/or count = N; default timeout 30000 ms
|
|
342
|
+
station = "CP-1"
|
|
343
|
+
booted = true # also wait for its BootNotification (default: the socket is open)
|
|
344
|
+
|
|
345
|
+
[[steps]]
|
|
346
|
+
action = "wait_call" # an inbound CALL; optional station and payload_contains (subset match)
|
|
347
|
+
call = "StartTransaction"
|
|
348
|
+
payload_contains = { connectorId = 1 }
|
|
349
|
+
timeout_ms = 20000
|
|
350
|
+
|
|
351
|
+
[[steps]]
|
|
352
|
+
action = "call" # raw CALL; expect_status checks payload.status, expect_error a CALLERROR
|
|
353
|
+
station = "CP-1"
|
|
354
|
+
call = "ChangeConfiguration"
|
|
355
|
+
payload = { key = "HeartbeatInterval", value = "60" }
|
|
356
|
+
expect_status = "Accepted"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`wait_call` consumes the CALL it matches: each inbound CALL satisfies at most one `wait_call`, but waits may come in any order (a later wait can match a CALL that arrived earlier than the previous wait's match). CALLs that arrive while other steps run are kept (the last 10 000, at most about 16 MiB); only CALLs some `wait_call` of the script names are remembered, and their payload only if a `wait_call` for that action uses `payload_contains`.
|
|
360
|
+
|
|
361
|
+
Other steps: `sleep {ms}`, `respond {call, payload}` (answer every such CALL with this CALLRESULT payload), `clear_respond {call}`, `inject_fault {call, kind = "call_error" | "silence" | "close" | "invalid_payload", payload, times}`. Every step takes an optional `name`; `timeout_ms` defaults to 5000. The report looks like `kirby-ocpp run` (`PASS` / `FAIL` / `SKIP` per step; `--json` for a JSON report line).
|
|
362
|
+
|
|
363
|
+
Exit codes: 0 success or clean shutdown, 1 script failed or timed out (or the server could not start), 2 usage, configuration or script error, 130 script interrupted by SIGINT.
|
|
364
|
+
|
|
365
|
+
## Testing against a mock
|
|
366
|
+
|
|
367
|
+
`crates/kirby-ocpp-testkit` is a dev-only scripted mock CSMS (plain and TLS) used by the integration tests; it is not published. Use it from tests of your own, or point `kirby-ocpp` at any real or local CSMS.
|
|
368
|
+
|
|
369
|
+
## Development
|
|
370
|
+
|
|
371
|
+
Workspace crates: `kirby-ocpp-core` (protocol, station state machine, scenarios, WebSocket/TLS; no terminal code), `kirby-ocpp-tui` (ratatui UI), `kirby-ocpp` (binary: CLI and headless runner), `kirby-ocpp-testkit` (mock CSMS, not published).
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
cargo fmt --all --check
|
|
375
|
+
cargo clippy --workspace --all-targets -- -D warnings
|
|
376
|
+
cargo test --workspace # includes doc tests and process-level CLI tests
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Protocol coverage is documented in [docs/OCPP_1.6J_CONFORMANCE.md](docs/OCPP_1.6J_CONFORMANCE.md).
|
|
380
|
+
|
|
381
|
+
## License
|
|
382
|
+
|
|
383
|
+
MIT, see [LICENSE](LICENSE). The bundled OCPP JSON schemas belong to the Open Charge Alliance; see [THIRD_PARTY_NOTICES.md](crates/kirby-ocpp/THIRD_PARTY_NOTICES.md) or run `kirby-ocpp --licenses`. Crate licences are recorded in `Cargo.lock` and the crates' metadata.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
## OCPP 1.6J JSON schemas and Security Whitepaper schemas
|
|
4
|
+
|
|
5
|
+
`kirby-csms` bundles the JSON schemas of the OCPP 1.6J specification package (56 files, `crates/kirby-ocpp-core/schemas`) and of the OCPP 1.6 Security Whitepaper "Improved security for OCPP 1.6-J" (22 files: CertificateSigned, DeleteCertificate, ExtendedTriggerMessage, GetInstalledCertificateIds, GetLog, InstallCertificate, LogStatusNotification, SecurityEventNotification, SignCertificate, SignedFirmwareStatusNotification, SignedUpdateFirmware and their responses). They are embedded in the binary for local protocol validation.
|
|
6
|
+
|
|
7
|
+
- Creator and copyright holder: Open Charge Alliance. Copyright (c) 2010-2025 Open Charge Alliance for the OCPP 1.6 specification package, copyright (c) 2022 and 2026 Open Charge Alliance for Security Whitepaper editions 3 and 4. All rights reserved by the Open Charge Alliance.
|
|
8
|
+
- Licence: the Open Charge Alliance publishes these materials under the Creative Commons Attribution-NoDerivatives 4.0 International Public License (CC BY-ND 4.0): https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
|
9
|
+
- Source: the official downloads at https://openchargealliance.org/my-oca/ocpp/ ("OCPP 1.6 (all files & errata)", "OCPP 1.6 Security Whitepaper Edition 4" and the archived Edition 3). The bundled files are byte-for-byte identical to the schemas in those packages (the whitepaper schemas are identical in editions 3 and 4). They are included unmodified.
|
|
10
|
+
- Disclaimer: the materials are provided as is, without warranties of any kind, as stated in section 5 of the CC BY-ND 4.0 licence. The Open Charge Alliance does not sponsor or endorse this project.
|
|
11
|
+
|
|
12
|
+
The project licence in `LICENSE` applies to this project's own source code, not to Open Charge Alliance materials.
|
|
13
|
+
|
|
14
|
+
## Rust dependencies
|
|
15
|
+
|
|
16
|
+
`kirby-csms` is statically linked from open-source Rust crates (tokio, rustls, ring, x509-parser, rcgen, ratatui, crossterm, clap, serde and others), each under its own licence (MIT, Apache-2.0, ISC, BSD and similar permissive licences). Their names, versions and licences are recorded in `Cargo.lock` and each crate's own metadata; run `cargo install cargo-license && cargo license` in a source checkout, or inspect the dependency tree with `cargo tree`, for the exact list. Binary distributions must keep this notice together with the licence texts of those crates.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher: runs the native binary shipped by the matching platform package. One script
|
|
3
|
+
// serves both launcher packages: build.mjs copies it to bin/<name>.js and the binary name
|
|
4
|
+
// (`kirby-ocpp` or `kirby-csms`) is taken from the script's own file name. Both binaries
|
|
5
|
+
// live in the same @archhaze24/kirby-ocpp-<platform> packages.
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const { spawn } = require("node:child_process");
|
|
9
|
+
const os = require("node:os");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
|
|
12
|
+
const NAME = path.basename(__filename, ".js");
|
|
13
|
+
|
|
14
|
+
// Candidate packages per platform, in order of preference. The musl build is fully static,
|
|
15
|
+
// so on Linux x64 it is also a valid fallback when the glibc package was not installed.
|
|
16
|
+
const PLATFORMS = {
|
|
17
|
+
"linux-x64": ["@archhaze24/kirby-ocpp-linux-x64", "@archhaze24/kirby-ocpp-linux-x64-musl"],
|
|
18
|
+
"linux-arm64": ["@archhaze24/kirby-ocpp-linux-arm64"],
|
|
19
|
+
"darwin-x64": ["@archhaze24/kirby-ocpp-darwin-x64"],
|
|
20
|
+
"darwin-arm64": ["@archhaze24/kirby-ocpp-darwin-arm64"],
|
|
21
|
+
"win32-x64": ["@archhaze24/kirby-ocpp-win32-x64"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// True on a musl-based Linux (Alpine etc.): glibc reports its version, musl does not.
|
|
25
|
+
function isMusl() {
|
|
26
|
+
if (process.platform !== "linux") return false;
|
|
27
|
+
try {
|
|
28
|
+
const report = process.report.getReport();
|
|
29
|
+
const header = typeof report === "string" ? JSON.parse(report).header : report.header;
|
|
30
|
+
if (header && "glibcVersionRuntime" in header) return !header.glibcVersionRuntime;
|
|
31
|
+
} catch {
|
|
32
|
+
// fall through to the file check
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return require("node:fs").readdirSync("/lib").some((name) => name.startsWith("ld-musl-"));
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function binaryPath() {
|
|
42
|
+
const key = `${process.platform}-${process.arch}`;
|
|
43
|
+
const candidates = PLATFORMS[key];
|
|
44
|
+
if (!candidates) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${NAME} has no prebuilt binary for ${key}. Supported: ${Object.keys(PLATFORMS).join(", ")}. ` +
|
|
47
|
+
`Build from source instead: cargo install ${NAME}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const ordered = key === "linux-x64" && isMusl() ? [...candidates].reverse() : candidates;
|
|
51
|
+
const exe = process.platform === "win32" ? `${NAME}.exe` : NAME;
|
|
52
|
+
for (const pkg of ordered) {
|
|
53
|
+
try {
|
|
54
|
+
const manifest = require.resolve(`${pkg}/package.json`);
|
|
55
|
+
return path.join(path.dirname(manifest), "bin", exe);
|
|
56
|
+
} catch {
|
|
57
|
+
// not installed, try the next candidate
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error(
|
|
61
|
+
`The platform package ${ordered[0]} is not installed (npm skipped the optional dependency). ` +
|
|
62
|
+
`Reinstall without --omit=optional, or run: cargo install ${NAME}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let binary;
|
|
67
|
+
try {
|
|
68
|
+
binary = binaryPath();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`${NAME}: ${error.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
75
|
+
|
|
76
|
+
// Forward termination signals so the native process does not outlive the launcher (docker
|
|
77
|
+
// stop, timeout, CI cancel). Windows only supports SIGINT/SIGBREAK; the others are skipped.
|
|
78
|
+
const FORWARDED = process.platform === "win32" ? ["SIGINT", "SIGBREAK"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
79
|
+
for (const signal of FORWARDED) {
|
|
80
|
+
process.on(signal, () => {
|
|
81
|
+
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
child.on("error", (error) => {
|
|
86
|
+
console.error(`${NAME}: could not run ${binary}: ${error.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
child.on("exit", (code, signal) => {
|
|
91
|
+
if (signal) {
|
|
92
|
+
process.exit(128 + (os.constants.signals[signal] ?? 0));
|
|
93
|
+
}
|
|
94
|
+
process.exit(code ?? 1);
|
|
95
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kirby-csms",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Terminal OCPP 1.6J central system (CSMS) for charge point firmware development and load tests",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ocpp",
|
|
7
|
+
"ocpp16",
|
|
8
|
+
"ev",
|
|
9
|
+
"csms",
|
|
10
|
+
"central-system",
|
|
11
|
+
"charge-point",
|
|
12
|
+
"tui"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "archhaze24",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/archhaze24/kirby-ocpp.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/archhaze24/kirby-ocpp#readme",
|
|
21
|
+
"bin": {
|
|
22
|
+
"kirby-csms": "bin/kirby-csms.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"THIRD_PARTY_NOTICES.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"optionalDependencies": {
|
|
34
|
+
"@archhaze24/kirby-ocpp-linux-x64": "0.4.0",
|
|
35
|
+
"@archhaze24/kirby-ocpp-linux-x64-musl": "0.4.0",
|
|
36
|
+
"@archhaze24/kirby-ocpp-linux-arm64": "0.4.0",
|
|
37
|
+
"@archhaze24/kirby-ocpp-darwin-x64": "0.4.0",
|
|
38
|
+
"@archhaze24/kirby-ocpp-darwin-arm64": "0.4.0",
|
|
39
|
+
"@archhaze24/kirby-ocpp-win32-x64": "0.4.0"
|
|
40
|
+
}
|
|
41
|
+
}
|