Skip to main content

Embeint HTF Station

An extensible Python runtime for Embeint Hardware Test Framework (HTF) stations. It subscribes to commands over MQTT, drives programmers and DUTs, and reports logs and results to a compatible HTF server.

The server and web UI are separate private deployments. This repository contains the station runtime, sample stations, and the public MQTT wire contract only.

Install

uv add embeint-htf-station

For a development checkout:

uv sync --all-groups
uv run pytest

Compatibility

The MQTT contract is versioned in protocol/asyncapi.yaml. Station releases use semantic versioning and are tagged as vX.Y.Z.

  • Patch releases do not change the wire contract.
  • Minor releases may add backwards-compatible fields or messages.
  • Major releases may require a compatible server upgrade.

Pin the station version in an HTF workspace with the west.yml manifest supplied by the private server repository. Application projects should depend on released package versions through uv/PyPI; use an editable dependency only while developing the runtime itself.

Publishing a release

The package is published to PyPI from vX.Y.Z tags on the long-lived release branch. The tag must match project.version in pyproject.toml, and its commit must be reachable from release. CI runs on each release-branch push; the tag workflow reruns checks before building a wheel and source distribution. Only its separate publish job can request a PyPI identity token.

Before the first release, protect the release branch and v* tags, require CI for branch changes, and configure the GitHub pypi environment with required reviewers and deployment limited to v* tags. Add a PyPI Trusted Publisher for owner Embeint, repository embeint-htf-station, workflow publish.yml, and environment pypi. No long-lived PyPI API token is needed.

Create release from tested main once, then advance it with subsequent changes from main. Update project.version for each release and apply any release-only fixes there. Once release-branch CI passes, tag its tip and push the tag. For the first release:

git switch -c release origin/main
git push -u origin release
git tag -a v0.2.0 -m v0.2.0
git push origin v0.2.0

Repeat with new versions and tags along the same branch. PyPI distributions cannot be replaced under the same version. Pin station installations to an exact version and upgrade deliberately. A GitHub Release can be created from the published tag for release notes; creating it does not trigger PyPI upload.

Layout

src/embeint_htf_station/
├── cli.py                    # Click entry points
├── config.py                 # Pydantic settings
├── messaging/                # MQTT client and batched logs
├── stages/                   # Reusable stage implementations
├── stations/                 # Station runtimes
└── contracts/                # Generated from protocol/asyncapi.yaml

Samples

cp samples/basic-station/.env.example samples/basic-station/.env
uv run python samples/basic-station/main.py DUT-001

Configuration reads identity and credentials from environment variables, so secrets remain outside source control. Library-provided stages live under src/embeint_htf_station/stages/; samples can register their own stage factories.

The htf-station run command only maintains a broker heartbeat; use a sample main.py as the starting point for a station that executes test plans.

Station-specific task secrets can be configured in the HTF operator UI. With HTF_API_KEY set, the station pulls them from the server at startup and after reconnecting, then exposes them to built-in and custom stages through context.require_secret("INFUSE_API_KEY") or the read-only context.secrets mapping. The runtime keeps the values in memory and does not add them to run outputs or logs. A failed authenticated pull stops station startup instead of running with an old value. Restart or reconnect after changing a secret.

Station credentials

Create a station from the HTF operator UI and save the one-time .env output. Each station has its own MQTT username/password and API key:

HTF_MQTT_USERNAME=station-...
HTF_MQTT_PASSWORD=st_mqtt_...
HTF_API_KEY=st_api_...

Never reuse these values between stations. MQTT credentials are restricted by the broker to the station's own heartbeat, stage, log, and result topics, plus its command subscription. Rotate a credential in the operator UI after exposure; revoked credentials cannot reconnect or access station APIs.

MQTT session takeover detection

The broker requires the provisioned MQTT username as the station's MQTT client ID. MQTT permits only one active connection for a client ID, so another connection using the same provisioned credentials will disconnect the active station session. This can be caused by a network or broker fault as well as credential reuse; MQTT does not expose enough information to distinguish them reliably.

An established session that ends unexpectedly emits a broker.disconnected warning with possible_session_takeover=true. Treat that event as a credential-exposure signal: investigate the broker and network logs, then rotate the station credential in the operator UI when reuse cannot be ruled out.

Contract generation

The generated MQTT models are committed to the package. Regenerate them after changing the AsyncAPI contract:

./scripts/gen-mqtt-contracts.sh
git diff --exit-code -- src/embeint_htf_station/contracts

MQTT TLS and station certificates

New configurations default to verified TLS on port 8883. The production endpoint is mqtt.app.embeint-htf.com:8883 and requires a unique station client certificate and the station MQTT username/password. Download the one-time provisioning bundle from the HTF station setup page. Keep its private key readable only by the station service account (for example chmod 600 client.key). Never commit it.

mqtt:
  transport: mtls
  host: mqtt.app.embeint-htf.com
  port: 8883
  client_cert: ./credentials/client.pem
  client_key: ./credentials/client.key
  # Optional custom broker trust; omit to use the system public CA store.
  # ca_cert: ./credentials/broker-ca.pem
station:
  org_id: ${HTF_ORG_ID}
  station_id: ${HTF_STATION_ID}

HTF_MQTT_TRANSPORT, HTF_MQTT_HOST, HTF_MQTT_PORT, HTF_MQTT_USERNAME, HTF_MQTT_PASSWORD, HTF_MQTT_CA_CERT, HTF_MQTT_CLIENT_CERT, and HTF_MQTT_CLIENT_KEY override their YAML values. Adjacent .env values fill missing process environment variables. Certificate paths in YAML or its overrides resolve relative to the YAML file. With the environment-only CLI, paths resolve relative to the working directory. Legacy HTF_BROKER_* names remain supported by the environment-only CLI; prefer HTF_MQTT_* for new installations.

Modes are tls (verified server TLS, optional paired client certificate/key), mtls (requires both client files), and plaintext (explicit development only). Local samples select plaintext; set both transport and port when switching an existing sample to production. For an environment-only local development run, set HTF_MQTT_TRANSPORT=plaintext and HTF_MQTT_PORT=1883. There is no insecure TLS mode. A client trust CA issued for stations is not necessarily the CA that signed the broker certificate.

Missing/unreadable PEM files and mismatched/encrypted keys stop startup before MQTT connects. TLS handshake failure prevents the station from accepting runs. For rejected connections check the broker hostname, system clock, broker CA chain, client chain/expiry and MQTT credentials. Do not share keys or passwords in support logs. To rotate, replace the certificate/key pair atomically while the station is stopped, restart to load them, verify a heartbeat, then revoke the old certificate. Existing contexts are not changed by overwriting files.

Automatic renewal

Updated HTF servers support station-owned automatic certificate renewal. It is enabled by default for mtls; set HTF_MQTT_AUTO_RENEW=false to opt out. Keep HTF_API_KEY and a verified HTTPS HTF_API_BASE_URL configured. The station does not need an OpenBao token. Initial certificate installation is still manual.

The runtime checks hourly while idle and renews seven days before expiry (or in the last third of a shorter certificate lifetime). Active and queued jobs defer renewal, so allow an idle interval before expiry. It generates a fresh private key locally, proves possession of its current key, and requests a replacement through the HTF API. Network failures retry with bounded backoff without discarding the current pair. Revoked/expired certificates require admin recovery.

The certificate's parent directory must be writable by the station account. The private .htf-mtls-<station UUID> directory stores pending requests and versioned key/certificate pairs, using owner-only permissions on POSIX. Keep this directory persistent across restarts, private, and out of Git/support logs. An atomic pointer activates the replacement and MQTT reconnects between jobs. The server retires the old certificate after the overlap (normally 24 hours). Original enrollment files are preserved. For manual re-enrollment, stop the station and securely archive the old renewal directory before installing the new bundle. Do not run two station processes with the same identity.

Look for station.certificate.renewed or station.certificate.renewal_failed in station logs. The GUI shows the new certificate's expiry and the old one's retirement time. Older server deployments will reject renewal until upgraded; do not assume installing only the station update enables end-to-end renewal.

Run uv run pytest for unit tests. Run HTF_TLS_INTEGRATION=1 uv run pytest tests/test_tls_integration.py with Docker available for real Mosquitto mTLS tests (also required by CI).

A complete production transport sample is in samples/production-mtls/config.yaml. After installing the credentials and setting station environment variables, run uv run python samples/basic-station/main.py --listen --config samples/production-mtls/config.yaml.

Project ID pools

Project variables use uploaded CSV pools. Configure HTF_API_KEY and HTF_API_BASE_URL, then reserve values, use them, and explicitly commit after all required work succeeds:

stages:
  - name: Reserve IDs
    kind: reserve_variables
    variables: [infuse_id, serial_number]
    record_version: v2

  - name: Show ID
    kind: print
    message: 'ID=${provisioning.infuse_id}'
    wait_seconds: 0

  - name: Register with external service
    kind: http_request
    http:
      url: https://manufacturing.example.com/devices
      method: POST
      headers:
        Authorization: 'Bearer ${MANUFACTURING_TOKEN}'
      json:
        dut_id: '${dut_id}'
        infuse_id: '${provisioning.infuse_id}'
        serial_number: '${provisioning.serial_number}'
      timeout_seconds: 30
      expected_statuses: [200, 201]
      outputs:
        external.receipt: receipt.id

  - name: Program reserved ID
    kind: infuse_provisioning
    provisioning_source: id_pool
    programmer: jlink_1
    uicr:
      - name: infuse_id
        source: context
        value: provisioning.infuse_id
        bytes: 8
        endian: LSB

  - name: Commit IDs
    kind: commit_variables
    variables: [infuse_id, serial_number]

Replace the example service URL and response mapping with your service contract. jlink_1 must be a local programmer with a target device that defines its UICR address; alternatively supply an explicit UICR address. Hardware programming is optional: use any required stages between reservation and commit. The same stage syntax works inside named lane plans.

reserve_variables (also available as allocate_variables) only reserves. Reservations are exclusive to the project/DUT/variable and never expire or release automatically. The server handles concurrent stations atomically. Re-requesting the same DUT returns its reserved or committed values, even if the pool is otherwise empty. Additional variables in a later configuration version are reserved without replacing existing committed values. Values stay strings, including leading zeros.

Reservation writes provisioning.<variable> and reservation.<variable> into this run's context. ${provisioning.infuse_id}, ${external.receipt}, ${dut_id}, and ${run_id} can be used in string settings, including nested HTTP JSON and headers. ${context.provisioning.infuse_id} is an equivalent explicit context reference. References resolve once immediately before each stage; a missing reference fails the stage. Stage names, kinds, programmer routing, variable lists, locks and dependency declarations remain static. Numeric configuration fields such as timeout and width must be literal numbers. Uppercase ${ENV_VAR} is the existing configuration-time environment expansion; runtime values use lowercase/dotted names. Python stages can still use context.get_output_value("provisioning.infuse_id").

http_request supports GET/POST/PUT/PATCH/DELETE, explicit headers, optional JSON, timeout up to 120 seconds, expected status codes, and response outputs mapped from dotted JSON paths (including array indexes). Outputs must be strings or integers; response JSON is limited to 1 MiB when extracting outputs. The station credential is never automatically sent to an external service. Requests do not follow redirects or retry automatically. The stage supplies an Idempotency-Key derived from DUT, stage name and reservation IDs unless you provide one. It is stable across retries for the same reservations, and changes after explicit release/re-reservation. Your service must implement idempotency for this to prevent duplicate side effects; keep the stage name, request and reserved variable set stable when retrying.

A timeout may mean the external service consumed the ID. The run fails and keeps the reservation. Reconcile the external outcome before rerunning side effects, committing, or releasing; never release just because a request timed out. After confirmed external success, an operator can use an approved recovery plan that reserves the same DUT, verifies completion and commits without repeating that side effect. Commit timeouts can be retried with the same reservation IDs.

Put commit_variables after every required operation. It commits exactly the listed variables using their reservation IDs and is retry-safe. A missing, released or replaced reservation is rejected, and a repeated commit preserves the original timestamp/station. Plans containing a commit stop after any failed stage. Re-reserving a variable within one run must return the same token and value; a changed pair fails the run before it can be committed. Commit also rejects failed prerequisites and changed context values. Every stage in a plan that reserves IDs checks its declared dependencies, including standalone runs. Same-lane dependencies use stages already completed in this run. A cross-lane dependency without a batch result fails before that stage runs, so registration cannot proceed without its verification prerequisite. Every dependency in a reservation plan must declare and reach passed; a declared failed outcome cannot authorize an external side effect or commit. Only declared cross-lane dependencies are checked; independent lanes keep their own contexts.

Admin release in the project's DUT records & IDs history is the only way to make a reserved or committed value reusable. History retains reservation, commit and release attribution. No failure, abort, disconnect or reimport clears an ID.

infuse_provisioning with provisioning_source: id_pool can also reserve its requested keys directly, but still needs a subsequent commit_variables stage. Explicit provisioning.hardware_id references select the pool even when a chip hardware ID exists. Context-only/literal UICR writes make no allocation request. The default infuse_api retains the existing Infuse workflow; it is a separate data source and does not automatically import legacy IDs into project pools.

Deploy the companion server and its reservation migration before this station change, and update pool plans to include explicit commit. Existing permanent assignments remain committed during migration. This change does not publish or deploy automatically.

License

Licensed under the Functional Source License, Version 1.1, ALv2 Future License (FSL-1.1-ALv2). See LICENSE.md for the full terms.

Release files for embeint-htf-station 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 embeint-htf-station 0.2.0
File Size Uploaded
embeint_htf_station-0.2.0.tar.gz 51.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for embeint-htf-station 0.2.0
File Interpreter ABI Platform
embeint_htf_station-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 120.8 kB

Release files / embeint_htf_station-0.2.0.tar.gz

Download URL embeint_htf_station-0.2.0.tar.gz
Size 51.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3b2ba53cce3d864e04fdd2148f9dd99b455044e128829f5a6804a2f8d4179b32
BLAKE2b-256 checksum
How to use checksums
3c79638ad89064659d51592d1db7d01fd57626c1916b860ba55f39455c5f4d58
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 Sep 26, 2026.

Transparency log

Release files / embeint_htf_station-0.2.0-py3-none-any.whl

Download URL embeint_htf_station-0.2.0-py3-none-any.whl
Size 69.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bfd0fd0a410c099aa95668942eb95dee6b44233d7f9643c06ef4b4714adc1130
BLAKE2b-256 checksum
How to use checksums
0e1964679cc2e87312920c6bb2e08122c97dbf1f5ea9d91429f4c5cc2fed1c80
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 Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

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