Skip to content

Standalone nodes

Constantly adding and running nodes through the node stack is highly inefficient during development. In that scenario, you want to be able to run your node as a regular Rust/Python program and use your favorite IDE to debug it.

While debugging the nodes based on the logs can be quite helpful, nothing beats the ability to fire up the debugger to inspect the code that is supposed to run inside the peppy node stack. To support this, peppy can run a node in “standalone mode”—it communicates with other nodes in the stack but runs as a regular program outside of it, allowing you to use standard debugging tools. Let’s create a new node:

  1. Initialize the node:

    Terminal window
    peppy node init --toolchain uv standalone
  2. Navigate into the directory:

    Terminal window
    cd standalone

with the following configuration:

peppy.json5
{
schema_version: 1,
manifest: {
name: "standalone",
tag: "0.1.0",
},
interfaces: {},
execution: {
language: "python",
// A bunch of fake parameters required to start our node
parameters: {
device: {
physical: "string",
sim: "string",
priority: "string"
},
video: {
frame_rate: "u16",
resolution: {
width: "u16",
height: "u16",
},
encoding: "string",
},
},
build_cmd: [
"uv",
"sync"
],
run_cmd: [
"uv",
"run",
"standalone"
]
}
}

Now sync the node:

Terminal window
peppy node sync

(You can also pass --sync/-s to peppy node add if you want to sync and add in one step — e.g. peppy node add . -sb to sync, add, and build together.)

Now if you try to run the node:

Terminal window
uv run standalone

You’ll run into the following error:

Terminal window
RuntimeError: missing required parameter(s) for standalone mode: device, video. Provide them via StandaloneConfig().with_parameters()

These parameters are usually provided during peppy node run, but since we want this node to run as a standalone program, we need to pass them outside of the peppy daemon environment. We can define our parameters in a params.json file at the root of the project:

params.json
{
"device": {
"physical": "/dev/video0",
"sim": "virtual_camera",
"priority": "high"
},
"video": {
"frame_rate": 30,
"resolution": {
"width": 1920,
"height": 1080
},
"encoding": "h264"
}
}

Then modify the source file to read from this file:

src/standalone/__main__.py
import json
from peppygen import NodeBuilder, NodeRunner, StandaloneConfig
from peppygen.parameters import Parameters
async def setup(params: Parameters, node_runner: NodeRunner):
print("Inside the setup callback!")
def main():
# Parameters can also be defined directly in code:
#
# from peppygen.parameters import Device, Video, VideoResolution
#
# params = Parameters(
# device=Device(
# physical="/dev/video0",
# sim="virtual_camera",
# priority="high",
# ),
# video=Video(
# frame_rate=30,
# resolution=VideoResolution(
# width=1920,
# height=1080,
# ),
# encoding="h264",
# ),
# )
with open("params.json") as f:
params = json.load(f)
standalone_config = StandaloneConfig().with_parameters(params)
NodeBuilder().standalone(standalone_config).run(setup)
if __name__ == "__main__":
main()

Now if we run the following command again:

Terminal window
uv run standalone

The node should run without a crash. The standalone object allows us to load parameters from an external JSON file and pass them to the node, which in turn allows us to run our node as a regular Rust/Python program. Note that the standalone config is completely ignored when a node is run with peppy node run, all parameters provided during the node run operation take precedence.

You can also run a variant as a standalone program. Variants typically omit the manifest and interfaces sections of peppy.json5 and inherit them from their parent root node. When you launch a variant directly with cargo run / uv run, peppy walks up the directory tree to find the nearest ancestor peppy.json5 that declares a manifest and merges the two configs — the variant’s execution wins, the parent’s manifest and interfaces are adopted as-is.

This means you can sit inside a variant subdirectory (typically variants/<name>/) and debug exactly what the daemon would run when that variant is selected:

uvc_camera/
├── peppy.json5 ← root node (manifest + interfaces, no execution)
└── variants/
├── default/
│ └── peppy.json5 ← execution only
└── mock/
├── peppy.json5 ← execution only
├── params.json
└── src/
└── main.rs
Terminal window
cd uvc_camera/variants/mock
cargo run # or `uv run mock` for Python

When standalone mode initializes, peppy loads the variant’s peppy.json5, walks up to find the parent root, and produces a merged NodeConfig where the node is known as uvc_camera:0.1.0 (inherited from the root manifest) with the variant’s own execution (including parameters). The standalone messenger then uses those merged interfaces to talk to the rest of the stack, just like a non-variant standalone node. Cargo / uv remain in charge of actually running the binary — run_cmd is only consulted by the daemon when the node is launched through peppy node run, not in standalone mode.