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.
Debugging a node
Section titled “Debugging a node”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:
-
Initialize the node:
Terminal window peppy node init --toolchain uv standaloneTerminal window peppy node init --toolchain cargo standalone -
Navigate into the directory:
Terminal window cd standalone
with the following configuration:
{ 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" ] }}{ schema_version: 1, manifest: { name: "standalone", tag: "0.1.0", }, interfaces: {}, execution: { language: "rust", // 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: [ "cargo", "build", "--release" ], run_cmd: [ "./target/release/standalone" ] }}Now sync the node:
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:
uv run standaloneYou’ll run into the following error:
RuntimeError: missing required parameter(s) for standalone mode: device, video. Provide them via StandaloneConfig().with_parameters()cargo runYou’ll run into the following error:
Error: ParameterDeserialization(ParameterDeserializationError(["device", "video"]))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:
{ "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:
import json
from peppygen import NodeBuilder, NodeRunner, StandaloneConfigfrom 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:
uv run standaloneuse peppygen::{NodeBuilder, Parameters, Result};use peppylib::runtime::StandaloneConfig;
fn main() -> Result<()> { // Parameters can also be defined directly in code: // // use peppygen::parameters::{device::Device, video::{Video, VideoResolution}}; // // let params = Parameters { // device: Device { // physical: "/dev/video0".to_string(), // sim: "virtual_camera".to_string(), // priority: "high".to_string(), // }, // video: Video { // frame_rate: 30, // resolution: VideoResolution { // width: 1920, // height: 1080, // }, // encoding: "h264".to_string(), // }, // };
let json = std::fs::read_to_string("params.json") .expect("failed to read params.json"); let params: Parameters = serde_json::from_str(&json) .expect("failed to parse params.json");
let standalone_config = StandaloneConfig::new().with_parameters(¶ms); NodeBuilder::new() .standalone(standalone_config) .run(|args: Parameters, node_runner| async { println!("Inside the run closure!"); let _ = args; let _ = node_runner; Ok(()) })}Now if we run the following command again:
cargo runThe 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.
Running a variant in standalone mode
Section titled “Running a variant in standalone mode”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.rscd uvc_camera/variants/mockcargo run # or `uv run mock` for PythonWhen 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.