Docs / Parts
Your board's parts, by the schematic's names
A simulated board is made of parts: the processor running your firmware, and the chips around it. You can call each one by its reference designator, as your schematic does: U4, U40, J5. That matters most when a board carries several identical parts. Two e-fuses of the same type are both “E-fuse (TPS25947)” to the simulation, but U4 and U6 to you.
You can also look parts up the way you would query a web page's elements: every part on a bus, the one at an I2C address, everything wired to the processor. Then you can walk from one part to the parts it is connected to.
Everything on this page works in the Python SDK, the hardlabs command line, scenario files and the MCP tools for coding agents. All of them use the same names.
The examples use one small board throughout. U1 is the processor (an STM32G474). U4 and U6 are two identical TPS25947 e-fuses, enabled from processor pins PB3 and PB4. U40 and U41 are INA219 current monitors at 0x40 and 0x41 on the processor's i2c0. U9 is a BME280 at 0x76 that has no model yet.
1. Name a part by its reference designator
Every control (something you can do to the board) and every reading (a value the board shows) has a name made from its panel's title and its own label: e-fuse-tps25947.extra-load-current. Two panels with the same title can't both have that name, so the second one gets -2: e-fuse-tps25947.extra-load-current-2. That suffix doesn't say which chip is which.
A part's controls and readings therefore also answer to <refdes>.<name>:
| Panel name | Reference designator name |
|---|---|
| e-fuse-tps25947.extra-load-current | U4.extra-load-current |
| e-fuse-tps25947.extra-load-current-2 | U6.extra-load-current |
| e-fuse-tps25947.fault-2 | U6.fault |
| u40-current-monitor-ina219.bus-voltage | U40.bus-voltage |
- Any case. u6.extra-load-current works too.
- The panel names still work. Scenarios and scripts written against them don't need to change.
- The name after the dot is the label, lower case, with anything that isn't a letter or a digit turned into a dash: “Extra load current” becomes extra-load-current. When one part has two items with the same label, the second gets -2, counted per part, so it reads the same on every copy of the part.
- A short name (just extra-load-current) still works when only one part has it. When several parts do, it is refused and the refusal lists them, so a test never drives the wrong chip by accident.
2. Which parts get one
A part answers to its reference designator when the simulation knows which panel shows it:
- the board's parts list (what the parts command shows) has a row for exactly that one part, and the row points at the panel; or
- the panel's title starts with the reference designator. Boards HardLabs builds from your netlist title their panels that way: “U40 Current monitor (INA219)”.
Some panels don't get one:
- A panel that shows several parts (one parts-list row for U13, U14) names none of them as a whole, because the panel can't tell them apart. Each part is still listed, and items on the panel that belong to one of them can say so (see limits).
- Panels of the board's nets (its lines and analog signals) aren't one part. A connector that a line runs through doesn't lend its name to every line on the board.
3. See what a part can do
In Python
Install the SDK with pip install hardlabs-sdk and sign in once with npx hardlabs login (in CI, set HARDLABS_TOKEN). Each part prints its actions:
from hardlabs_sdk import Client
board = Client().start("company_name/board_name", "build/app.elf")
for part in board.parts():
print(part)U1 STM32G474RE — Processor, partial
Runs your firmware.
wired to U4, U6, U9, U40, U41
(nothing to do or read on this part)
U4 TPS25947 — E-fuse, simulated
wired to U1, U40
do U4.extra-load-current <value 0..5 A>
do U4.short-the-output
read U4.load-current, U4.fault, U4.power-good
U6 TPS25947 — E-fuse, simulated
wired to U1
do U6.extra-load-current <value 0..5 A>
do U6.short-the-output
read U6.load-current, U6.fault, U6.power-good
U40 INA219 — Current monitor, i2c0 0x40, simulated
wired to U1, U4
read U40.bus-voltage
…
U9 BME280 — Environment sensor, i2c0 0x76, not simulated
No model yet: reads at 0x76 get no answer.
wired to U1
(nothing to do or read on this part)board.parts()lists every part except passives. That includes parts the simulation doesn't cover yet, so you can see what your firmware won't get a real answer from. One part is board.U4 or board.part("U4"). In an interactive session, Tab completes reference designators after the first board.parts().
In the hardlabs shell
hardlabs> controls U4
U4.extra-load-current slider <value 0..5 A> (e-fuse-tps25947.extra-load-current)
U4.short-the-output button (e-fuse-tps25947.short-the-output)
hardlabs> status U6 # U6's panel and its values now
hardlabs> scope U6 # what U6's model covers, assumes and leaves out
hardlabs> parts U9 # U9's row in the parts list, in fullWithout a part, controls lists every control under its panel name, with its reference designator names after it. Tab completes both.
For a coding agent (MCP)
list_controls, board_status and model_scope take a reference designator as their panel: {"panel": "U4"}. Controls are listed with their other names (also U4.extra-load-current), so an agent can use either.
4. Drive and read a part
| Where | Example |
|---|---|
| Python | board.U4.do("extra-load-current", 0.5) or board.do("U4.extra-load-current", 0.5) |
| Python, read | board.U6.read("fault") · board.U6.readings() · board.read("U40.bus-voltage") |
| hardlabs shell | do U4.extra-load-current 0.5 · do U4.short-the-output |
| Scenario file | do: U4.extra-load-current · expect: U6.fault |
| MCP do_control | {"control": "U4.extra-load-current", "values": ["0.5"]} |
| Website Monitor console | report U4 · U4 GetStateJson |
board.U4.do("extra-load-current", 4.5) # above U4's current limit
board.wait(0.5) # simulated seconds
assert board.U4.read("fault") is True
assert board.U6.read("fault") is False # the other e-fuse didn't trip
board.expect_console("SENSORS rail fault")simulation: company_name/board_name
name: E-fuse faults
cases:
- name: An overload on the sensors rail trips U4 only
steps:
- do: U4.extra-load-current
values: [4.5]
- wait: 500ms
- expect: U4.fault
equals: true
- expect: U6.fault
equals: false
- expect_console: SENSORS rail faultA value works as the control's kind expects: a number for a slider, true or false for a toggle, named values for a form (board.U2.do("set", volts=5.0)), nothing for a button. hardlabs run --list -s company_name/board_name prints every name a scenario can use, with its reference designator names.
5. Find parts by where they sit
Filter the parts by bus, I2C address, what they are wired to, or how far the simulation covers them. Filters combine.
| Filter | Finds | Example |
|---|---|---|
| bus | the parts on that bus, as your board names it (any case) | bus="i2c0" |
| address | the part at that I2C address, as a number or text | address=0x40 · address="0x40" · address=64 |
| connected_to | the parts wired to that part (see below) | connected_to="U1" |
| status | simulated, partial, folded, not_simulated or not_fitted | status="not_simulated" |
board.parts(bus="i2c0") # [U40, U41, U9]
board.find(bus="i2c0", address=0x41) # U41: exactly one part, or LookupError
board.parts(connected_to="U1", status="not_simulated") # what the firmware can't get answers from
for part in board.mcu.connected(): # every part the processor reaches
print(part.ref, part.bus, part.address, part.mcu_pins)
board.U4.connected() # [U1, U40]: walk from a part to its neighbours
board.U4.mcu_pins # ({'pin': 'PB3', 'net': 'EN_SENSORS'},)- board.parts(...) returns parts in parts-list order. part.connected() puts the processor first.
- board.find(...) takes the same filters and returns the one part they match. When none or several match, it raises LookupError naming them: 3 parts with bus='i2c0': U40, U41, U9.
- board.mcu is the processor. connected_to takes a reference designator or a part: board.parts(connected_to=board.U40).
- The SDK reads the parts list once per boot and filters it locally, so a loop over parts costs no requests. board.restart() reads it again.
hardlabs> parts bus=i2c0
U40 INA219 — Current monitor, i2c0 0x40, simulated
wired to U1, U4
read U40.bus-voltage
U41 INA219 — Current monitor, i2c0 0x41, simulated
wired to U1
read U41.bus-voltage
U9 BME280 — Environment sensor, i2c0 0x76, not simulated yet
wired to U1
hardlabs> parts address=0x40
hardlabs> parts connected=U1 status=not_simulatedIn the shell, at= is short for address=. Without a filter, parts shows the whole parts list grouped by status, as before. For coding agents, the MCP tool board_parts takes bus, address, connected_to and status.
6. What “wired to” means
Two parts are wired to each other when one of these holds. It always goes both ways: if U4 is wired to U40, U40 is wired to U4.
| Connection | Example |
|---|---|
| A processor bus or pin | U40 on the processor's i2c0, or U4's enable on pin PB3: both are wired to U1 |
| Behind an I2C switch | a sensor at 0x76 behind switch U42 is wired to U42, not to the processor |
| A shared signal net | U4's fault output and U40's alert pin on the net FLT_SENSORS: U4 and U40 |
Supply and ground pins don't count, otherwise every part would be wired to every other. A processor pin reached through one series resistor counts as wired, and the pin says which resistor ({"pin": "PA9", "net": "TX", "via": "R12"}).
Where these facts come from
- Bus, address and chip select: from how the board was built. On a board HardLabs builds from your netlist, that is the bus and address each part is attached at. On a board described by hand, they are read from where the parts list places a part (“i2c0 0x40”, “spi0 CS PC16”, “0x5A behind U12”).
- Processor pins and shared nets: from your netlist. A board without one knows each part's bus and address but not its other wiring.
7. What a part tells you
A Part from the Python SDK has these fields. The HTTP API returns the same facts (see below).
| Field | What it holds |
|---|---|
| ref | its reference designator: "U4" |
| part | its part number or name from the parts list: "TPS25947" |
| what | what it is: "E-fuse", "Processor" |
| status | how far the simulation covers it: simulated, partial, folded (treated as ideal inside another part), not_simulated, not_fitted |
| note | what the simulation leaves out of it, when it says |
| where | where the parts list places it, as text: "i2c0 0x40" |
| bus | the bus it is on, as your board names it: "i2c0" |
| address | its I2C address as a number: 0x40 (64) |
| cs | an SPI part's chip-select pin: "PC16" |
| behind | the I2C switch its bus runs through: "U42" |
| mcu_pins | the processor pins it reaches: ({"pin": "PB3", "net": "EN_SENSORS"},) |
| connected_refs | the reference designators it is wired to, the processor first |
| is_mcu | True on the processor |
| controls | its controls, each named U4.<control> with its usage |
| reading_names | its readings' names: ("U4.load-current", "U4.fault", …) |
| Method | Does |
|---|---|
| part.do(control, value) | works one of its controls: part.do("extra-load-current", 0.5) |
| part.read(name) | one reading's value: a number, True/False, a name, or None when the board doesn't report it |
| part.readings() | all its readings now, by U4.<name>, each with its value, unit and text |
| part.connected() | the parts it is wired to, as Part objects |
| print(part) | what it is, what it is wired to, and every action |
8. When a name doesn't fit
Every refusal says what would work instead.
| You wrote | You get |
|---|---|
| extra-load-current | 'extra-load-current' is on several parts: U4.extra-load-current, U6.extra-load-current |
| U4.load | U4 has no control 'load': U4.extra-load-current, U4.short-the-output |
| board.U99 | AttributeError: no part 'U99' on this board — parts: U1, U4, U6, U40, U41, U9 |
| board.find(bus="i2c0") | LookupError: 3 parts with bus='i2c0': U40, U41, U9 |
| parts address=sixty | address: 'sixty' is not a number (0x6B or 107) |
A scenario that names a part or control the board doesn't have fails before anything runs (exit code 2), listing every name that doesn't exist.
9. HTTP API
The SDK and the CLI use these endpoints on a running simulation. Use them directly from another language with your API token.
| Request | Returns |
|---|---|
| GET /v1/jobs/<id>/sim/parts | every part with its facts, controls and readings, and the processor as mcu |
| GET …/sim/parts?bus=i2c0&address=0x40 | only the parts matching every filter: bus, address, connected_to, status |
| GET …/sim/controls | every control; a part's also lists refs ["U4"] and aliases ["U4.extra-load-current"] |
| GET …/sim/readings?name=U6.fault | readings by any of their names, with refs and aliases |
| POST …/sim/do {"control": "U4.extra-load-current", "value": 0.5} | works a control by any of its names |
{
"mcu": "U1",
"parts": [
{
"ref": "U1", "part": "STM32G474RE", "what": "Processor",
"status": "partial", "note": "Runs your firmware.", "mcu": true,
"connected": ["U4", "U6", "U9", "U40", "U41"],
"controls": [], "readings": []
},
{
"ref": "U40", "part": "INA219", "what": "Current monitor",
"where": "i2c0 0x40", "status": "simulated",
"bus": "i2c0", "address": 64,
"connected": ["U1", "U4"],
"controls": [],
"readings": [
{ "name": "U40.bus-voltage", "label": "Bus voltage", "unit": "V",
"reading": "u40-current-monitor-ina219.bus-voltage" }
]
}
]
}Each control in a part's controls has its name (U4.extra-load-current), kind, usage, its range and unit, and its panel name as control. Fields with no value are left out: a part on no bus has no bus. An address that isn't a number is a 400.
10. What it doesn't do
- Passives aren't listed. Resistors, capacitors and the like are counted in the parts list but have no actions.
- Several parts on one panel (two haptic drivers shown together) don't get names of their own unless the board's description says which item belongs to which. A board described by hand can mark an item with "ref": "U13". Ask us to do it for a board we built for you.
- No wiring without a netlist. Processor pins and shared nets come from your netlist. Without one, only bus and address are known.
- Nets aren't parts. To follow a signal over time, use the website's Signals view, or the MCP tools list_signals and signal_history.
- Every value comes from a model. Before you blame your firmware for a surprising reading, check what the part's model covers (scope U4, or the panel's Model chip on the website).
Next: the hardlabs command line · all docs