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 topython -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 atbus.path(/dev/ttys012) for tools that want one. Don't use it with a framework that callsreset_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 rs485boots 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:
ConsoleTimeoutsays which text didn't appear and shows what the console printed instead.SimulationStoppedmeans 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")isboard.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,controlsandreading_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) andconnected_refs.part.connected()gives the parts themselves, the processor first. connected_tothe 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hardlabs_sdk-0.2.0.tar.gz | 30.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.