Skip to main content

Lockbox Python API

Python 3.11+ synchronous controls for the Lockbox server. Install from this directory with python -m pip install . (or python -m pip install -e ".[dev]" for development).

from lockbox import Server, SlopePreference

with Server("http://lockboxcontrol.internal:5106") as server:
    box = server.get_lockbox(1)  # Choose an ID from server.list_lockboxes().
    box.on_coarse_resonance_found(lambda: print("Coarse resonance found"))
    box.on_fine_resonance_found(lambda: print("Fine resonance found"))
    box.on_lock_acquired(server.stop)

    box.set_setpoint(0.0)
    box.set_low_high_threshold(-100, 100, SlopePreference.POSITIVE)
    box.lock()
    server.run_forever()  # Wait until lock is acquired, or press Ctrl+C.

Server owns one HTTP client and two SignalR connections: /Hubs/Status for events and /Hubs/Run for setpoint and threshold commands. It uses the server's JSON hub protocol. Entering the context connects both hubs; exiting closes all connections without changing the hardware. There are no reconnects or retries. Server(url, timeout=10.0) sets HTTP timeouts and the time allowed for hub handshake readiness and command completion, not for physical device operations. Supply the server root URL, including any hosting path prefix, without /api.

list_lockboxes() and get_lockbox(id) reuse objects by ID, preserving callbacks while refreshing their name, IP and MAC metadata. Objects are registered for status routing automatically. connect_to_status_hub(box) also registers manually created objects; repeating registration of the same object is harmless.

Keeping an experiment running

Call server.run_forever() inside the with block to keep the script alive. It blocks the calling thread while callbacks continue on the SignalR status receiver thread. Call server.stop() from a callback or another thread to return from the wait. Do not call run_forever() inside a callback.

stop() is idempotent and only stops waiting: it leaves connections open and does not change hardware state. Exiting the context closes connections. Ctrl+C propagates KeyboardInterrupt, so leaving the context also cleans up normally. The wait uses 0.1-second blocking intervals to stay responsive on Windows.

A stop received before run_forever() is remembered. Subsequent waits in the same context return immediately; re-entering the context resets this state. Calling run_forever() outside an active context raises RuntimeError. If either hub closes unexpectedly, the wait raises ConnectionError identifying the hub, including when the disconnect happened before the wait. A recorded disconnect takes precedence over stop(). Intentional context cleanup releases the wait without creating a disconnect error. There is no automatic reconnection.

Commands

Commands, except the boolean helpers below, return None after HTTP success or Run hub invocation completion. They do not wait for a status or physical completion. Failed HTTP responses raise LockboxHttpError with the method, path, HTTP status/reason, and response body in its message; hub errors use the server-provided error message. Transport errors propagate.

Method Argument convention
set_gain(gain) Input gain, at least 1
set_demodulation_cutoff(frequency) Hz
set_demodulation_phase(phase) Radians
set_demodulation_phase_deg(phase) Degrees; converted to radians before sending
set_modulation_amplitude(amplitude) Integer DAC codes, 0–524287
set_demodulation_amplitude(amplitude) Server amplitude scale, 0–100
set_setpoint(setpoint) Native device setpoint units
set_pid(p, i, d) Native device PID coefficients
set_threshold(threshold) Integer reflection threshold
set_low_high_threshold(low, high, slope_preference=SlopePreference.NONE) Integer error-signal limits; enum values NONE, NEGATIVE, POSITIVE
set_fine_output_code(code), set_coarse_output_code(code) Signed DAC codes, -524287–524287
lock(), unlock() Normal lock; unlock without starting a scan
stop(), go() Stop or run the device, matching GUI Stop/Start
coarse_scan_only(lower_bound=-524287, upper_bound=524287, period_ms=0) Coarse-only scan; bounds in DAC codes, period in milliseconds
fine_scan_only(lower_bound=-524287, upper_bound=524287, period_ms=0) Fine-only scan; bounds in DAC codes, period in milliseconds
freeze(), unfreeze() Both toggle freeze through the same endpoint
try_freeze(), try_unfreeze() Same toggle; True on success, False on HTTP 409; other errors propagate
reload_configuration() Apply the server's saved configuration

Setters change runtime values, not saved configuration. Validation is left to the server, except conversion of the slope enum. Slope preference is forwarded as the server enum value (0, 1, or 2). The current firmware ThresholdsEndpoint updates limits but does not read the slope query parameter; actual slope behavior depends on firmware support. This package does not change firmware.

box.stop() sends POST /api/Run/stop?teensyId={id} and box.go() sends POST /api/Run/start?teensyId={id}, both without a request body. They return on HTTP success and raise LockboxHttpError on failure. box.stop() stops the device; server.stop() only releases run_forever().

box.stop()
box.go()  # Same action as Start in the GUI.

Catch HTTP errors by status without parsing their message:

from lockbox import LockboxHttpError

try:
    box.go()
except LockboxHttpError as error:
    if error.status_code == 409:
        print("Lockbox is already running:", error)
    else:
        raise

For Stop, 409 means already stopped; for Go, it means already running. The updated server preserves the firmware status and message for these commands. Older servers may translate these errors into 400 or 500; Python reports the status it actually receives. error.body preserves the response text, and error.response exposes the original HTTP response. This replaces the previous body-only exception message, including for saved-configuration errors.

set_demodulation_phase() takes radians. Use the degrees helper when more convenient; both methods send the same runtime command:

import math

box.set_demodulation_phase(math.pi / 2)  # 90 degrees, expressed in radians.
box.set_demodulation_phase_deg(90)      # Equivalent command using degrees.

The freeze endpoint may translate a firmware rejection (including not-ready 409) into a server 500. The client reports the response it receives.

try_freeze() and try_unfreeze() return True on HTTP success and False only when the server responds with HTTP 409. Other HTTP errors and transport errors still raise exceptions, including a firmware conflict translated to 500 by the server. Both methods toggle freeze, just like freeze() and unfreeze(); they do not track frozen state or wait for a status callback.

if box.try_freeze():
    print("Freeze toggle accepted")
else:
    print("Freeze toggle rejected with HTTP 409")

Scan-only operation

Both scan methods use the GUI's POST /api/Run/scan endpoint and start a scan without attempting to acquire lock. A zero period leaves the device's scan period unchanged. All three scan parameters are sent so custom bounds are not discarded by the server when the period is omitted in Python.

box.coarse_scan_only()  # Full DAC range, existing scan period.
# Or select a fine-only scan with explicit bounds and timing:
box.fine_scan_only(lower_bound=-1000, upper_bound=1000, period_ms=2000)

These are runtime commands: they return on HTTP success, do not wait for the scan, and do not save configuration. Calling the second method switches the scan mode.

Callbacks

Register callbacks after entering the context. Each registration stores one zero-argument function; registering again replaces it.

Registration Incoming Arduino status
on_coarse_resonance_found(callback) CoarseResonanceFound
on_fine_resonance_found(callback) FineResonanceFound
on_lock_acquired(callback) Locked
on_lock_lost(callback) Unlocked
on_lockbox_frozen(callback) FrozenEnabled
on_lockbox_unfrozen(callback) FrozenDisabled
on_lockbox_freeze_ready(callback) DriftEstimateReady

on_lockbox_recentering raises NotImplementedError because no dedicated status exists. HTTP results never trigger callbacks. Unregistered devices and unrelated statuses are ignored.

on_lock_acquired requires firmware and server versions supporting Locked (status 26). It reports completed acquisition and entry into feedback control; it does not guarantee continued lock stability. Each successful reacquisition emits another event. FineResonanceFound remains a separate event and does not trigger this callback. Initial startup and freeze toggles do not emit Locked.

Functions run directly on the status receiver thread, without arguments or custom exception recovery. They may issue commands using the separate HTTP/Run connections. While a callback runs, it delays further status delivery: do not wait inside it for another callback. There is no replay, ordering contract, or experiment scheduler.

Saved configuration

Fetch an independent snapshot, edit its supported fields, and explicitly save it:

from lockbox import Server, PidControllerType, SlopePreference

with Server("http://lockboxcontrol.internal:5106") as server:
    box = server.get_lockbox(1)
    config = box.get_configuration()
    config.pid.proportional_gain = 2.0
    config.pid.controller_type = PidControllerType.FILTERED_PID
    config.demodulation.set_point = 0.1
    config.lock_thresholds.out_of_lock_threshold = 80
    config.lock_thresholds.ms_without_lock = 250
    config.lock_thresholds.delock_smoothing_constant = 0.25
    config.coarse_scan.scan_min = -2000
    config.coarse_scan.scan_max = 2000
    config.fine_scan.scan_time_ms = 1500
    if config.pdh is not None:
        config.pdh.low_limit = -100.5
        config.pdh.high_limit = 100.25
        config.pdh.slope_preference = SlopePreference.POSITIVE
    box.save_configuration(config)

    # Apply only when wanted:
    box.reload_configuration()

get_configuration() reads GET /api/LockboxConfigurations/arduino/{id} on every call. It returns a LockboxConfiguration with typed, snake_case sections: pid, demodulation, lock_thresholds, pdh, coarse_scan, fine_scan, coarse_refinement, fine_refinement, and top-level modulation_amplitude. An absent optional PDH section is None. Runtime setters do not modify snapshots. PidControllerType has SIMPLE_PI and FILTERED_PID; slope settings reuse SlopePreference.NONE, NEGATIVE, and POSITIVE.

All lock-threshold and coarse/fine scan settings are editable. IDs, device identity, section references, notch_filters, and filter_selections are read-only. Filter selections expose notch_filter_id, enabled, and the associated read-only notch_filter definition when present. Snapshots update existing records only; a missing configuration raises the normal HTTP error. They cannot create records or replace section identities.

save_configuration(config) returns None after these eight sequential writes (seven when PDH is absent):

Saved settings Endpoint
PID coefficients, controller type, derivative cutoff PUT /api/PidConfigurations/{pidId}/persist
Demodulation cutoff, phase, amplitude, setpoint PUT /api/DemodulationConfigurations/{id}/persist
Modulation amplitude PUT /api/PidConfigurations/{pidId}/modulation
All lock-threshold settings PUT /api/LockThresholdSettings/{id}
PDH low/high limits and slope, when present PUT /api/PdhSettings/{id}
Coarse scan limits and time PUT /api/ScanSettings/{coarseId}
Fine scan limits and time PUT /api/ScanSettings/{fineId}
Coarse and fine refinement PUT /api/PidConfigurations/{pidId}/refinement

Saving does not apply hardware settings or modify filters. It sends all supported values, including unchanged ones. Fetch again before editing if another client may have changed the saved configuration; there is no conflict detection.

The snapshot's device/server identity, required linked records, and payloads are checked before any writes, including both required scan records. Nonfinite or unserializable payloads are rejected before writing. PDH limits accept decimal values and are transmitted without integer conversion. Range and ordering validation is performed by the server. PDH is skipped only when both its section and linked ID are absent; a linked ID with a missing section is an error.

Saving is not atomic. A failed response stops subsequent requests and raises LockboxHttpError; earlier successful writes remain saved. There is no rollback or retry. Other server-side validation errors can therefore leave a partially saved configuration. The ordinary PID update endpoint is not used because it also applies changes to hardware.

This version requires the server's resource-style PUT endpoints for LockThresholdSettings, PdhSettings, and ScanSettings. There is no fallback to the old integer-only threshold-saving endpoint. Notch-filter editing remains excluded; definitions, selections, and enabled flags are preserved unchanged. Runtime setters still use their existing interfaces; fractional PDH support here applies to saved configuration.

Development

Run python -m pytest and python -m ruff check src tests from this directory. Tests use mock HTTP responses and hub connections; they do not operate hardware.

Metadata

Release files for bouwmeester-lockbox-api 0.1.6

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

Source distribution (sdist)

Source distribution for bouwmeester-lockbox-api 0.1.6
File Size Uploaded
bouwmeester_lockbox_api-0.1.6.tar.gz 34.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bouwmeester-lockbox-api 0.1.6
File Interpreter ABI Platform
bouwmeester_lockbox_api-0.1.6-py3-none-any.whl Python 3 none any Details

Total release size: 62.0 kB

Release files / bouwmeester_lockbox_api-0.1.6.tar.gz

Download URL bouwmeester_lockbox_api-0.1.6.tar.gz
Size 34.1 kB
Tags Source
SHA-256 checksum
How to use checksums
fe09a2cf72665378c7ede7c0f9bb4768dbd702d38068b0cbafa04b504ab9b91e
BLAKE2b-256 checksum
How to use checksums
6a61d0924fcee7d9d25facba86d1153ab54b3a8b93e55a2eb435b4dc3d503e77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / bouwmeester_lockbox_api-0.1.6-py3-none-any.whl

Download URL bouwmeester_lockbox_api-0.1.6-py3-none-any.whl
Size 27.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4592610741991235c10e867d9bef3eddc5eef412244dc67fe4fc431b2f26685c
BLAKE2b-256 checksum
How to use checksums
ae2072993a6cc6dd666cd11a5e84f7e97d07966833284d39a2945d220fb5518b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.6 This release

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

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