Skip to main content

HardLabs SDK for Python

Run your tests against a HardLabs simulation of your board, running the firmware you'd flash. Use it two ways:

  • Keep your test framework. If your tests talk to the device over a serial line (RS-485, a debug UART), the SDK opens the simulated board's port on your computer. pyserial opens it with serial_for_url(...), so your framework runs unchanged.
  • Drive the board from your tests. Work the board's controls (supply voltage, a sensor's value, a button), read what its panels show, type into the firmware's console and wait for output. Waits count simulated time.
pip install hardlabs-sdk
npx hardlabs login        # once per computer; in CI, set HARDLABS_TOKEN instead

Firmware: one ELF, or what your flash holds

from hardlabs_sdk import Client

board = Client().start("company_name/board_name", "build/app.elf")

# A bootloader and its application, each at the address you flash it to:
board = Client().start("company_name/board_name", ["bootloader.bin@0x00400000", "app.bin@0x00410000"])
print(board.firmware["images"])   # where each image went, with its sha256

Raw .bin images need their address. Intel HEX files and ELF files carry their own addresses. The CPU boots from the image at the lowest address, as it would from flash.

Your framework, on the simulated board

with Client().start("company_name/board_name", ["bootloader.bin@0x00400000", "app.bin@0x00410000"]) as board:
    print(board.serial_ports())          # ['console', 'rs485', 'rs485-b']
    bus = board.serial("rs485")
    print(bus.url)                       # socket://127.0.0.1:53817

    import serial
    port = serial.serial_for_url(bus.url, baudrate=115200, timeout=5)
    port.write(frame)                    # bytes go to the firmware's UART as they are
  • Bytes: nothing is added or stripped. A binary protocol (SLIP frames, CRCs) arrives as sent.
  • Restarts: the local port stays the same while the board restarts. The bridge reconnects to the new session by itself.
  • Device files: on macOS and Linux, board.serial("rs485", pty=True) gives a pseudo-terminal at bus.path (/dev/ttys012) for tools that want one.
  • Ports by hand: python -m hardlabs_sdk up company_name/board_name --firmware bootloader.bin@0x00400000 --firmware app.bin@0x00410000 --serial rs485 boots the board and prints each port's address. Point your own CLI at it. Ctrl-C stops it.

Swapping a bench rig for the simulation in pytest

If your conftest finds the device's serial port in a session fixture, replace that one fixture. The same pattern runs a suite against a local simulator:

# conftest.py next to the tests you want to run on the simulated board
import pytest

@pytest.fixture(name="setup", scope="session", autouse=True)
def setup_fixture(pytestconfig, hardlabs_serial):
    port = hardlabs_serial("rs485")              # the board, booted once for the session
    set_context(port=port.url, ...)              # your framework's own setup, minus USB discovery
pytest tests/pcb --hardlabs-sim company_name/board_name \
    --hardlabs-firmware bootloader.bin@0x00400000 --hardlabs-firmware app.bin@0x00410000

pytest fixtures

Installing the package adds them:

hardlabs_board the board, booted once per session with --hardlabs-firmware (or HARDLABS_FIRMWARE)
hardlabs_serial(port, pty=False) a local serial port for the session, the same across restarts
@pytest.mark.hardlabs_fresh boot the board afresh before this test

A failing test's report includes the console's last lines. It also lists what went wrong in the run: the firmware's crashes, asserts and restarts, and anything that may be a limit of the simulation.

Driving the board

board.wait_for("app ready", within=5)          # a console line, within 5 s of simulated time
board.send("status")                           # one line into the firmware's shell
board.expect_console("mode: SAFE", "faults: 0")

board.do("bench-supply.volts", 11.5)           # a control, by the name the website and CLI use
board.do("usb.plugged-in", True)
board.do("reset.press")
for c in board.controls():
    print(c.name, c.usage)

assert board.read("power-monitor.bus-current") < 0.2
print(board.readings())                        # every value the panels show, with units

board.wait(2.0)                                # let 2 s of simulated time pass
board.clock()                                  # simulated seconds since boot
board.restart()                                # boot again with the same firmware
board.problems()                               # crashes, asserts, restarts; the simulation's limits
  • Names: controls and readings use the names the website, the CLI and scenario files use: <panel>.<control>. The part after the dot is enough when only one panel has it. A part's also answer to its schematic reference designator, U4.extra-load-current (below).
  • Timeouts: ConsoleTimeout says which text didn't appear and shows what the console printed instead. SimulationStopped means simulated time stopped moving.

Parts by reference designator

The full guide, with every filter, field and error: https://www.hardlabs.io/docs/parts

Name a part the way the schematic does. Several identical parts (five load switches, two haptic drivers) are told apart by their refdes:

board.U4.do("extra-load-current", 0.5)         # or board.do("U4.extra-load-current", 0.5)
assert not board.U4.read("fault")
print(board.U6.readings())                     # {'U6.load-current': Reading(...), 'U6.fault': ...}

for part in board.parts():                     # every part on the board, passives left out
    print(part)
# U4 MAX890LESA+ — Load switch, simulated
#   do   U4.extra-load-current <value 0..2 A>
#   read U4.load-current, U4.fault
# U9 BME280 — Environment sensor, not simulated
#   ...
  • board.part("U4") is board.U4, in any case. A part the board lacks raises with the ones it has.
  • Each part has ref, part (its part number or name), what, where (a bus and address, when it is on one), status (how far it is simulated), note, controls and reading_names.
  • A part with nothing to do or read is still listed: the parts list says what the simulation leaves out.

Finding parts by where they sit

Like a DOM query: filter the parts by bus, address, or what they are wired to.

board.parts(bus="i2c0")                     # every part on that bus, as the board names it
board.find(bus="i2c0", address=0x6B)        # exactly one part, or LookupError
board.parts(connected_to="U1", status="not_simulated")

for part in board.mcu.connected():          # every part the processor reaches
    print(part.ref, part.bus, part.address, part.mcu_pins)
board.U43.behind                            # "U42": the I2C switch it sits behind
  • Each part has bus, address, cs (an SPI part's chip select), behind, mcu_pins (the processor pins it reaches, with their nets) and connected_refs. part.connected() gives the parts themselves, the processor first.
  • connected_to the processor includes the parts on its buses. For any other part it means a shared signal net, or the switch a part sits behind.
  • Wiring comes from the board's netlist. A board without one knows only what its parts list says about each part's bus and address.

On-target unit tests

A test binary built for the board's processor runs on the simulated one. The SDK reads its Unity or GoogleTest results from the console:

board = Client().start("company_name/board_name", "build/tests.elf")
report = board.run_unit_tests(within=120)
open("results.xml", "w").write(report.junit())
assert report.ok, report.summary()

What it is not

  • Not your physical board. Every value comes from a model of a part. Before you blame the firmware for a disagreement with the board, check what the model covers: hardlabs → scope, or a panel's Model chip on the website.
  • One session per board per account. Two CI jobs that use the same token on the same board share one simulation. Give parallel jobs their own boards, or run them one after the other.

Metadata

Release files for hardlabs-sdk 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hardlabs-sdk 0.1.0
File Size Uploaded Provenance
hardlabs_sdk-0.1.0.tar.gz 27.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hardlabs-sdk 0.1.0
File Interpreter ABI Platform
hardlabs_sdk-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 52.7 kB

Release files / hardlabs_sdk-0.1.0.tar.gz

Download URL hardlabs_sdk-0.1.0.tar.gz
Size 27.0 kB
Tags Source
SHA-256 checksum
How to use checksums
428dff13daed3a6fd63185ef10eac04dbc14cb63f8b53d514b63b2fa208975b2
BLAKE2b-256 checksum
How to use checksums
5a6225c5ff82e817c44e7d44e4f68ab4c67a4cdfbabcb03d7da0615bfd03b9b9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.5

Release files / hardlabs_sdk-0.1.0-py3-none-any.whl

Download URL hardlabs_sdk-0.1.0-py3-none-any.whl
Size 25.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b7d2e168782cc282e157a51f3a807588df95bd7d7c3360a6fb429ca9e49fea13
BLAKE2b-256 checksum
How to use checksums
a17579baebbeaad6f9c00b3f15a0979a8e9ac29bce772d1e9b43ae88470e67ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.5

Security for hardlabs-sdk 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page