This release is a pre-release and may not be stable for production use.
Wisent Wire Python SDK
Python client library for the Wisent Wire hardware testing platform. A single typed client wraps the whole REST API — device control, firmware flashing, on-target debugging, serial and GPIO I/O, telemetry, reservations, and organization management — with automatic auth, typed responses, typed errors, and convenience pollers for asynchronous operations.
Installation
pip install wisent-wire-sdk
Quick start
from wisentwire import WisentWireClient
# API key (recommended — scripts, CI, MCP)
client = WisentWireClient(
api_key="wwk_...", # or set WISENT_WIRE_API_KEY and omit api_key
)
# Cognito (email + password)
client = WisentWireClient(
email="user@company.com",
password="secret",
)
# Nothing passed at all — everything comes from the environment
client = WisentWireClient()
Create an API key from the web app under Settings → API keys. Keys are shown once — store them securely. Default expiry is 1 year; up to 10 active keys per user.
Auth resolution precedence: explicit argument > environment, and across methods
api_key > email + password.
Environment variables
Every setting can be supplied ambiently, so the same script runs unchanged
wherever it is pointed. All names use the WISENT_WIRE_* spelling.
| Variable | Purpose |
|---|---|
WISENT_WIRE_API_KEY |
API key (wwk_…). Recommended. |
WISENT_WIRE_EMAIL + WISENT_WIRE_PASSWORD |
Cognito login. Both are required. |
WISENT_WIRE_URL |
API base URL. Omit it to target production. |
WISENTWIRE_API_KEY (no underscore) is still read as a deprecated alias and
warns on use — it is the one no-underscore name with released history.
WISENT_WIRE_ENV, WISENTWIRE_ENV and WISENTWIRE_URL are gone, and setting
any of them now raises rather than being ignored. Ignoring them would not
fail: the client would fall back to production, so a stale export would quietly
send an int credential at prod. Use WISENT_WIRE_URL with the full address.
An exported-but-empty variable counts as unset, so a stray export WISENT_WIRE_API_KEY= in a shell profile falls through to the next method
rather than sending an empty credential.
The MCP server reads nothing of its own — it hands the whole job to this client, so one exported environment configures both packages identically.
Choosing a backend
base_url is optional and defaults to production. Give it an address to target
anything else:
client = WisentWireClient("https://int.wisent-wire.com", api_key="wwk_...")
client = WisentWireClient("http://localhost:7998", api_key="wwk_...")
Or set it ambiently, so the same script runs against any backend unchanged:
export WISENT_WIRE_URL=https://int.wisent-wire.com
Resolution precedence: base_url= > $WISENT_WIRE_URL > production
(https://app.wisent-wire.com).
The target is an address, never a name. There was a named-environment form
(env="int", WISENT_WIRE_ENV) and it is gone. The abstraction could not reach
the case it most needed to cover — a local backend has a per-developer port, so
it can never be one of the names — which meant an address form had to exist
regardless. Two ways of saying the same thing is a second thing to keep in sync,
and it was the second thing that drifted.
Features
- Wires — list, inspect, create, update, delete wires; configure per-bus communications (UART / RS485 / CAN); check availability.
- Power supply — set state / voltage / current, read the device shadow, toggle and read power telemetry.
- Firmware & flashing — list, upload, and delete firmware; dispatch a flash job, poll its status by id, stream flasher logs.
- Flasher catalog — list programmers, targets, interfaces, toolchains, and the valid launch-command combinations.
- Debug (GDB) — start / stop an on-target GDB server, read live session state, poll the dispatched job by id.
- Console (UART / RS485 / CAN) — send hex or ASCII on a byte-stream bus, send structured CAN frames, read the per-bus console, wait for an RX match.
- GPIO — read the shadow, drive outputs, set input pull policy, name pins, toggle and read input telemetry.
- Device registry — list registered and connected USB devices, register, update and remove devices, trigger a device scan.
- Frame definitions — CRUD for protocol frame definitions and their commands.
- Reservations — list a week's reservations, inspect, create, and delete them.
- Organization — create and read the org, manage members and roles, invitations, and membership requests.
- Account & system — read the authenticated user, check auth, and report the backend version and deployment stage.
- Typed errors & pollers — every non-2xx maps to a
WisentWireErrorsubclass;wait_*helpers poll asynchronous jobs with a configurable timeout.
Usage
Wires
wires = client.list_wisentwires()
ww = client.get_wisentwire(ww_id=1)
print(ww.name, ww.is_virtual, ww.status, ww.connected)
# Per-bus communications — one entry per configured bus
from wisentwire import CanCommunications, UartCommunications
client.update_wisentwire(
ww_id=1,
name="bench-1",
communications=[
UartCommunications(baud_rate=115200),
CanCommunications(bitrate=500_000, termination=True),
],
)
print(ww.uart, ww.can) # convenience accessors; None when not configured
communications=None keeps the current config; an empty list clears all buses.
Power supply
A wisent wire can carry more than one supply, so every call names which one by its registered-device tag.
# Which supplies will accept a write, and what each reports
for psu in client.list_power_supplies(ww_id=1):
print(psu.tag, psu.name)
tag = client.power_supply_tags(ww_id=1)[0]
client.set_power_supply(1, tag, state="ON", voltage=5.0, current=2.0)
entry = client.wait_shadow_state(1, tag, "ON")
print(entry.reported.voltage_setpoint)
client.set_power_supply_telemetry(ww_id=1, enabled=True)
readings = client.get_power_telemetry(ww_id=1) # {tag: [points]}
Take the tag from list_power_supplies / power_supply_tags, not from
get_shadow. They answer different questions: the backend accepts a write only
for a supply the device registry says is plugged in, while the shadow keeps
reporting one after it has been unplugged — so a tag read from the shadow can
look fine and be refused by every write with 404 No power supply <tag> is plugged into this wisent wire.
get_shadow is still how you read state, and on a wire with exactly one supply
.only() hands back (tag, entry) without you having to know its name:
tag, entry = client.get_shadow(ww_id=1).only() # raises, naming the tags, if not exactly one
print(entry.reported.state)
Firmware & flashing
# Upload firmware (one call: presigned URL -> S3 -> confirm)
fw = client.upload_firmware("app.bin", firmware_bytes)
# Resolve a valid (target, programmer, interface, toolchain) combination
cmd = client.list_launch_commands()[0]
# Dispatch a flash job and wait for it by job id
dispatch = client.flash(
ww_id=1,
firmware_id=fw.id,
target_id=cmd.target_id,
programmer_id=cmd.programmer_id,
interface_id=cmd.interface_id,
toolchain_id=cmd.toolchain_id,
)
status = client.wait_flash_complete(ww_id=1, job_id=dispatch.job_id)
print(status.status)
for entry in client.get_flasher_logs(ww_id=1):
print(entry.source, entry.data)
# Manage stored binaries
binaries = client.list_firmware()
client.delete_firmware(firmware_id=fw.id)
upload_firmware wraps the three-step flow; request_upload_url and
confirm_firmware are available if you need to drive the S3 PUT yourself.
Debug (GDB)
cmd = client.list_launch_commands()[0]
dispatch = client.start_debug(
ww_id=1,
target_id=cmd.target_id,
programmer_id=cmd.programmer_id,
interface_id=cmd.interface_id,
toolchain_id=cmd.toolchain_id,
port=3333,
)
client.wait_debug_complete(ww_id=1, job_id=dispatch.job_id)
state = client.get_debug_state(ww_id=1)
print(state.active, state.local_ip, state.port)
client.stop_debug(ww_id=1)
Console (UART / RS485 / CAN)
client.send_uart_ascii(ww_id=1, text="HELLO")
msg = client.wait_for_uart_rx(ww_id=1, match_hex="48454C4C4F")
print(msg.direction, msg.protocol, msg.bytes_hex)
Byte-stream buses (uart, rs485) take raw hex; CAN is frame-oriented:
from wisentwire import CanFrame
client.send_bus(ww_id=1, bus="rs485", hex_data="DEADBEEF")
client.send_can_frame(ww_id=1, frame=CanFrame(id="0x100", data_hex="0102030405060708"))
for msg in client.get_bus_messages(ww_id=1, bus="can"):
print(msg.can_id, msg.dlc, msg.bytes_hex)
msg = client.wait_for_bus_rx(ww_id=1, bus="can", match_hex="0102")
On CAN messages bytes_hex holds the frame's data bytes and the can_id /
ext / fd / rtr / dlc fields are populated; they are None for
uart/rs485.
GPIO
from wisentwire import GpioPullPolicy
shadow = client.get_gpio_shadow(ww_id=1)
client.set_gpio_output(ww_id=1, pin=0, is_high=True)
client.set_gpio_input_pull_policy(ww_id=1, pin=0, pull_policy=GpioPullPolicy.UP)
# Friendly pin labels
names = client.get_gpio_names(ww_id=1)
names.outputs[0] = "LED"
client.update_gpio_names(ww_id=1, names=names)
# Input telemetry
client.set_gpio_telemetry(ww_id=1, enabled=True)
for r in client.get_gpio_telemetry(ww_id=1):
print(r.pin, r.high, r.timestamp_micros)
Device registry
from wisentwire import DeviceLabel
client.trigger_device_scan(ww_id=1)
connected = client.list_connected_devices(ww_id=1)
devices = client.list_registered_devices() # optional label= filter
device = client.create_registered_device(
name="bench PSU",
label=DeviceLabel.POWER_SUPPLY,
vendor_id="0483",
product_id="5740",
serial_short="A1B2C3",
)
client.delete_registered_device(device_id=device.id)
Frame definitions
frames = client.list_frame_definitions()
commands = client.list_commands(frame_def_id=frames[0].id)
Reservations
list_reservations is scoped to one week — pass the week's start date:
reservations = client.list_reservations(week_start="2026-06-15")
booking = client.create_reservation(
ww_id=1,
start_at="2026-06-20T09:00:00Z",
end_at="2026-06-20T10:00:00Z",
)
client.delete_reservation(reservation_id=booking.id)
Organization
org = client.get_organization()
members = client.list_members()
client.create_invitation(email="new@company.com")
client.update_member_role(user_id=7, role="ADMIN")
Account & system
me = client.me()
print(me.email, me.organization)
version = client.get_version()
print(version.version, version.stage) # confirm which backend you hit
Error handling
Every API error raises a typed exception inheriting from WisentWireError:
from wisentwire import NotFoundError
try:
client.get_wisentwire(ww_id=999)
except NotFoundError as e:
print(f"Wire not found: {e}")
| HTTP status | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | ForbiddenError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 429 | RateLimitError |
| 503 | ServiceUnavailableError |
Any other non-2xx status raises the base WisentWireError. Each exception
carries .status and the parsed ProblemDetail body as .detail.
The job pollers raise their own exceptions when a job reaches a terminal
failure status (FAILED, TIMED_OUT, REJECTED, REMOVED, CANCELED).
These signal device-side failure rather than an HTTP error, but they still
inherit from WisentWireError, so a single except WisentWireError catches
both transport and device-side failures:
from wisentwire import FlashFailedError
try:
client.wait_flash_complete(ww_id=1, job_id=dispatch.job_id, timeout=120)
except FlashFailedError as e:
print(f"Flash failed on device: {e}")
wait_debug_complete raises DebugFailedError the same way. Every wait_*
helper takes a keyword-only timeout (seconds) and re-raises the last error
when it expires.
Development
pip install -e ".[dev]"
pytest tests/ -v
Requirements
- Python >= 3.10
requests>= 2.28tenacity>= 8.0
Release files for wisent-wire-sdk 0.12.3rc1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| wisent_wire_sdk-0.12.3rc1.tar.gz | 62.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wisent_wire_sdk-0.12.3rc1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 121.4 kB
Release files / wisent_wire_sdk-0.12.3rc1.tar.gz
| Download URL | wisent_wire_sdk-0.12.3rc1.tar.gz |
|---|---|
| Size | 62.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2479efdd509b0715d94353293e80a9db3d50a4d77930f30b307330e4667a4bf5
|
|
BLAKE2b-256 checksum How to use checksums |
b62bc49e89430cef63aaaee6c0aa76b111256bd342b6f7a7f4bf6cbbe2cb9b43
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / wisent_wire_sdk-0.12.3rc1-py3-none-any.whl
| Download URL | wisent_wire_sdk-0.12.3rc1-py3-none-any.whl |
|---|---|
| Size | 59.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
59248bb2b1140bcca6090fd56747180674653d76b6fcd7864a74b7f5806bc7e4
|
|
BLAKE2b-256 checksum How to use checksums |
18b599a7f223529a01e7a67e5de41d0b919acf3fe77a49433124d8f1c8d50f37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|