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.
- hector/__init__.py +4 -0
- hector/__main__.py +7 -0
- hector/commands/__init__.py +35 -0
- hector/commands/base.py +111 -0
- hector/commands/export.py +26 -0
- hector/commands/init.py +15 -0
- hector/commands/run.py +27 -0
- hector/commands/test.py +52 -0
- hector/commands/validate.py +15 -0
- hector/connections.py +130 -0
- hector/core.py +259 -0
- hector/dependencies.py +41 -0
- hector/docker.py +191 -0
- hector/generator.py +292 -0
- hector/hubs.py +196 -0
- hector/mappings.py +167 -0
- hector/modules.py +201 -0
- hector/peripherals.py +168 -0
- hector/pipeline.py +693 -0
- hector/reporters.py +111 -0
- hector/runners.py +380 -0
- hector/scaffold.py +75 -0
- hector/validator.py +414 -0
- hector_cli-0.1.0.dist-info/METADATA +1401 -0
- hector_cli-0.1.0.dist-info/RECORD +29 -0
- hector_cli-0.1.0.dist-info/WHEEL +5 -0
- hector_cli-0.1.0.dist-info/entry_points.txt +2 -0
- hector_cli-0.1.0.dist-info/licenses/LICENSE +661 -0
- hector_cli-0.1.0.dist-info/top_level.txt +1 -0
hector/modules.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""
|
|
3
|
+
MODULE_KINDS — how a module type is built into a loadable artifact (.so or .dll).
|
|
4
|
+
|
|
5
|
+
Register a builder `(module_name, module_config, BuildContext) -> artifact_host_path`:
|
|
6
|
+
@MODULE_KINDS.register("my-kind")
|
|
7
|
+
def build_my_kind(module_name, cfg, ctx):
|
|
8
|
+
return path_to_artifact
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import glob
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from .core import (ARTIFACTS_DIR, LIBOPENLIBM_NAME, Registry,
|
|
17
|
+
container_path_for, split_host_container, to_container_path)
|
|
18
|
+
from .docker import run_build
|
|
19
|
+
|
|
20
|
+
MODULE_KINDS = Registry("module kind")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BuildContext:
|
|
25
|
+
renode_dir_container: str
|
|
26
|
+
integration_dir_host: str
|
|
27
|
+
image: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _libopenlibm_flag(integration_dir_host):
|
|
31
|
+
libm = os.path.join(integration_dir_host, "lib", LIBOPENLIBM_NAME)
|
|
32
|
+
if os.path.isfile(libm):
|
|
33
|
+
return f'-DLIBOPENLIBM="{container_path_for(libm)}"'
|
|
34
|
+
print(f" [!] {LIBOPENLIBM_NAME} not found at {libm}; skipping -DLIBOPENLIBM "
|
|
35
|
+
"(add it via cmake_flags if your build needs it).")
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@MODULE_KINDS.register("renode-verilator")
|
|
40
|
+
def build_renode_verilator(module_name, module_config, ctx):
|
|
41
|
+
"""Build a verilated co-simulation peripheral into a .so, inside the Renode image."""
|
|
42
|
+
if not module_config.get("type"):
|
|
43
|
+
raise ValueError(f"Module '{module_name}' is missing required 'type' (its Renode class).")
|
|
44
|
+
source = module_config.get("source")
|
|
45
|
+
if not source:
|
|
46
|
+
raise ValueError(f"Module '{module_name}' is missing required 'source'.")
|
|
47
|
+
|
|
48
|
+
source_host, source_container = split_host_container(source)
|
|
49
|
+
if not os.path.isdir(source_host):
|
|
50
|
+
raise FileNotFoundError(f"Module '{module_name}': source dir not found: {source} ({source_host})")
|
|
51
|
+
|
|
52
|
+
build_host = os.path.join(ARTIFACTS_DIR, "build", "modules", module_name)
|
|
53
|
+
os.makedirs(build_host, exist_ok=True)
|
|
54
|
+
build_container = to_container_path(build_host)
|
|
55
|
+
cmake_flags = module_config.get("cmake_flags", "")
|
|
56
|
+
auto_flags = _libopenlibm_flag(ctx.integration_dir_host)
|
|
57
|
+
uid, gid = os.getuid(), os.getgid()
|
|
58
|
+
|
|
59
|
+
print(f"\n[MODULE] Building '{module_name}' ({module_config['type']}) in {ctx.image}")
|
|
60
|
+
print(f" source: {source}")
|
|
61
|
+
|
|
62
|
+
run_build(
|
|
63
|
+
"set -e\n"
|
|
64
|
+
"export DEBIAN_FRONTEND=noninteractive\n"
|
|
65
|
+
"apt-get update\n"
|
|
66
|
+
"apt-get install -y --no-install-recommends build-essential cmake verilator\n"
|
|
67
|
+
f'cmake -S "{source_container}" -B "{build_container}" '
|
|
68
|
+
f'-DUSER_RENODE_DIR="{ctx.renode_dir_container}" {auto_flags} {cmake_flags}\n'
|
|
69
|
+
f'cmake --build "{build_container}"\n'
|
|
70
|
+
f'chown -R {uid}:{gid} "{build_container}"\n',
|
|
71
|
+
ctx.image,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
so_files = glob.glob(os.path.join(build_host, "**", "*.so"), recursive=True)
|
|
75
|
+
if not so_files:
|
|
76
|
+
raise RuntimeError(f"Module '{module_name}' produced no .so in {build_host}.")
|
|
77
|
+
if len(so_files) > 1:
|
|
78
|
+
raise RuntimeError(f"Module '{module_name}' produced multiple .so files: {so_files}.")
|
|
79
|
+
print(f" -> {so_files[0]}")
|
|
80
|
+
return so_files[0]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Renode DLLs bundled in its own installation — filter these out when locating
|
|
84
|
+
# the user's output DLL after a dotnet build.
|
|
85
|
+
_RENODE_DLL_PREFIXES = (
|
|
86
|
+
"Antmicro.", "Mono.", "System.", "Microsoft.", "mscorlib",
|
|
87
|
+
"netstandard", "IronPython", "FasterReflection",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@MODULE_KINDS.register("csharp")
|
|
92
|
+
def build_csharp(module_name, module_config, ctx):
|
|
93
|
+
"""Build or locate a C# Renode peripheral plugin (.dll).
|
|
94
|
+
`source` can be a pre-built .dll path or a directory containing a .csproj.
|
|
95
|
+
The DLL is loaded at emulation scope via `i @<path>`; one import covers all instances.
|
|
96
|
+
"""
|
|
97
|
+
if not module_config.get("type"):
|
|
98
|
+
raise ValueError(f"Module '{module_name}' is missing required 'type' (the C# class name).")
|
|
99
|
+
source = module_config.get("source")
|
|
100
|
+
if not source:
|
|
101
|
+
raise ValueError(f"Module '{module_name}' is missing required 'source'.")
|
|
102
|
+
|
|
103
|
+
source_host, _ = split_host_container(source)
|
|
104
|
+
|
|
105
|
+
# ---- pre-built DLL: use as-is ----
|
|
106
|
+
if source_host.endswith(".dll"):
|
|
107
|
+
if not os.path.isfile(source_host):
|
|
108
|
+
raise FileNotFoundError(f"Module '{module_name}': DLL not found: {source} ({source_host})")
|
|
109
|
+
print(f"\n[MODULE] C# '{module_name}' ({module_config['type']}) using pre-built DLL")
|
|
110
|
+
print(f" source: {source}")
|
|
111
|
+
return source_host
|
|
112
|
+
|
|
113
|
+
# ---- build from source ----
|
|
114
|
+
if not os.path.isdir(source_host):
|
|
115
|
+
raise FileNotFoundError(
|
|
116
|
+
f"Module '{module_name}': source not found: {source} ({source_host})\n"
|
|
117
|
+
" Provide either a directory containing a .csproj or a path to a pre-built .dll."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
source_container = to_container_path(source_host)
|
|
121
|
+
build_host = os.path.join(ARTIFACTS_DIR, "build", "modules", module_name)
|
|
122
|
+
os.makedirs(build_host, exist_ok=True)
|
|
123
|
+
build_container = to_container_path(build_host)
|
|
124
|
+
uid, gid = os.getuid(), os.getgid()
|
|
125
|
+
|
|
126
|
+
print(f"\n[MODULE] Building C# '{module_name}' ({module_config['type']}) in {ctx.image}")
|
|
127
|
+
print(f" source: {source}")
|
|
128
|
+
|
|
129
|
+
run_build(
|
|
130
|
+
"set -e\n"
|
|
131
|
+
# Renode's official image ships Mono; dotnet may also be present.
|
|
132
|
+
# Try dotnet first (produces a self-contained output), fall back to msbuild/xbuild.
|
|
133
|
+
f'if command -v dotnet >/dev/null 2>&1; then\n'
|
|
134
|
+
f' dotnet build "{source_container}" -o "{build_container}" -c Release\n'
|
|
135
|
+
f'elif command -v msbuild >/dev/null 2>&1; then\n'
|
|
136
|
+
f' msbuild "{source_container}" /p:OutputPath="{build_container}" /p:Configuration=Release\n'
|
|
137
|
+
f'else\n'
|
|
138
|
+
f' echo "[ERROR] Neither dotnet nor msbuild found in container {ctx.image}"; exit 1\n'
|
|
139
|
+
f'fi\n'
|
|
140
|
+
f'chown -R {uid}:{gid} "{build_container}"\n',
|
|
141
|
+
ctx.image,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Collect DLLs, excluding Renode/framework assemblies
|
|
145
|
+
all_dlls = glob.glob(os.path.join(build_host, "**", "*.dll"), recursive=True)
|
|
146
|
+
user_dlls = [
|
|
147
|
+
f for f in all_dlls
|
|
148
|
+
if not any(os.path.basename(f).startswith(p) for p in _RENODE_DLL_PREFIXES)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
if not user_dlls:
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
f"Module '{module_name}': no user DLL found in {build_host}.\n"
|
|
154
|
+
f" All .dll files found: {[os.path.basename(f) for f in all_dlls]}"
|
|
155
|
+
)
|
|
156
|
+
if len(user_dlls) > 1:
|
|
157
|
+
# Try to pick the one whose name matches the module or the last path component of source
|
|
158
|
+
stem = os.path.basename(source_host)
|
|
159
|
+
candidates = [f for f in user_dlls
|
|
160
|
+
if module_name.lower() in os.path.basename(f).lower()
|
|
161
|
+
or stem.lower() in os.path.basename(f).lower()]
|
|
162
|
+
if len(candidates) == 1:
|
|
163
|
+
user_dlls = candidates
|
|
164
|
+
else:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"Module '{module_name}': multiple user DLLs found — cannot determine which "
|
|
167
|
+
f"to use: {[os.path.basename(f) for f in user_dlls]}.\n"
|
|
168
|
+
f" Rename your project output or point 'source' directly at the .dll."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
print(f" -> {user_dlls[0]}")
|
|
172
|
+
return user_dlls[0]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_modules(modules_cfg, ctx):
|
|
176
|
+
"""Build every declared module via its registered kind.
|
|
177
|
+
|
|
178
|
+
Returns {module_name: {'type': renode_class, 'artifact_host': path, 'artifact_type': 'so'|'dll'}}
|
|
179
|
+
"""
|
|
180
|
+
registry = {}
|
|
181
|
+
for name, cfg in (modules_cfg or {}).items():
|
|
182
|
+
kind = cfg.get("kind", "renode-verilator")
|
|
183
|
+
builder = MODULE_KINDS.get(kind)
|
|
184
|
+
artifact_host = builder(name, cfg, ctx)
|
|
185
|
+
artifact_type = "dll" if artifact_host.endswith(".dll") else "so"
|
|
186
|
+
registry[name] = {
|
|
187
|
+
"type": cfg.get("type"),
|
|
188
|
+
"artifact_host": artifact_host,
|
|
189
|
+
"artifact_type": artifact_type,
|
|
190
|
+
}
|
|
191
|
+
return registry
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def materialize_instance_so(node_name, instance, module_so_host):
|
|
195
|
+
"""Copy a module's .so to a per-instance file so each instance gets a distinct inode
|
|
196
|
+
(and therefore independent simulation state when loaded)."""
|
|
197
|
+
inst_dir = os.path.join(ARTIFACTS_DIR, "build", "instances")
|
|
198
|
+
os.makedirs(inst_dir, exist_ok=True)
|
|
199
|
+
dest = os.path.join(inst_dir, f"{node_name}__{instance}.so")
|
|
200
|
+
shutil.copy2(module_so_host, dest)
|
|
201
|
+
return dest
|
hector/peripherals.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Peripheral instantiation: place a native Renode class OR a module type into a machine.
|
|
3
|
+
|
|
4
|
+
Verilator (.so) modules: per-instance .so copy + SimulationFilePathLinux command
|
|
5
|
+
(stored in NodeDescriptor.so_commands).
|
|
6
|
+
C# (.dll) modules: emulation-level `i @<dll>` import (stored in
|
|
7
|
+
NodeDescriptor.csharp_dlls; pipeline.py emits these before
|
|
8
|
+
any machine block in the resc).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from .core import to_container_path
|
|
14
|
+
from .generator import NodeDescriptor, PeripheralDef
|
|
15
|
+
from .modules import materialize_instance_so
|
|
16
|
+
|
|
17
|
+
_RESERVED = {"type", "at", "peripherals", "init", "connections"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_repl_signals(raw):
|
|
21
|
+
"""Parse repl-style signal connection lines into {peripheral_name: [repl_line, ...]}.
|
|
22
|
+
|
|
23
|
+
Each line: <periph>[<pin_range>] -> <target>@<pin>
|
|
24
|
+
nvic -> cpu@0 => nvic: ["-> cpu@0"]
|
|
25
|
+
nvic.IRQ -> cpu@0 => nvic: ["IRQ -> cpu@0"]
|
|
26
|
+
gpioPortC[0-15] -> nvic@6 => gpioPortC: ["[0-15] -> nvic@6"]
|
|
27
|
+
|
|
28
|
+
The destination (right of '->') is preserved verbatim, so Renode's receiver
|
|
29
|
+
qualifiers pass through untouched, e.g. a local index '#N' and an input pin '@N':
|
|
30
|
+
timer1@0 -> gpioPortA#08@01 => timer1: ["0 -> gpioPortA#08@01"]
|
|
31
|
+
The source (left of '->') is only a named signal, a number, or a range — '#' is a
|
|
32
|
+
receiver-only qualifier, so a '#' on the source side is flagged, not emitted.
|
|
33
|
+
"""
|
|
34
|
+
result = {}
|
|
35
|
+
for line in (raw or "").splitlines():
|
|
36
|
+
line = line.strip()
|
|
37
|
+
if not line or line.startswith("#"):
|
|
38
|
+
continue
|
|
39
|
+
if " -> " not in line:
|
|
40
|
+
print(f" [WARN] irqs: '{line}' has no '->' — skipped.")
|
|
41
|
+
continue
|
|
42
|
+
source, target = line.split(" -> ", 1)
|
|
43
|
+
source, target = source.strip(), target.strip()
|
|
44
|
+
if "#" in source:
|
|
45
|
+
# '#localIndex' is a Renode *receiver* qualifier (it belongs on the
|
|
46
|
+
# destination, right of '->'). It has no meaning on the source side, so
|
|
47
|
+
# emitting it would produce a .repl Renode rejects. Flag and skip.
|
|
48
|
+
print(f" [WARN] irqs: '{line}' has '#' on the source side; '#index' is a "
|
|
49
|
+
"destination (receiver) qualifier and belongs after '->' "
|
|
50
|
+
"(e.g. 'timer1@0 -> gpioPortA#08@01'). Skipped.")
|
|
51
|
+
continue
|
|
52
|
+
if "[" in source:
|
|
53
|
+
periph, pin = source.split("[", 1)
|
|
54
|
+
irq_line = f"[{pin} -> {target}"
|
|
55
|
+
elif "." in source:
|
|
56
|
+
periph, signal = source.split(".", 1)
|
|
57
|
+
irq_line = f"{signal} -> {target}"
|
|
58
|
+
elif "@" in source:
|
|
59
|
+
periph, pin = source.split("@", 1)
|
|
60
|
+
irq_line = f"{pin} -> {target}"
|
|
61
|
+
else:
|
|
62
|
+
periph = source
|
|
63
|
+
irq_line = f"-> {target}"
|
|
64
|
+
result.setdefault(periph.strip(), []).append(irq_line)
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _gather_connections(peripherals):
|
|
69
|
+
"""Collect `connections:` blocks declared on peripherals (recursively).
|
|
70
|
+
|
|
71
|
+
Lets wiring live next to the peripheral it concerns — e.g. an `mcu` grouping
|
|
72
|
+
peripheral can carry all the MCU-internal wiring, separate from a board-level
|
|
73
|
+
`connections:` at the node. Returns the combined raw text, parsed together with
|
|
74
|
+
the node-level connections. These are always intra-machine signals; cross-machine
|
|
75
|
+
hub links must still use the node-level `connections:` block.
|
|
76
|
+
"""
|
|
77
|
+
blocks = []
|
|
78
|
+
for spec in (peripherals or {}).values():
|
|
79
|
+
spec = spec or {}
|
|
80
|
+
if not isinstance(spec, dict):
|
|
81
|
+
continue
|
|
82
|
+
conn = spec.get("connections")
|
|
83
|
+
if conn:
|
|
84
|
+
blocks.append(conn if isinstance(conn, str) else "\n".join(str(c) for c in conn))
|
|
85
|
+
child = spec.get("peripherals")
|
|
86
|
+
if child:
|
|
87
|
+
sub = _gather_connections(child)
|
|
88
|
+
if sub:
|
|
89
|
+
blocks.append(sub)
|
|
90
|
+
return "\n".join(blocks)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def resolve_peripherals(concrete_nodes, modules_registry):
|
|
94
|
+
"""Build a NodeDescriptor for each renode node.
|
|
95
|
+
|
|
96
|
+
Returns {node_name: NodeDescriptor} with peripheral_defs, so_commands,
|
|
97
|
+
csharp_dlls, and init populated. updating_entries is left empty — connections.py
|
|
98
|
+
fills those in later.
|
|
99
|
+
"""
|
|
100
|
+
descriptors = {}
|
|
101
|
+
for node_name, node_config in concrete_nodes.items():
|
|
102
|
+
if node_config.get("backend", "renode") != "renode":
|
|
103
|
+
continue
|
|
104
|
+
# Wiring may be declared at the node level (`connections:`) or inline on a
|
|
105
|
+
# peripheral (a `connections:` block on the peripheral it concerns). Both are
|
|
106
|
+
# intra-machine signals and merge into one map.
|
|
107
|
+
raw_signals = node_config.get("_repl_signals", "")
|
|
108
|
+
periph_signals = _gather_connections(node_config.get("peripherals") or {})
|
|
109
|
+
signals_map = _parse_repl_signals(
|
|
110
|
+
"\n".join(s for s in (raw_signals, periph_signals) if s))
|
|
111
|
+
init = (node_config.get("init") or "").strip() or None
|
|
112
|
+
descriptor = NodeDescriptor(name=node_name, init=init)
|
|
113
|
+
|
|
114
|
+
for instance, spec in (node_config.get("peripherals") or {}).items():
|
|
115
|
+
_resolve(instance, spec or {}, node_name, descriptor, modules_registry,
|
|
116
|
+
default_at="sysbus", signals_map=signals_map)
|
|
117
|
+
|
|
118
|
+
# Signals targeting peripherals not declared in this YAML (from a platform .repl file)
|
|
119
|
+
# can't be inlined; emit them as updating entries instead.
|
|
120
|
+
declared = {p.name for p in descriptor.peripheral_defs}
|
|
121
|
+
for periph_name, wires in signals_map.items():
|
|
122
|
+
if periph_name not in declared:
|
|
123
|
+
for wire in wires:
|
|
124
|
+
descriptor.updating_entries.append((periph_name, wire))
|
|
125
|
+
|
|
126
|
+
descriptors[node_name] = descriptor
|
|
127
|
+
return descriptors
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _resolve(instance, spec, node_name, descriptor, modules_registry, default_at, signals_map=None):
|
|
131
|
+
type_name = spec.get("type")
|
|
132
|
+
if not type_name:
|
|
133
|
+
raise ValueError(f"peripherals.{instance} on '{node_name}' is missing 'type'.")
|
|
134
|
+
|
|
135
|
+
at = spec["at"] if "at" in spec else default_at
|
|
136
|
+
at = at or None
|
|
137
|
+
signals = list((signals_map or {}).get(instance, []))
|
|
138
|
+
init = spec.get("init") or None
|
|
139
|
+
args = {k: v for k, v in spec.items() if k not in _RESERVED}
|
|
140
|
+
|
|
141
|
+
if type_name in modules_registry:
|
|
142
|
+
mod = modules_registry[type_name]
|
|
143
|
+
descriptor.peripheral_defs.append(
|
|
144
|
+
PeripheralDef(name=instance, ptype=mod["type"], at=at,
|
|
145
|
+
args=args, signals=signals, init=init))
|
|
146
|
+
|
|
147
|
+
if mod["artifact_type"] == "so":
|
|
148
|
+
inst_so = materialize_instance_so(node_name, instance, mod["artifact_host"])
|
|
149
|
+
descriptor.so_commands.append(
|
|
150
|
+
f"{instance} SimulationFilePathLinux @{to_container_path(inst_so)}")
|
|
151
|
+
print(f"[INST] {node_name}.{instance} = module '{type_name}' ({mod['type']})"
|
|
152
|
+
+ (f" @ {at}" if at else "")
|
|
153
|
+
+ f" [.so: {os.path.basename(inst_so)}]")
|
|
154
|
+
else:
|
|
155
|
+
descriptor.csharp_dlls.add(mod["artifact_host"])
|
|
156
|
+
print(f"[INST] {node_name}.{instance} = C# module '{type_name}' ({mod['type']})"
|
|
157
|
+
+ (f" @ {at}" if at else "")
|
|
158
|
+
+ f" [.dll: {os.path.basename(mod['artifact_host'])}]")
|
|
159
|
+
else:
|
|
160
|
+
descriptor.peripheral_defs.append(
|
|
161
|
+
PeripheralDef(name=instance, ptype=type_name, at=at,
|
|
162
|
+
args=args, signals=signals, init=init))
|
|
163
|
+
print(f"[INST] {node_name}.{instance} = native {type_name} "
|
|
164
|
+
+ (f"@ {at}" if at else "(no registration)"))
|
|
165
|
+
|
|
166
|
+
for child_instance, child_spec in (spec.get("peripherals") or {}).items():
|
|
167
|
+
_resolve(child_instance, child_spec or {}, node_name, descriptor, modules_registry,
|
|
168
|
+
default_at=instance, signals_map=signals_map)
|