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.
  • Opening the port per command: pyserial's socket:// close waits 0.3 s before returning. For the bridge's own ports it doesn't, so a framework that opens and closes the port around every command runs at the board's pace. This holds where the bridge lives in the same process as your framework (the pytest fixtures below). A tool in another process, next to python -m hardlabs_sdk up, pays the 0.3 s each time it closes the port.
  • 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. Don't use it with a framework that calls reset_output_buffer() (or tcflush) before closing: on macOS that discards bytes the bridge hasn't read yet, so a command sent without waiting for a reply can be lost.
  • 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.2.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.2.0
File Size Uploaded Provenance
hardlabs_sdk-0.2.0.tar.gz 30.5 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 57.8 kB

Release files / hardlabs_sdk-0.2.0.tar.gz

Download URL hardlabs_sdk-0.2.0.tar.gz
Size 30.5 kB
Tags Source
SHA-256 checksum
How to use checksums
735dd8283f24c6e431b314d7574871613eabdb5dc3f91666069e365d6c87db13
BLAKE2b-256 checksum
How to use checksums
8d73bc6c40f620fc921247fd5b92886ee1d73a62578218c3cd97943c95db0495
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.2.0-py3-none-any.whl

Download URL hardlabs_sdk-0.2.0-py3-none-any.whl
Size 27.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c5394f650eb90fa662571b8a6895243f71d6ac56720e079d60621b89cbd87869
BLAKE2b-256 checksum
How to use checksums
c9e13ef9398f78143956433b339de267096181df427434594eb81d2d9f5c8e1e
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.2.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.2.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