Template for a machine edge service in Python (PUDA CLI v0.1.2, Python SDK 0.0.18). Use this to scaffold a new machine integration.
edge-python-template/
├── pyproject.toml # Python project dependencies
├── uv.lock # Locked dependency versions (uv)
├── main.py # Main edge service — NATS + driver instance
├── driver.py # Machine driver: @command methods and @tlm_stream telemetry
├── Dockerfile # Container build
├── compose.yml # Docker Compose
├── start_edge.bat # Windows launcher script
├── .env.example # Environment variable template
├── .dockerignore # Docker build context exclusions
└── .gitignore # Git ignore rules
Follow these steps in order to adapt this template for a new machine.
Pick a short, lowercase, hyphen-separated identifier for the machine and edit the .env file. This becomes MACHINE_ID everywhere.
Search the repo for every TODO marker and resolve each one — typically renaming placeholder strings, filling in package metadata, and updating import paths to match the chosen MACHINE_ID.
Add the machine driver source in driver.py. The driver must expose a class that puda.EdgeRunner can wrap.
- Require
puda>=0.0.21inpyproject.toml(already set in this template). Add other hardware dependencies as needed. - Put a one-sentence summary in the class docstring.
puda machine listandpuda machine infoshow that first paragraph asdescription. - Decorate every remotely callable method with
@command. Undecorated methods are not advertised or callable. A driver with no@commandmethods fails at startup. - Parameters should be JSON primitives (
str,int,float,bool,dict,list) rather than custom class instances. - Keep helpers private (
_name). Raise on hardware failure. ReturningFalseis still a successful PUDA response ({"result": false}). - Attach
@safetyto commands that can cause harm. It is published in the catalog; it does not block edge dispatch, so enforce hard preconditions in the method body by raising. Seemove_toindriver.py(confirm=Trueso the agent prompts the operator before the move). Omit@safetyon read-only commands. - Mark methods that return live readings with
@tlm_stream(interval=...), e.g. position, weight, or temperature.EdgeRunnercalls each one everyintervalseconds in a separate worker thread and publishes the returned dict topuda.<MACHINE_ID>.tlm.stream.<name>. Streams can run at the same time as commands. ReturnNoneto skip a sample. Remove the exampleget_positionstream if the machine has no position. - Return machine-level state (homed flag, loaded labware, current mode) from the
@machine_statemethod. Its dict is merged into every MACHINE_STATE update read bypuda machine state. At most one method may be marked.- It is called on each state change (startup, command start and end, errors, shutdown), not on a timer, so values that change outside commands belong in a
@tlm_stream. - It runs on the edge's event loop. Return values the driver already has in memory; a slow device read here stalls commands and telemetry.
- Don't return
state,run_id, ortimestamp; PUDA sets those.
- It is called on each state change (startup, command start and end, errors, shutdown), not on a timer, so values that change outside commands belong in a
In .env, add any new config fields below MACHINE_ID (e.g. ${MACHINE_ID}_PORT, ${MACHINE_ID}_HOST). Keep NATS_SERVERS and MACHINE_ID as-is.
In main.py:
- Add any driver-specific environment variables to
Config(e.g. device port, IP address). - Instantiate the driver using those config fields.
EdgeRunner publishes the heartbeat, host health (CPU, memory, temperature), every @tlm_stream, and the @machine_state fields on its own, so main.py only needs config and wiring.
In Dockerfile, add any system-level dependencies your driver needs (e.g. libusb-dev for USB devices).
Replace this file with a machine-specific README describing what the machine does, how to connect to it, and any hardware prerequisites.
From repo root:
cp .env.example .envEdit .env and fill in:
MACHINE_ID— machine identifierNATS_SERVERS— comma-separated NATS server URLsPUDA_NATS_CAandPUDA_E2E_KEY_FILE— optional CA and shared key file paths when NATS E2EE is set up- Any additional driver-specific variables
All commands below run from repo root.
For NATS E2EE, set both PUDA_NATS_CA and PUDA_E2E_KEY_FILE in
.env to existing absolute paths on the local machine, and use a tls://
NATS server URL. Compose mounts those files and supplies their container paths
to the SDK. The CA is read-only; the key is writable to persist key rotations.
Keep these files outside the Docker build context. Leave both variables empty
or commented out for plaintext nats:// connections (unused /dev/null
mounts are used in that case). TLS-only connections use the CA without a key;
payload encryption can also be configured independently. The SDK validates
configured files before connecting.
The template passes paths directly into EdgeNatsClient, using SDK 0.0.21
from PyPI. Run uv sync --locked to install the locked dependencies.
Build and start:
docker compose -f compose.yml up -d --buildView logs:
docker compose -f compose.yml logs -fStop:
docker compose -f compose.yml downuv sync
uv run python main.pyWith the edge running, check that it advertises its description and telemetry streams, then watch one:
puda machine info <MACHINE_ID>
puda machine watch 'puda.<MACHINE_ID>.tlm.stream.>'Login:
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdinBuild and push:
docker compose -f compose.yml build
docker compose -f compose.yml push- Docker build context is the repository root.
- Dockerfile path is
Dockerfile. MACHINE_IDin.envis used for NATS subject routing and Docker image/container naming.