hector-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,1401 @@
1
+ Metadata-Version: 2.4
2
+ Name: hector-cli
3
+ Version: 0.1.0
4
+ Summary: YAML-driven Renode + Verilator simulation orchestrator for embedded hardware CI
5
+ Home-page: https://hector-ci.com
6
+ Author: Nemesis
7
+ Author-email: info@hector-ci.com
8
+ License: AGPL-3.0-or-later
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pyyaml>=6.0
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: license
26
+ Dynamic: license-file
27
+ Dynamic: requires-dist
28
+ Dynamic: requires-python
29
+ Dynamic: summary
30
+
31
+ # Hector
32
+
33
+ **`hector`** is the open-source CLI at the core of the Hector platform: a
34
+ YAML-driven orchestrator for simulating embedded hardware systems. It combines
35
+ [Renode](https://renode.io/) machine emulation with
36
+ [Verilator](https://www.veripool.org/verilator/) co-simulated HDL modules,
37
+ wiring them together from a single `.hector.yaml` file. Everything runs inside
38
+ the official `antmicro/renode` Docker image, so builds are reproducible
39
+ regardless of host toolchain.
40
+
41
+ One config drives every mode — interactive simulation (`hector run`), automated
42
+ testing (`hector test`), and command export (`hector export`) — and the same
43
+ file is what [Hector CI](https://hector-ci.com) runs in continuous integration.
44
+
45
+ Licensed under **AGPL-3.0-or-later** — see [LICENSE](LICENSE) and
46
+ [LICENSING.md](LICENSING.md).
47
+
48
+ ---
49
+
50
+ ## Contents
51
+
52
+ - [Requirements](#requirements)
53
+ - [Quick start](#quick-start)
54
+ - [Invocation modes](#invocation-modes)
55
+ - [Configuration reference](#configuration-reference)
56
+ - [Top-level fields](#top-level-fields)
57
+ - [arguments](#arguments)
58
+ - [matrix](#matrix)
59
+ - [modules](#modules)
60
+ - [hubs](#hubs)
61
+ - [machines](#machines)
62
+ - [peripherals](#peripherals)
63
+ - [connections (per-machine)](#connections-per-machine)
64
+ - [mappings](#mappings)
65
+ - [artifacts](#artifacts)
66
+ - [connections (global)](#connections-global)
67
+ - [mappings (global)](#mappings-global)
68
+ - [build](#build)
69
+ - [tests](#tests)
70
+ - [quantum](#quantum)
71
+ - [ci](#ci)
72
+ - [Connection syntax](#connection-syntax)
73
+ - [Interpolation](#interpolation)
74
+ - [Generated artifacts](#generated-artifacts)
75
+ - [Complete example](#complete-example)
76
+
77
+ ---
78
+
79
+ ## Requirements
80
+
81
+ - Python 3.10+ with `pyyaml` (`pip install pyyaml`)
82
+ - Docker (the Renode image is pulled automatically on first run)
83
+ - Git — only needed when a config builds `modules:` or uses `${RENODE_DIR}`; the framework then clones the Renode source on first run (see [Renode source checkout](#renode-source-checkout))
84
+
85
+ ---
86
+
87
+ ## Quick start
88
+
89
+ ```
90
+ project/
91
+ ├── .hector
92
+ └── platforms/
93
+ └── boards/
94
+ └── stm32f4_discovery.repl # your Renode platform file
95
+ ```
96
+
97
+ Bootstrap a new project:
98
+
99
+ ```bash
100
+ hector init
101
+ ```
102
+
103
+ Then simulate or test:
104
+
105
+ ```bash
106
+ hector run # simulate (interactive Renode monitor)
107
+ hector test # run the tests: section
108
+ hector export # print the run command without executing it
109
+ ```
110
+
111
+ Generated files go in `.hector/` — add it to your `.gitignore`. The Renode **binary** runs from the Docker image; the Renode **source** is only cloned when a config needs it (see [Renode source checkout](#renode-source-checkout)).
112
+
113
+ ---
114
+
115
+ ## Invocation modes
116
+
117
+ | Command | Behaviour |
118
+ |---|---|
119
+ | `hector run` | Run simulation interactively (Renode monitor) |
120
+ | `hector run --set BIN=fw.elf` | Override a config argument at the command line |
121
+ | `hector run --set BIN=fw.elf --set BOARD=nucleo` | Override multiple arguments |
122
+ | `hector run --debug boardA:3333` | Halt a CPU and open a GDB server |
123
+ | `hector run --debug boardA:3333 --debug boardB:3334` | Halt multiple CPUs on different ports |
124
+ | `hector run --renode-args '--console'` | Pass extra flags verbatim to Renode |
125
+ | `hector run --gather-execution-metrics` | Enable Renode's CPU execution profiler for all machines |
126
+ | `hector run --gather-execution-metrics boardA` | Enable profiler for one specific machine |
127
+ | `hector run --gather-execution-metrics boardA --gather-execution-metrics boardB` | Enable profiler for selected machines |
128
+ | `hector run --renode-version 1.16.1` | Override the Renode version from `.hector.yaml` |
129
+ | `hector run --renode-dir /cache/renode` | Use/cache the Renode source checkout at this path (only fetched when needed) |
130
+ | `hector run --renode-integration-dir /cache/integration` | Likewise for the verilator-integration checkout |
131
+ | `hector run --no-docker` | Use locally installed `renode` / `renode-test` instead of Docker |
132
+ | `hector run --workspace-mount /mnt` | Override the container path the project root is mounted at |
133
+ | `hector run --snapshot path/to/snapshot.save` | Load a snapshot instead of booting from the resc |
134
+ | `hector run --snapshot snap.save --renode-version 1.16.1` | Load a snapshot without a `.hector.yaml` |
135
+ | `hector test` | Run the `tests:` section; exit 1 on any failure |
136
+ | `hector test --fail-fast` | Stop after the first failing test |
137
+ | `hector test --test-file tests/boot.robot` | Run a specific `.robot` file instead of the YAML tests |
138
+ | `hector test --test-name "Button press"` | Run only tests whose name contains the given substring |
139
+ | `hector test --test-name "boot" --test-name "uart"` | Run tests matching any of the given names |
140
+ | `hector test --live` | Stream bash output line-by-line and enable verbose keyword output for robot tests |
141
+ | `hector test --snapshot path/to/snapshot.save` | Load a snapshot at the start of every test |
142
+ | `hector test --reporters junit --reporters json` | Select which reporters emit after tests (repeatable) |
143
+ | `hector test --output my-results` | Write test results and artifacts to `my-results/` instead of `results/` |
144
+ | `hector test --renode-test-args '--loglevel DEBUG'` | Pass extra flags verbatim to `renode-test` |
145
+ | `hector run --job BOARD=stm32f7` | Select the matrix combination where BOARD=stm32f7 |
146
+ | `hector run --job BOARD=stm32f7,FW=release.elf` | Select the exact combination (pin every matrix variable) |
147
+ | `hector export` | Generate the files and print the run command instead of executing it |
148
+ | `hector export --no-docker` | Print the bare `renode <resc>` command instead of the `docker run …` one |
149
+ | `hector validate` | Validate `.hector.yaml` without running anything |
150
+ | `hector init` | Scaffold a starter `.hector.yaml` |
151
+ | `hector --version` | Print the tool version |
152
+
153
+ `hector` is structured as subcommands: `run` (simulate), `test` (run the `tests:`
154
+ section), `export` (emit the command without running it), plus `init` and
155
+ `validate`. Run `hector <command> --help` for the flags each one accepts.
156
+
157
+ ### Debug mode (`--debug NODE:PORT`)
158
+
159
+ Repeat the flag for each machine you want to debug. ([Renode docs: GDB debugging](https://renode.readthedocs.io/en/latest/debugging/gdb.html))
160
+
161
+ - Adds `machine StartGdbServer <port>` to the generated resc while that machine is active.
162
+ - Adds `cpu IsHalted true` to freeze the CPU until a debugger attaches.
163
+ - Exposes the port via `-p PORT:PORT` in Docker.
164
+
165
+ All other machines boot and run normally.
166
+
167
+ `--debug` also works alongside `--snapshot`: the loaded snapshot opens the same GDB
168
+ server(s) and halts the debugged CPU(s) before resuming, so you can attach to a
169
+ restored state. (A GDB server is a live connection, never part of saved snapshot
170
+ state, so it is always (re)started on load.)
171
+
172
+ Connect with:
173
+ ```bash
174
+ arm-none-eabi-gdb firmware.elf
175
+ (gdb) target extended-remote localhost:3333
176
+ (gdb) break main
177
+ (gdb) continue
178
+ ```
179
+
180
+ Or use VS Code with the Cortex-Debug extension pointed at `localhost:3333`.
181
+
182
+ ### Test mode (`hector test`)
183
+
184
+ Runs every step in the `tests:` section in declaration order. All steps run to completion even if one fails. The process exits with code 1 if any step failed, making it usable directly in CI.
185
+
186
+ Add `--fail-fast` to stop after the first failing job when running a matrix.
187
+
188
+ #### Filtering tests by name (`--test-name`)
189
+
190
+ Run only the tests whose name contains the given substring. The flag is repeatable — any match runs:
191
+
192
+ ```bash
193
+ # run one test
194
+ hector test --test-name "Button press"
195
+
196
+ # run two tests by name fragment
197
+ hector test --test-name "boot" --test-name "uart"
198
+ ```
199
+
200
+ Matching is case-sensitive substring search against the `name:` field in the YAML (or the basename of `--test-file`).
201
+
202
+ #### Streaming output live (`--live`)
203
+
204
+ By default bash test output is captured and printed as a block after the step finishes, and robot tests show only a per-test summary line. With `--live`:
205
+
206
+ - **bash steps** — each output line is printed as it is written by the script.
207
+ - **robot steps** — `renode-test` runs with `--verbose`, printing every keyword call and its timing as it executes.
208
+
209
+ ```bash
210
+ hector test --live
211
+
212
+ # combine with --test-name to focus on one test with full output
213
+ hector test --test-name "Button press" --live
214
+ ```
215
+
216
+ Both flags are independent and can be used together or separately.
217
+
218
+ #### Loading a snapshot (`--snapshot`)
219
+
220
+ Load a previously saved Renode snapshot instead of booting from the generated resc. The snapshot restores the complete emulation state (all machines, memory, peripheral registers) and resumes execution.
221
+
222
+ ```bash
223
+ # simulation mode: drop into the monitor with state restored
224
+ hector run --snapshot results/step_1_Linux_boots/snapshot.save
225
+
226
+ # test mode: every test starts from the snapshot instead of booting
227
+ hector test --snapshot results/step_1_Linux_boots/snapshot.save
228
+ ```
229
+
230
+ When used with `--renode-version`, no `.hector.yaml` is needed at all — useful for inspecting a snapshot outside of any project:
231
+
232
+ ```bash
233
+ hector run --snapshot /path/to/snapshot.save --renode-version 1.16.1
234
+ ```
235
+
236
+ The CLI flag overrides any per-test `snapshot:` field in the YAML (see [tests](#tests)).
237
+
238
+ **Saving a snapshot** from the interactive Renode monitor:
239
+
240
+ ```
241
+ Save @/home/user/projects/myboard/.hector/snapshots/post_boot.save
242
+ ```
243
+
244
+ Use the actual host path to your project directory (the same path you see on the host — the container mounts it at the identical location). Create the directory first.
245
+
246
+ #### Running a specific `.robot` file (`--test-file`)
247
+
248
+ Pass any `.robot` file directly instead of the YAML-defined tests. Hector generates the resc from your YAML config, then runs the file against it. The generated resc path is injected as `${RESC}` so the file can load the emulation:
249
+
250
+ ```bash
251
+ hector test --test-file tests/boot.robot
252
+ ```
253
+
254
+ `tests/boot.robot` must include the resc itself:
255
+ ```robot
256
+ *** Settings ***
257
+ Library Collections
258
+ Resource ${RENODEKEYWORDS}
259
+ Suite Teardown Reset Emulation
260
+
261
+ *** Test Cases ***
262
+ Boot sequence
263
+ Execute Command include @${RESC}
264
+ Create Terminal Tester sysbus.usart2 machine=boardA
265
+ Wait For Line On Uart Board initialized timeout=30
266
+ ```
267
+
268
+ #### Changing the output directory (`--output`)
269
+
270
+ By default Hector writes test results, Robot Framework XML/HTML reports, and JUnit XML to `results/`. Pass `--output` to redirect everything to a different path:
271
+
272
+ ```bash
273
+ hector test --output ci-results
274
+ hector test --output /tmp/run-42
275
+ ```
276
+
277
+ In simulation mode the flag controls the directory pre-created for Renode to write file-mapped outputs (e.g. UART logs from `file:results/uart.log`). Note that your YAML `file:` paths should match whatever directory you choose here.
278
+
279
+ #### Test reporters (`--reporters`)
280
+
281
+ After all steps complete, Hector calls one or more reporters with the full result list. Two are built in:
282
+
283
+ - **`junit`** (default) — writes `<output>/junit.xml`, compatible with GitLab CI, GitHub Actions, and Jenkins.
284
+ - **`json`** — writes `<output>/manifest.json`, a machine-readable index for dashboards / CI: one entry per test with `status`, `duration`, `commands`, and relative paths to its detail artifacts (`report_html`, plus `log_html` / `robot_xml` for robot tests).
285
+
286
+ The flag is repeatable (repeat it once per reporter):
287
+
288
+ ```bash
289
+ hector test # default: junit
290
+ hector test --reporters junit --reporters json # also emit the dashboard manifest
291
+ ```
292
+
293
+ To add a custom reporter, register it in Python before invoking the pipeline:
294
+
295
+ ```python
296
+ from hector.reporters import REPORTERS
297
+
298
+ @REPORTERS.register("csv")
299
+ def csv_reporter(results, output_dir):
300
+ import csv, os
301
+ with open(os.path.join(output_dir, "results.csv"), "w") as f:
302
+ w = csv.DictWriter(f, fieldnames=["name", "type", "passed", "duration"])
303
+ w.writeheader()
304
+ for r in results:
305
+ w.writerow({"name": r.name, "type": r.type,
306
+ "passed": r.passed, "duration": r.duration})
307
+ ```
308
+
309
+ Then pass its name with `--reporters csv`. The flag is repeatable, so combine reporters by repeating it: `--reporters junit --reporters csv`.
310
+
311
+ #### Passing extra flags to `renode-test` (`--renode-test-args`)
312
+
313
+ Forwards additional flags verbatim to the `renode-test` invocation for every test step. Accepts any flag that Robot Framework accepts:
314
+
315
+ ```bash
316
+ # Enable verbose logging for debugging
317
+ hector test --renode-test-args '--loglevel DEBUG'
318
+
319
+ # Inject a variable into every robot step
320
+ hector test --renode-test-args '--variable FOO:bar'
321
+
322
+ # Run only tests tagged "smoke"
323
+ hector test --renode-test-args '--include smoke'
324
+ ```
325
+
326
+ These are appended after any per-step `args:` keys defined in the YAML, so per-step args take precedence.
327
+
328
+ ### Simulation mode
329
+
330
+ Launches one Renode emulation per job in the Docker container. Each machine declared in the YAML becomes a named Renode machine in that emulation. The Renode monitor is interactive; type `help` at the prompt for available commands.
331
+
332
+ #### Gathering execution metrics (`--gather-execution-metrics`)
333
+
334
+ Enables Renode's built-in CPU execution profiler. Without a machine name, enables it for all machines. Repeatable to target specific machines:
335
+
336
+ ```bash
337
+ # all machines
338
+ hector run --gather-execution-metrics
339
+
340
+ # specific machines only
341
+ hector run --gather-execution-metrics boardA
342
+ hector run --gather-execution-metrics boardA --gather-execution-metrics boardB
343
+
344
+ # combine with --output
345
+ hector run --gather-execution-metrics --output ci-results
346
+ ```
347
+
348
+ For each targeted machine, Hector injects `cpu EnableProfiler @<path>` into the generated resc. Profiler output is written to the output directory, one binary file per machine per job:
349
+
350
+ ```
351
+ results/
352
+ ├── metrics_boardA_job_1.bin
353
+ └── metrics_boardB_job_1.bin
354
+ ```
355
+
356
+ Works in both simulation and test mode. In test mode each test step re-runs the resc, so the metrics file for each machine reflects only the most recent test step's execution.
357
+
358
+ Analyse the output with Renode's [ExecutionMetricsAnalyzer](https://renode.readthedocs.io/en/latest/advanced/execution-metrics.html).
359
+
360
+ #### Passing extra flags to Renode (`--renode-args`)
361
+
362
+ Forwards additional flags verbatim to the `renode` process:
363
+
364
+ ```bash
365
+ hector run --renode-args '--console'
366
+ hector run --renode-args '--hide-log --config my.conf'
367
+ ```
368
+
369
+ #### Running without Docker (`--no-docker`)
370
+
371
+ Call the locally installed `renode` and `renode-test` binaries directly instead of launching a Docker container. `build:` steps and `shell` tests also run on the host (their `image:` is ignored, with a warning), as do module builds.
372
+
373
+ ```bash
374
+ hector run --no-docker
375
+ hector test --no-docker
376
+ ```
377
+
378
+ Requires `renode` and `renode-test` to be on your `PATH` — and, since images are ignored, whatever toolchains your `build:`/`shell` scripts call (e.g. a compiler). **`--no-docker` and `--workspace-mount` are mutually exclusive** — they both control how the project root is made accessible and cannot be combined.
379
+
380
+ #### Overriding the Renode version (`--renode-version`)
381
+
382
+ Override the `renode_version:` from `.hector.yaml` without editing the file:
383
+
384
+ ```bash
385
+ hector run --renode-version 1.15.0
386
+ hector test --renode-version 1.15.0
387
+ ```
388
+
389
+ When combined with `--snapshot` in simulation mode, `.hector.yaml` is not required at all — the two flags together are self-contained:
390
+
391
+ ```bash
392
+ hector run --snapshot path/to/snapshot.save --renode-version 1.16.1
393
+ ```
394
+
395
+ #### Renode source checkout
396
+
397
+ Running a simulation does **not** need the Renode source — the binary and its bundled platforms come from the `antmicro/renode` Docker image. Hector clones the Renode source (and the verilator-integration repo) into `.hector/` **only when a config actually needs it**:
398
+
399
+ - it builds `modules:` (Verilated / C# co-simulated peripherals, which compile against the Renode source), or
400
+ - it interpolates `${RENODE_DIR}` / `${INTEGRATION_DIR}`.
401
+
402
+ For everything else — plain or YAML-defined machines, firmware URLs, `build:`/`shell` steps — nothing is cloned, so runs start immediately.
403
+
404
+ When the source *is* needed, point hector at a cached or shared checkout instead of re-cloning each time:
405
+
406
+ ```bash
407
+ hector test --renode-dir /cache/renode --renode-integration-dir /cache/renode-integration
408
+ ```
409
+
410
+ An existing directory is reused as-is; a missing one is cloned into. The location may live **outside the project** (e.g. a persistent CI cache) — hector bind-mounts an out-of-tree checkout into every container 1:1 (at the same path), so `modules:` builds and `${RENODE_DIR}` references still resolve. A path under the project works too (it's covered by the normal workspace mount).
411
+
412
+ #### Overriding the workspace mount path (`--workspace-mount`)
413
+
414
+ By default the project root is bind-mounted into the container at the **same absolute path** as on the host (e.g. `/home/user/projects/myboard`), so paths look identical inside and outside the container. Override this only if the host path is unavailable at that location inside the image:
415
+
416
+ ```bash
417
+ hector run --workspace-mount /workspace
418
+ ```
419
+
420
+ **Cannot be combined with `--no-docker`** — if you are running locally there is no container mount to configure.
421
+
422
+ ### Selecting a matrix combination (`--job`)
423
+
424
+ **hector runs exactly one matrix combination per invocation — it does not expand the matrix itself.** Iterating combinations (and running them in parallel) is the CI's job: it reads the `matrix:`, then calls hector once per combination.
425
+
426
+ - **No matrix:** a single invocation runs the one (empty) job — no `--job` needed.
427
+ - **Matrix defined:** you must pin it to one combination with `--job KEY=VALUE`, repeating keys with commas to constrain every variable:
428
+
429
+ ```bash
430
+ hector test --job BOARD=stm32f7 # one variable pins it
431
+ hector test --job BOARD=stm32f7,FW=release.elf # pin every variable
432
+ ```
433
+
434
+ If `--job` is missing (or matches more than one combination), hector lists the combinations and exits without running:
435
+
436
+ ```
437
+ [ERROR] The matrix produces 3 combinations; a run targets one. Select it with
438
+ --job KEY=VALUE (e.g. --job BOARD=stm32f4).
439
+ Combinations:
440
+ BOARD=stm32f4 FW=debug.elf
441
+ BOARD=stm32f7 FW=debug.elf
442
+ BOARD=stm32f4 FW=release.elf
443
+ ```
444
+
445
+ Values match as strings, so `--job RATE=2` selects a numeric `2` in the matrix.
446
+ ### Export mode (`hector export`)
447
+
448
+ Builds everything a run would — the per-machine `.repl` files and the emulation
449
+ `.resc` — but, instead of launching, prints the exact command that *would* run it:
450
+
451
+ ```bash
452
+ # the docker invocation hector would execute
453
+ hector export
454
+ # → docker run -it --rm … antmicro/renode:1.16.1 renode .hector/resc/job_1.resc
455
+
456
+ # with --no-docker, the bare local command instead
457
+ hector export --no-docker
458
+ # → renode .hector/resc/job_1.resc
459
+ ```
460
+
461
+ Useful for inspecting or wrapping the command (custom Docker flags, a different
462
+ runner, CI plumbing). With a matrix, one command per job is printed. The same
463
+ build flags as `run` apply (`--set`, `--debug`, `--snapshot`, `--renode-args`, …).
464
+
465
+ ### Validate mode (`hector validate`)
466
+
467
+ ```bash
468
+ hector validate
469
+ ```
470
+
471
+ Parses and validates `.hector.yaml` without touching Docker. All errors are collected and reported at once:
472
+
473
+ ```
474
+ [VALIDATE] Checking .hector.yaml ...
475
+ ERROR modules.uart0.type: Required: the Renode class this module exposes.
476
+ ERROR hubs.mylink.type: Unknown hub type 'spi'. Available: ble, can, ...
477
+ WARN machines.boardA.peripherals.btn: Unknown machine key 'tyep'.
478
+ [VALIDATE] FAILED — 2 error(s), 1 warning(s).
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Configuration reference
484
+
485
+ The configuration lives in `.hector.yaml`.
486
+
487
+ ### Top-level fields
488
+
489
+ | Field | Required | Description |
490
+ |---|---|---|
491
+ | `version` | No | Schema version (currently `"0.1"`). Warns if unrecognised. |
492
+ | `renode_version` | Yes | Renode version, no leading `v` (e.g. `1.16.1`). Selects both the git tag for cloning and the Docker image tag. |
493
+ | `arguments` | No | Scalar defaults, overridable by env var or `--set`. |
494
+ | `build` | No | Pre-sim shell steps run once per job in containers (compile firmware, fetch/generate files, …). Interpolated with the job's arguments/matrix variables. |
495
+ | `matrix` | No | Cross-product job expansion. |
496
+ | `modules` | No | Verilated / C# peripheral type definitions. |
497
+ | `hubs` | No | Emulation-level connection objects (uart, can, ethernet, gpio, usb, wireless, ble, wisun). |
498
+ | `machines` | No | The machines to simulate. Optional: a config may be build-only or run sim-independent (`requires_sim: false`) shell tests. `run`/`export` and sim-backed tests require at least one. |
499
+ | `connections` | No | Global signal wiring. |
500
+ | `mappings` | No | Emulated peripheral → host resource bindings. |
501
+ | `tests` | No | Test steps run by `hector test`. |
502
+ | `quantum` | No | Override the global time quantum (seconds). |
503
+ | `ci` | No | CI pipeline definitions: when and how a CI server runs this config. |
504
+
505
+ ---
506
+
507
+ ### arguments
508
+
509
+ Scalar parameters with defaults. Usable anywhere in the YAML as `${NAME}`. Matrix variables take precedence over arguments when names collide.
510
+
511
+ ```yaml
512
+ arguments:
513
+ BOARD: stm32f4_discovery
514
+ FW: firmware.elf
515
+ ```
516
+
517
+ Override at runtime — three ways, in increasing precedence order:
518
+
519
+ ```bash
520
+ # 1. config default (declared above)
521
+
522
+ # 2. environment variable — same name as the argument key
523
+ BOARD=stm32f7_discovery hector run
524
+
525
+ # 3. --set flag — highest precedence, beats env vars
526
+ hector run --set BOARD=stm32f7_discovery
527
+ hector run --set BOARD=stm32f7_discovery --set FW=release.elf
528
+ ```
529
+
530
+ `--set` can also introduce keys not declared in `arguments:` — they are merged into the interpolation context and can be referenced in the YAML as `${KEY}`.
531
+
532
+ ---
533
+
534
+ ### matrix
535
+
536
+ Declares the cross-product of values that make up the build/test space. Each combination is one complete simulation run. **hector does not run the whole matrix** — a single invocation runs one combination, selected with [`--job`](#selecting-a-matrix-combination---job); iterating the matrix is the CI's job. The `matrix:` block is the source of truth the CI reads to enumerate combinations.
537
+
538
+ ```yaml
539
+ matrix:
540
+ variables:
541
+ BOARD: ["stm32f4_discovery", "stm32f7_discovery"]
542
+ FW: ["debug.elf", "release.elf"]
543
+ ```
544
+
545
+ This declares four combinations (run one with e.g. `--job BOARD=stm32f4_discovery,FW=debug.elf`). An `exclude` block can remove specific combinations:
546
+
547
+ ```yaml
548
+ matrix:
549
+ variables:
550
+ BOARD: ["stm32f4_discovery", "stm32f7_discovery"]
551
+ FW: ["debug.elf", "release.elf"]
552
+ exclude:
553
+ - BOARD: stm32f7_discovery
554
+ FW: debug.elf
555
+ ```
556
+
557
+ A 1×1 matrix is a convenient way to keep the field present in the YAML while running only one job:
558
+
559
+ ```yaml
560
+ matrix:
561
+ variables:
562
+ VARIANT: ["default"]
563
+ ```
564
+
565
+ ---
566
+
567
+ ### modules
568
+
569
+ Defines reusable peripheral **types**. A module is built once per job, producing a loadable artifact (`.so` for Verilator, `.dll` for C#). Modules are referenced by name from a machine's `peripherals:` section.
570
+
571
+ #### kind: renode-verilator
572
+
573
+ Builds a verilated co-simulation peripheral into a `.so` inside the Renode Docker container (guaranteeing ABI compatibility). Loaded by Renode via its co-simulation interface. ([Renode docs: co-simulation with HDL](https://renode.readthedocs.io/en/latest/advanced/co-simulating-with-an-hdl-simulator.html))
574
+
575
+ ```yaml
576
+ modules:
577
+ uartlite:
578
+ kind: renode-verilator # default if omitted
579
+ type: CoSimulated.CoSimulatedUART
580
+ source: "${INTEGRATION_DIR}/samples/uartlite"
581
+ cmake_flags: "" # optional extra CMake arguments
582
+ ```
583
+
584
+ | Field | Required | Description |
585
+ |---|---|---|
586
+ | `kind` | No (default: `renode-verilator`) | `renode-verilator` or `csharp`. |
587
+ | `type` | Yes | Fully qualified Renode class name the artifact exposes. |
588
+ | `source` | Yes | Path to the CMake project root. `${INTEGRATION_DIR}` and `${RENODE_DIR}` are available. |
589
+ | `cmake_flags` | No | Extra flags appended to the `cmake` invocation. |
590
+
591
+ `${INTEGRATION_DIR}` resolves to the managed clone of [renode-verilator-integration](https://github.com/antmicro/renode-verilator-integration) inside `.hector/`. Its `samples/` directory contains ready-to-build examples.
592
+
593
+ **Multiple instances** of one module are declared as separate `peripherals:` entries. The framework copies the built `.so` per instance so each gets independent simulation state.
594
+
595
+ #### kind: csharp
596
+
597
+ Builds (or locates) a C# Renode peripheral plugin. The resulting `.dll` is loaded at emulation scope via `i @<path>` before any machine block, making the type available to all machines in the job. ([Renode docs: writing peripherals](https://renode.readthedocs.io/en/latest/advanced/writing-peripherals.html))
598
+
599
+ Two usage modes:
600
+
601
+ **Pre-built DLL** — point `source` directly at the `.dll`:
602
+
603
+ ```yaml
604
+ modules:
605
+ my_periph:
606
+ kind: csharp
607
+ type: My.Namespace.MyPeripheral
608
+ source: prebuilt/my_periph.dll
609
+ ```
610
+
611
+ **Build from source** — point `source` at a directory containing a `.csproj`. `dotnet build` runs inside the Renode container (which ships with Mono/dotnet):
612
+
613
+ ```yaml
614
+ modules:
615
+ my_periph:
616
+ kind: csharp
617
+ type: My.Namespace.MyPeripheral
618
+ source: hw/cs/MyPeripheral
619
+ ```
620
+
621
+ | Field | Required | Description |
622
+ |---|---|---|
623
+ | `kind` | Yes | Must be `csharp`. |
624
+ | `type` | Yes | Fully qualified C# class name as it will appear in the generated `.repl`. |
625
+ | `source` | Yes | Path to a `.csproj` directory **or** a pre-built `.dll` file. |
626
+
627
+ The `type` field is what you would write in a `.repl` entry — it must match the class exposed by the compiled DLL. Multiple peripherals in the same job can share one DLL (different `type` values, same `source`); Hector emits a single `i @<path>` import even when multiple instances reference the same DLL.
628
+
629
+ ---
630
+
631
+ ### hubs
632
+
633
+ Emulation-level connection objects instantiated at the Renode emulation scope, spanning all machines. Declared here and referenced from `connections:`.
634
+
635
+ ```yaml
636
+ hubs:
637
+ uartlink: { type: uart }
638
+ canbus: { type: can }
639
+ lan: { type: ethernet }
640
+ irqline: { type: gpio }
641
+ usblink: { type: usb }
642
+ radio: { type: wireless }
643
+ ble_net: { type: ble }
644
+ mesh: { type: wisun }
645
+ ```
646
+
647
+ | Type | Renode object | Operator | Description |
648
+ |---|---|---|---|
649
+ | `uart` | `CreateUARTHub` | `<->` | Symmetric UART medium; N machines can connect |
650
+ | `can` | `CreateCANHub` | `<->` | Symmetric CAN bus |
651
+ | `ethernet` | [`CreateSwitch`](https://renode.readthedocs.io/en/latest/networking/wired.html) | `<->` | Ethernet switch |
652
+ | `gpio` | `CreateGPIOConnector` | `->` | Directional GPIO link; one source and one destination |
653
+ | `usb` | [`CreateUSBConnector`](https://renode.readthedocs.io/en/latest/tutorials/usbip.html) | `->` | Asymmetric USB; device side → hub → controller side |
654
+ | `wireless` | [`CreateIEEE802_15_4Medium`](https://renode.readthedocs.io/en/latest/networking/wireless.html) | `<->` | IEEE 802.15.4 mesh (ZigBee, Thread, Matter) |
655
+ | `ble` | [`CreateBLEMedium`](https://renode.readthedocs.io/en/latest/networking/wireless.html) | `<->` | Bluetooth Low Energy |
656
+ | `wisun` | `CreateWiSUNMedium` | `<->` | Wi-SUN IEEE 802.11ah mesh (smart grid IoT) |
657
+
658
+ When any hub is present, Hector automatically sets `emulation SetGlobalQuantum` to 10 µs to ensure cross-machine communication is deterministic. Override with the top-level `quantum:` field.
659
+
660
+ #### USB connections
661
+
662
+ USB is asymmetric — the arrow direction picks the role. Both forms are equivalent:
663
+
664
+ ```yaml
665
+ connections: |
666
+ # chained one-liner — device -> hub -> controller:
667
+ mcu.usb -> usblink -> host.usb
668
+
669
+ # …or the two endpoint→hub lines it expands to:
670
+ mcu.usb -> usblink # mcu is the USB DEVICE (periph → hub)
671
+ usblink -> host.usb # host is the USB CONTROLLER (hub → periph)
672
+ ```
673
+
674
+ Each `usb` hub is a **1-to-1 connector** and can carry only one device. The USB host controller, however, supports multiple devices — each on its own address. To attach multiple devices to the same host, declare one hub per device and point all of them at the same host peripheral:
675
+
676
+ ```yaml
677
+ hubs:
678
+ usb_kbd: { type: usb }
679
+ usb_storage: { type: usb }
680
+
681
+ connections: |
682
+ board.usb_keyboard -> usb_kbd
683
+ usb_kbd -> board.usb_host
684
+ board.usb_msc -> usb_storage
685
+ usb_storage -> board.usb_host
686
+ ```
687
+
688
+ Hector generates a `connector Connect` for the device side and a `RegisterInController` for the host side of each hub. The host enumerates devices sequentially and assigns each a unique USB address.
689
+
690
+ #### Wireless positioning and range models
691
+
692
+ Both `wireless` and `ble` (and `wisun`) support optional per-machine positioning and packet delivery models ([Renode docs: wireless networking](https://renode.readthedocs.io/en/latest/networking/wireless.html)). Add raw Renode monitor commands in a machine's `commands:` block:
693
+
694
+ ```yaml
695
+ machines:
696
+ nodeA:
697
+ commands: |
698
+ wireless SetPosition sysbus.radio 0.0 0.0 0.0
699
+ wireless SetMediumFunction RangeWirelessFunction 10.0
700
+ ```
701
+
702
+ Available medium functions: `SimpleWirelessFunction` (default, all delivered), `RangeWirelessFunction [maxRange]`, `RangeLossWirelessFunction [lossRange txRatio rxRatio]`.
703
+
704
+ ---
705
+
706
+ ### machines
707
+
708
+ Each entry under `machines:` becomes a named machine in the Renode emulation.
709
+
710
+ ```yaml
711
+ machines:
712
+ sensor_mcu:
713
+ backend: renode # optional; 'renode' is the only supported backend
714
+ platform:
715
+ - platforms/boards/${BOARD}.repl
716
+ firmware: ${FW}
717
+ peripherals: { ... }
718
+ connections: |
719
+ ...
720
+ mappings: |
721
+ ...
722
+ commands: |
723
+ ...
724
+ ```
725
+
726
+ | Field | Description |
727
+ |---|---|
728
+ | `backend` | Must be `renode` (default). |
729
+ | `platform` | List of [`.repl` platform description](https://renode.readthedocs.io/en/latest/advanced/platform_description_format.html) files loaded in order. |
730
+ | `firmware` | ELF file loaded with [`sysbus LoadELF`](https://renode.readthedocs.io/en/latest/basic/machines.html). Omit or set to `none` to skip. |
731
+ | `peripherals` | Peripheral instances composed onto this machine. See below. |
732
+ | `connections` | Signal wiring scoped to this machine. Covers GPIO, IRQ, and bracket-range connections. Bare peripheral names are accepted. |
733
+ | `mappings` | Per-machine host resource bindings. Same syntax as global `mappings:`. |
734
+ | `commands` | Raw [Renode monitor](https://renode.readthedocs.io/en/latest/basic/monitor-syntax.html) commands inserted into the resc after hardware loads. |
735
+
736
+ > **Note:** `artifacts` is now a **top-level** key, not a per-machine one. See [artifacts](#artifacts).
737
+
738
+ #### peripherals
739
+
740
+ Each entry declares one peripheral instance on this machine. The `type` field is either a built-in Renode class name, a `renode-verilator` module name, or a `csharp` module name. Properties are written directly as flat keys alongside `type` and `at`.
741
+
742
+ ```yaml
743
+ peripherals:
744
+ uart0: # instance name
745
+ type: uartlite # module name OR built-in Renode class
746
+ at: "sysbus <0x70000000, +0x100>"
747
+ frequency: 100000000 # peripheral property — passed through to the .repl
748
+
749
+ extra_ram:
750
+ type: Memory.MappedMemory
751
+ at: "sysbus <0x90000000, +0x10000>"
752
+ size: 0x10000
753
+
754
+ Btn:
755
+ type: Miscellaneous.Button
756
+ at: gpioPortC 13
757
+ invert: true
758
+
759
+ custom_accel:
760
+ type: my_accel # references a csharp module
761
+ at: "sysbus <0x40010000, +0x100>"
762
+
763
+ nvic:
764
+ type: IRQControllers.NVIC
765
+ at: sysbus 0xE000E000
766
+ priorityMask: 0xF0
767
+ init: |
768
+ someNvicSetupCommand
769
+ ```
770
+
771
+ The `at` field is passed through as-is:
772
+
773
+ | Form | Example | Used for |
774
+ |---|---|---|
775
+ | Bus range | `sysbus <0x70000000, +0x100>` | Memory-mapped peripherals |
776
+ | GPIO port + index | `gpioPortC 13` | Button, LED registered at a pin slot |
777
+ | Named object | `sysbus` | CPU, NVIC, and other sysbus-attached peripherals |
778
+ | `none` | `none` | Peripherals with no bus registration (e.g. `CombinedInput`) |
779
+
780
+ The optional `init:` block contains Renode monitor commands that run when the peripheral is loaded during platform initialisation (same as `init:` in a native `.repl` file).
781
+
782
+ #### connections (per-machine)
783
+
784
+ All signal wiring for a machine lives here. Peripheral names are automatically scoped to the parent machine; hub names and already-qualified `machine.x` tokens are left unchanged. Three forms are supported:
785
+
786
+ **Simple GPIO / IRQ** — connects a peripheral output to a GPIO input or NVIC line:
787
+ ```yaml
788
+ connections: |
789
+ Btn -> gpioPortC@13 # Button IRQ → GPIO port C pin 13
790
+ usart2 -> nvic@38 # UART interrupt → NVIC line 38
791
+ usart2 <-> uartlink # cross-machine hub
792
+ gpioPortA@7 -> irqline # GPIO pin → cross-machine GPIO hub
793
+ ```
794
+
795
+ **Signal name** — `periph.Signal` notation for named IRQ outputs:
796
+ ```yaml
797
+ connections: |
798
+ nvic.IRQ -> cpu@0 # NVIC IRQ output → CPU interrupt input
799
+ ```
800
+
801
+ **Bracket range** — compact multi-pin wiring (Renode `.repl` range syntax, passed through as-is):
802
+ ```yaml
803
+ connections: |
804
+ gpioPortC[0-15] -> exti@[0-15] # all 16 GPIO pins → EXTI lines
805
+ exti[0-4] -> nvic@[6-10] # EXTI lines 0-4 → NVIC inputs 6-10
806
+ ```
807
+
808
+ All three forms can be mixed freely in the same block.
809
+
810
+ **Peripheral-scoped wiring** — a `connections:` block may also be placed *on a peripheral* (including a nested grouping peripheral), to keep wiring next to the part it concerns. These lines are merged into the machine's wiring at build time — peripheral names are flat in the generated `.repl`, so scoping is purely organizational:
811
+
812
+ ```yaml
813
+ machines:
814
+ blackpill:
815
+ peripherals:
816
+ mcu: # a grouping peripheral
817
+ type: ...
818
+ peripherals: { cpu: {...}, nvic: {...}, ... }
819
+ connections: | # wiring scoped to the mcu
820
+ nvic.IRQ -> cpu@0
821
+ button:
822
+ type: Miscellaneous.Button
823
+ at: gpioPortC 13
824
+ connections: | # machine-level wiring
825
+ button -> gpioPortC@13
826
+ ```
827
+
828
+ Peripheral-scoped connections are always intra-machine; cross-machine hub links must use the machine-level `connections:` block.
829
+
830
+ #### mappings
831
+
832
+ Binds an emulated peripheral to a host resource. Same syntax as the global `mappings:` section.
833
+
834
+ ```yaml
835
+ mappings: |
836
+ uart4 -> file:results/uart_output.log
837
+ eth0 -> tap:tap0
838
+ ```
839
+
840
+ #### artifacts
841
+
842
+ A **top-level** (global) list of glob patterns. After each job completes, every
843
+ matching file is copied into `<output>/artifacts/job_<N>/…` (mirroring its source path,
844
+ so same-named files in different directories don't clash). This gives CI a single
845
+ directory to upload and a dashboard one place to look.
846
+
847
+ Two pattern styles are supported:
848
+
849
+ - **Bare filename** (no `/`, e.g. `*.xml`, `metrics_*.bin`) — searched **recursively**
850
+ across the whole project, skipping hidden dirs (the bundled Renode clone in `.hector/`,
851
+ `.git/`, …) and the artifacts output dir. Use this to grab outputs wherever they land.
852
+ - **Path pattern** (`results/*.log`, `logs/**/*.vcd`) — taken literally; `**` matches any
853
+ depth, a single `*` matches one level.
854
+
855
+ ```yaml
856
+ artifacts:
857
+ - "*.xml" # every .xml anywhere (junit.xml, robot_output.xml, …)
858
+ - results/*.log # .log files directly under results/
859
+ - logs/**/*.vcd # .vcd files at any depth under logs/
860
+ ```
861
+
862
+ Override the YAML list on the command line with `--artifacts` (repeatable); when given,
863
+ it replaces the config value — same precedence as `--renode-version`:
864
+
865
+ ```bash
866
+ hector test --artifacts 'results/*.bin' --artifacts 'logs/**/*.log'
867
+ ```
868
+
869
+ ---
870
+
871
+ ### connections (global)
872
+
873
+ The top-level `connections:` block is the natural home for cross-board wiring.
874
+
875
+ ```yaml
876
+ connections: |
877
+ boardA.usart2 <-> uartlink <-> boardB.usart2
878
+ boardA.gpioPortA@7 -> irqline -> boardB.gpioPortB@4
879
+ ```
880
+
881
+ See [Connection syntax](#connection-syntax) for the full grammar.
882
+
883
+ ---
884
+
885
+ ### mappings (global)
886
+
887
+ Binds emulated peripherals to host resources. Each line is `[machine.]peripheral -> backend:param`.
888
+
889
+ ```yaml
890
+ mappings: |
891
+ boardA.uart4 -> file:results/uart.log
892
+ boardA.usart2 -> tcp:4567
893
+ boardA.uart1 -> pty:/workspace/ptys/uart1
894
+ boardB.eth0 -> tap:tap0
895
+ ```
896
+
897
+ #### backend: file
898
+
899
+ Redirect UART output to a file using Renode's `CreateFileBackend` ([Renode docs: UART integration](https://renode.readthedocs.io/en/latest/host-integration/uart.html)). Subsequent runs append with a numeric suffix rather than overwriting.
900
+
901
+ ```yaml
902
+ boardA.uart4 -> file:results/uart_output.log
903
+ ```
904
+
905
+ The file path is relative to the project root. The directory must exist before the simulation starts.
906
+
907
+ #### backend: tcp
908
+
909
+ Expose a UART as a TCP socket terminal. With `--net=host` (the default Docker mode) the port is directly accessible from the host.
910
+
911
+ ```yaml
912
+ boardA.uart4 -> tcp:4567
913
+ ```
914
+
915
+ Connect with: `nc localhost 4567` or `telnet localhost 4567`.
916
+
917
+ Append `:raw` to suppress IAC telnet negotiation bytes (useful when connecting a tool like `picocom` via `nc`):
918
+
919
+ ```yaml
920
+ boardA.uart4 -> tcp:4567:raw
921
+ ```
922
+
923
+ #### backend: pty
924
+
925
+ Expose a UART as a PTY device (Linux/macOS only). Useful for connecting tools that expect a serial device path.
926
+
927
+ ```yaml
928
+ boardA.uart4 -> pty:/workspace/ptys/uart4
929
+ ```
930
+
931
+ **Note:** PTY devices are created inside the Docker container and are not visible on the host filesystem by default. For host-accessible serial ports, prefer `tcp`. To use PTY from the host, run the container with `--privileged` and bind-mount `/dev/pts`.
932
+
933
+ #### backend: tap
934
+
935
+ Connect an Ethernet peripheral to a Linux TAP network interface, bridging the emulated network to the host.
936
+
937
+ ```yaml
938
+ boardA.eth0 -> tap:tap0
939
+ ```
940
+
941
+ If the interface name is omitted, `tap0` is used. Requires `NET_ADMIN` capability in the container; with `--net=host` this is typically available automatically.
942
+
943
+ ---
944
+
945
+ ### build
946
+
947
+ Pre-simulation steps that produce the inputs a run/test needs — compile firmware, fetch a binary, generate a file. Each step runs **in a container** with the project bind-mounted (the same engine as the `shell` test type), once per job, *before* the machines are built. Steps run in order and the job aborts on the first failure (the sim depends on their output).
948
+
949
+ ```yaml
950
+ build:
951
+ - name: Compile firmware
952
+ image: arm-gcc:13 # a toolchain image — no Renode involved
953
+ steps:
954
+ - script: make -C firmware
955
+
956
+ - name: Fetch bootloader # no image → the run's renode image (logged)
957
+ script: |
958
+ wget -O boot.bin https://example.com/boot.bin
959
+
960
+ machines:
961
+ mcu:
962
+ firmware: firmware/build/app.elf # the build output, read straight back
963
+ ```
964
+
965
+ Same shape as a `shell` test (`name`, optional `image`, and `steps:` or a single-step `script:`). Build steps and the sim/tests all share the one bind-mounted project directory at the same path, so **a file a build step writes is simply there for the sim and tests** — reference it by its normal path, no copying or declaration. Under `--no-docker`, build steps run on the host (image ignored).
966
+
967
+ Build steps **report like tests**: each block becomes one entry (`type: "build"`) in the JUnit/JSON reports with its own `report.html`, alongside the test results. A failing build step marks the job failed and skips the simulation/tests.
968
+
969
+ > **Migration:** `build:` replaces the old host-side `prepare:` script. A one-line
970
+ > `prepare:` becomes a one-step `build:` (no `image:` → runs in the renode image, which
971
+ > has the usual shell tools). Old `prepare:`/`setup:` keys still parse, with a rename
972
+ > warning.
973
+
974
+ > **Tip:** containers may write build outputs as `root`. If later host steps (git, rm)
975
+ > trip on ownership, that's the known cause; gitignore generated outputs like `.hector/`.
976
+
977
+ ---
978
+
979
+ ### tests
980
+
981
+ A list of steps executed sequentially when running `hector test`. All steps run even if one fails. The process exits with code 1 if any step failed.
982
+
983
+ ```yaml
984
+ tests:
985
+ - name: Firmware boots
986
+ type: robot
987
+ script: |
988
+ Create Terminal Tester sysbus.usart2
989
+ Wait For Line On Uart Board initialized timeout=30
990
+
991
+ - name: Check output
992
+ type: shell
993
+ script: |
994
+ grep -q "Board initialized" results/uart_output.log \
995
+ && echo "PASS" || { echo "FAIL"; exit 1; }
996
+ ```
997
+
998
+ There are two test types: **`robot`** (boots the emulation and drives it with Robot
999
+ Framework) and **`shell`** (runs a shell script in a container). By default every
1000
+ test runs *after* the simulation; a `shell` test that doesn't need Renode can opt out
1001
+ with `requires_sim: false` (see below).
1002
+
1003
+ #### type: robot
1004
+
1005
+ Each `robot` step can provide its keywords either inline via `script:` or as an external file via `file:`. Both are run with `renode-test` inside the Docker container. ([Renode docs: Robot Framework testing](https://renode.readthedocs.io/en/latest/introduction/testing.html))
1006
+
1007
+ ##### Inline script
1008
+
1009
+ The `script` value is the body of a Robot Framework test case. Hector wraps it in a minimal `.robot` file that auto-loads the generated resc (which starts the emulation) and resets the emulation on teardown:
1010
+
1011
+ ```yaml
1012
+ - name: UART echo
1013
+ type: robot
1014
+ script: |
1015
+ Create Terminal Tester sysbus.usart2
1016
+ Write Line To Uart ping
1017
+ Wait For Line On Uart pong timeout=10
1018
+ ```
1019
+
1020
+ ##### External file
1021
+
1022
+ Point `file:` at an existing `.robot` file on the host. The file is passed to `renode-test` as-is; Hector injects the generated resc path as the Robot variable `${RESC}` so the file can load the emulation:
1023
+
1024
+ ```yaml
1025
+ - name: Boot sequence
1026
+ type: robot
1027
+ file: tests/boot.robot
1028
+ ```
1029
+
1030
+ `tests/boot.robot`:
1031
+ ```robot
1032
+ *** Settings ***
1033
+ Library Collections
1034
+ Suite Teardown Reset Emulation
1035
+
1036
+ *** Test Cases ***
1037
+ Boot sequence
1038
+ Execute Command include @${RESC}
1039
+ Create Terminal Tester sysbus.usart2
1040
+ Wait For Line On Uart Board initialized timeout=30
1041
+ ```
1042
+
1043
+ `${RESC}` resolves to the container-absolute path of the generated `.resc` for this job. If `file:` and `script:` are both present, `file:` takes precedence.
1044
+
1045
+ ##### Loading a snapshot for one test (`snapshot`)
1046
+
1047
+ Provide a `snapshot:` path to load a saved Renode state instead of booting from the resc for that specific step. The CLI `--snapshot` flag overrides this field when present.
1048
+
1049
+ ```yaml
1050
+ - name: Verify UART after boot
1051
+ type: robot
1052
+ snapshot: .hector/snapshots/post_boot.save
1053
+ script: |
1054
+ Execute Command mach set "boardA"
1055
+ Create Terminal Tester sysbus.usart2 machine=boardA
1056
+ Write Line To Uart ping
1057
+ Wait For Line On Uart pong timeout=5
1058
+ ```
1059
+
1060
+ This is useful when only one test in a suite needs a checkpoint — other tests still boot normally. `snapshot:` is only supported for inline `script:` tests; file-based (`file:`) tests ignore it.
1061
+
1062
+ ##### Extra renode-test flags (`args`)
1063
+
1064
+ Use the optional `args` field to pass additional flags directly to the `renode-test` invocation. Accepts a list or a shell-style string:
1065
+
1066
+ ```yaml
1067
+ - name: Verbose UART test
1068
+ type: robot
1069
+ args: "--loglevel DEBUG"
1070
+ script: |
1071
+ Create Terminal Tester sysbus.usart2
1072
+ Wait For Line On Uart Ready timeout=30
1073
+
1074
+ - name: Tagged subset
1075
+ type: robot
1076
+ args:
1077
+ - --include
1078
+ - smoke
1079
+ file: tests/full_suite.robot
1080
+ ```
1081
+
1082
+ Any flag that `renode-test` (Robot Framework) accepts can be passed here — `--include`, `--exclude`, `--loglevel`, `--variable`, `--listener`, etc.
1083
+
1084
+ ##### Available Robot keywords
1085
+
1086
+ | Keyword | Description |
1087
+ |---|---|
1088
+ | `Create Terminal Tester sysbus.usart2 machine=boardA` | Set up a UART listener; `machine=` required in multi-board setups |
1089
+ | `Wait For Line On Uart <text> timeout=<n>` | Assert UART output within N seconds |
1090
+ | `Write Line To Uart <text>` | Send a line to a UART |
1091
+ | `Execute Command mach set "boardA"` | Change the active machine (multi-board) |
1092
+ | `Execute Command <monitor command>` | Send any Renode monitor command |
1093
+ | `Read From Uart <n>` | Read N bytes from UART |
1094
+
1095
+ Test results (HTML report, JUnit XML) are written to `results/` by default (override with `--output`).
1096
+
1097
+ **Note:** each `robot` step is an independent simulation run (a fresh Renode process). Put all assertions that must share the same simulation state inside one step.
1098
+
1099
+ #### type: shell
1100
+
1101
+ Each step's `script` runs **inside a one-off Docker container** with the project directory bind-mounted at the workspace path, so the script can read/write your files and artifacts. Scripts execute with `set -ex` (exit on error; echo each command), so `+ command` trace lines appear in the output; a non-zero exit fails the step.
1102
+
1103
+ ```yaml
1104
+ - name: Validate log in a clean env
1105
+ type: shell
1106
+ image: python:3.12-slim # the container to run in
1107
+ script: |
1108
+ pip install --quiet pyyaml
1109
+ python3 tools/check_trace.py results/uart_output.log
1110
+ ```
1111
+
1112
+ - **`image:` is optional.** Omit it to use the run's renode image (which already has `python`/`wget`/shell tools) — hector logs which image it picked. Set it to pin a specific toolchain.
1113
+ - **`--no-docker`** runs the script on the host instead, ignoring `image:` (with a warning). See [Running without Docker](#running-without-docker---no-docker).
1114
+
1115
+ ##### `requires_sim: false`
1116
+
1117
+ By default a `shell` test runs after the simulation and counts as needing it. Set `requires_sim: false` for a check that's independent of Renode (lint a file, validate a generated artifact, run a host tool). Such a test runs even when the config defines **no machines at all** — handy for build-and-check pipelines:
1118
+
1119
+ ```yaml
1120
+ - name: Lint the generated config
1121
+ type: shell
1122
+ requires_sim: false
1123
+ image: python:3.12-slim
1124
+ script: |
1125
+ python3 -m yamllint .hector.yaml
1126
+ ```
1127
+
1128
+ A test that needs the simulation but finds no machine defined is skipped and marked failed, with a note telling you to add a machine or set `requires_sim: false`.
1129
+
1130
+ > **Migration:** the old `bash` and `docker` test types are now both `shell` (a `bash`
1131
+ > step → a `shell` step running on the host under `--no-docker`; a `docker` step → a
1132
+ > `shell` step with an `image:`). Old configs still run, with a rename warning.
1133
+
1134
+ ---
1135
+
1136
+ ### quantum
1137
+
1138
+ Override the emulation-wide time quantum (in seconds). The quantum controls how often Renode synchronises virtual time across machines and is only relevant in multi-machine jobs with hubs. ([Renode docs: time framework](https://renode.readthedocs.io/en/latest/advanced/time_framework.html))
1139
+
1140
+ ```yaml
1141
+ quantum: 0.0001 # 100 µs
1142
+ ```
1143
+
1144
+ If absent and any hub is declared, the framework defaults to `0.00001` (10 µs). If no hubs are present, the quantum is not set and Renode uses its internal default.
1145
+
1146
+ ---
1147
+
1148
+ ### ci
1149
+
1150
+ An **optional** block describing CI pipelines (when and how to run this config in
1151
+ continuous integration). The CLI itself never acts on `ci:` — no `hector` command
1152
+ reads it — it only validates the block's shape so a misconfiguration is caught
1153
+ early. The field is consumed by [Hector CI](https://hector-ci.com), the companion
1154
+ server that runs this same `.hector.yaml` (there is no separate `hector-ci.yml`);
1155
+ its full reference lives with that product. A minimal shape:
1156
+
1157
+ ```yaml
1158
+ ci:
1159
+ embedded-tests:
1160
+ when:
1161
+ branch: [main, "release/*"]
1162
+ event: [push, pull_request, manual]
1163
+ reporters: [json, junit]
1164
+ timeout: 30m
1165
+ ```
1166
+
1167
+ If you only ever drive `hector` yourself (locally or from your own CI runner),
1168
+ you can omit `ci:` entirely.
1169
+
1170
+ ---
1171
+
1172
+ ## Connection syntax
1173
+
1174
+ Pin and line numbers use Renode's `@` notation throughout.
1175
+
1176
+ ### Intra-machine signal wiring
1177
+
1178
+ Connects the output of one peripheral to the input of another on the same machine. Generates a Renode [platform description](https://renode.readthedocs.io/en/latest/advanced/platform_description_format.html) **updating entry** keyed on the source peripheral.
1179
+
1180
+ ```
1181
+ <machine>.<source> -> <machine>.<dest>@<pin>
1182
+ <machine>.<source>@<srcPin> -> <machine>.<dest>@<pin>
1183
+ <machine>.<source>.<Signal> -> <machine>.<dest>@<pin>
1184
+ <machine>.<source>[<range>] -> <machine>.<dest>@[<range>]
1185
+ ```
1186
+
1187
+ Inside a per-machine `connections:` block the machine prefix is optional — bare names are scoped to the parent machine automatically:
1188
+
1189
+ ```yaml
1190
+ connections: |
1191
+ button -> gpioPortC@13 # peripheral → GPIO pin
1192
+ gpioPortA@5 -> led@0 # GPIO pin → LED input
1193
+ nvic.IRQ -> cpu@0 # named signal output
1194
+ gpioPortC[0-15] -> exti@[0-15] # bracket range: 16 pins in one line
1195
+ exti[0-4] -> nvic@[6-10] # bracket range with offset destination
1196
+ ```
1197
+
1198
+ ### Cross-machine symmetric hub (uart / can / ethernet / wireless / ble / wisun)
1199
+
1200
+ Chained two-endpoint shorthand, or one endpoint↔hub line each — equivalent:
1201
+ ```
1202
+ <nodeA>.<periph> <-> <hub> <-> <nodeB>.<periph>
1203
+
1204
+ # …or as separate lines (and the only form for 3+ endpoints):
1205
+ boardA.usart2 <-> uartlink
1206
+ boardB.usart2 <-> uartlink
1207
+ boardC.usart2 <-> uartlink
1208
+ ```
1209
+
1210
+ ### Cross-machine GPIO (directional)
1211
+
1212
+ Left side is the SOURCE, right side is the DESTINATION. Chained one-liner, or the two
1213
+ endpoint→hub lines it expands to — equivalent:
1214
+ ```
1215
+ <nodeA>.<port>@<pin> -> <gpiohub> -> <nodeB>.<port>@<pin>
1216
+
1217
+ # …or split:
1218
+ boardA.gpioPortA@7 -> irqline # source
1219
+ irqline -> boardB.gpioPortB@4 # destination
1220
+ ```
1221
+
1222
+ ### USB (asymmetric)
1223
+
1224
+ Chained one-liner, or the two equivalent endpoint→hub lines:
1225
+ ```
1226
+ <device_node>.<periph> -> <usbhub> -> <host_node>.<periph>
1227
+
1228
+ # …or split:
1229
+ <device_node>.<periph> -> <usbhub>
1230
+ <usbhub> -> <host_node>.<periph>
1231
+ ```
1232
+
1233
+ ---
1234
+
1235
+ ## Interpolation
1236
+
1237
+ `${NAME}` is expanded anywhere in the YAML (keys, values, strings, block scalars). Precedence (highest wins):
1238
+
1239
+ 1. Matrix variables for the current job
1240
+ 2. `arguments:` values (or env var override of the same name)
1241
+ 3. Framework variables: `${RENODE_DIR}`, `${INTEGRATION_DIR}`
1242
+
1243
+ `${RENODE_DIR}` and `${INTEGRATION_DIR}` are container-absolute paths (`/workspace/.hector/renode` and `/workspace/.hector/renode-verilator-integration`). They are primarily useful in module `source:` fields.
1244
+
1245
+ **YAML quoting rule:** `${...}` inside a YAML flow sequence (`[...]`) or flow mapping (`{...}`) will cause a parse error because `{` is a reserved character in flow context. Use block style instead:
1246
+
1247
+ ```yaml
1248
+ # Wrong — will fail to parse
1249
+ platform: [ platforms/boards/${BOARD}.repl ]
1250
+
1251
+ # Correct
1252
+ platform:
1253
+ - platforms/boards/${BOARD}.repl
1254
+ ```
1255
+
1256
+ ---
1257
+
1258
+ ## Generated artifacts
1259
+
1260
+ Everything the framework generates lives under `.hector/` and is safe to delete (it will be rebuilt on the next run). Add this directory to `.gitignore`.
1261
+
1262
+ ```
1263
+ .hector/
1264
+ ├── renode/ # managed Renode clone
1265
+ ├── renode-verilator-integration/ # managed integration clone
1266
+ ├── build/
1267
+ │ ├── modules/<name>/ # compiled .so / .dll per module
1268
+ │ └── instances/<machine>__<inst>.so # per-instance .so copies (verilator only)
1269
+ ├── repl/
1270
+ │ └── job_<N>_<machine>.gen.repl # generated platform descriptions
1271
+ ├── resc/
1272
+ │ └── job_<N>.resc # generated emulation scripts
1273
+ └── tests/
1274
+ └── job_<N>_test_<M>.robot # generated Robot Framework wrappers
1275
+
1276
+ results/ # test + simulation output (override with --output)
1277
+ ├── build_0_<name>/ # one directory per build: step (reported like a test)
1278
+ │ └── report.html
1279
+ ├── test_0_<name>/ # one directory per test (robot / shell alike)
1280
+ │ ├── report.html # human-readable report (all test types)
1281
+ │ ├── log.html # robot only: full interactive log
1282
+ │ └── robot_output.xml # robot only: native Robot Framework XML
1283
+ ├── test_1_<name>/
1284
+ │ └── report.html
1285
+ ├── artifacts/ # files collected by the top-level 'artifacts:' globs
1286
+ │ └── job_<N>/… # mirrors each source path (avoids name clashes)
1287
+ ├── junit.xml # aggregated JUnit XML (--reporters junit)
1288
+ └── manifest.json # machine-readable result index (--reporters json)
1289
+ ```
1290
+
1291
+ `manifest.json` is the structured surface for dashboards / CI: one entry per test with
1292
+ its `status`, `duration`, `commands`, and relative paths to its detail artifacts
1293
+ (`report_html`, and for robot also `log_html` / `robot_xml`). Enable it with
1294
+ `--reporters junit --reporters json`.
1295
+
1296
+ ---
1297
+
1298
+ ## Complete example
1299
+
1300
+ ```yaml
1301
+ version: "0.1"
1302
+ renode_version: "1.16.1"
1303
+
1304
+ matrix:
1305
+ variables:
1306
+ VARIANT: ["default"]
1307
+
1308
+ arguments:
1309
+ BOARD: stm32f4_discovery
1310
+ FW: stm32f4_test.elf
1311
+
1312
+ # Pre-sim build steps, run once per job (in containers) before the machines are built.
1313
+ build:
1314
+ - name: Fetch firmware
1315
+ script: | # no image → the run's renode image (has wget)
1316
+ wget -q https://example.com/${FW} -O ${FW}
1317
+
1318
+ artifacts:
1319
+ - results/*.log
1320
+
1321
+ modules:
1322
+ uartlite:
1323
+ kind: renode-verilator
1324
+ type: CoSimulated.CoSimulatedUART
1325
+ source: "${INTEGRATION_DIR}/samples/uartlite"
1326
+
1327
+ hubs:
1328
+ uartlink: { type: uart }
1329
+ irqline: { type: gpio }
1330
+
1331
+ machines:
1332
+ boardA:
1333
+ platform:
1334
+ - platforms/boards/${BOARD}.repl
1335
+ firmware: ${FW}
1336
+ peripherals:
1337
+ uart0:
1338
+ type: uartlite
1339
+ at: "sysbus <0x70000000, +0x100>"
1340
+ frequency: 100000000
1341
+ Btn:
1342
+ type: Miscellaneous.Button
1343
+ at: gpioPortC 13
1344
+ connections: |
1345
+ Btn -> gpioPortC@13
1346
+ mappings: |
1347
+ uart4 -> file:results/${VARIANT}_uart.log
1348
+
1349
+ boardB:
1350
+ platform:
1351
+ - platforms/boards/${BOARD}.repl
1352
+ firmware: ${FW}
1353
+
1354
+ connections: |
1355
+ boardA.usart2 <-> uartlink <-> boardB.usart2
1356
+ boardA.gpioPortA@7 -> irqline -> boardB.gpioPortB@4
1357
+
1358
+ tests:
1359
+ - name: Firmware boots
1360
+ type: robot
1361
+ script: |
1362
+ Create Terminal Tester sysbus.usart2
1363
+ Wait For Line On Uart Board initialized timeout=30
1364
+
1365
+ - name: UART echo
1366
+ type: robot
1367
+ script: |
1368
+ Create Terminal Tester sysbus.usart2
1369
+ Write Line To Uart ping
1370
+ Wait For Line On Uart pong timeout=10
1371
+
1372
+ - name: Check log
1373
+ type: shell
1374
+ script: |
1375
+ grep -q "Board initialized" results/${VARIANT}_uart.log \
1376
+ && echo "PASS" || { echo "FAIL"; exit 1; }
1377
+ ```
1378
+
1379
+ Run it:
1380
+
1381
+ ```bash
1382
+ hector run # simulate
1383
+ hector run --debug boardA:3333 # debug boardA
1384
+ hector test # run tests
1385
+ hector test --fail-fast # stop on first failure
1386
+ hector run --job VARIANT=default # run only one matrix combination
1387
+ BOARD=stm32f7_discovery hector run # override argument via env
1388
+ ```
1389
+
1390
+ ---
1391
+
1392
+ ## License
1393
+
1394
+ Hector (the CLI) is licensed under the **GNU Affero General Public License v3.0
1395
+ or later** (AGPL-3.0-or-later). The full text is in [LICENSE](LICENSE); see
1396
+ [LICENSING.md](LICENSING.md) for what that means in practice — notably that your
1397
+ own firmware, `.hector.yaml` configs, and test files are **not** derivative
1398
+ works of Hector and carry no license obligation from it.
1399
+
1400
+ A separate commercial license is available for the **Hector CI** server; contact
1401
+ `info@hector-ci.com`.