Skip to content

Creating Your First Node

Now that you have PeppyOS installed, let’s create your first node. A node is the fundamental unit of computation in PeppyOS - it represents a runnable application or service. In this guide, we’ll explore how nodes work and how they communicate with each other.

You can think of nodes as compute units that are each responsible for a single job. For example, a node can:

  • Emit frames from a USB camera
  • Move a single actuator
  • Act as the brain of a robot by receiving input from sensors (audio, video, etc.) and emitting actions

The key point is that a node is responsible for performing a single function.

Run:

  1. Initialize the node:

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

    Terminal window
    cd hello_world

In the node folder, you’ll find the following:

  • A peppy.json5 file
  • A .peppy folder local to your node
  • Your project scaffolding based on the selected toolchain

This is the entry point and the single source of truth for every node. All node interfaces and communication are defined in the peppy.json5 configuration file, regardless of the programming language used. A developer who needs to understand or share a node can simply look at this configuration file and immediately know how it works without digging into the code. Everything in a peppy.json5 is meant to be shared and distributed, so nothing local to your system should be added to this file.

This is a cache folder that contains everything automatically generated by the peppy daemon. Nothing here should be modified manually, and the folder should be added to .gitignore.

Let’s open the peppy.json5 file of the node we just created and explore it together.

peppy.json5
{
schema_version: 1,
manifest: {
name: "hello_world",
tag: "0.1.0",
language: "python",
},
process: {
add_cmd: [
"uv",
"sync"
],
start_cmd: [
"uv",
"run",
"hello_world"
]
},
interfaces: {}
}

Let’s go through the different options:

  • schema_version: Always set to version 1 for the moment, it allows the daemon to identify the structure of the configuration file
  • manifest: Contains information about the node. Each node is identified by its name and tag
  • process: Contains the execution commands for the node
    • add_cmd: Command run when the node is added to the Node stack (more on this later). This is typically the place where you want to run heavy operations like code compilation.
    • start_cmd: Command run when a node instance is started
  • interfaces: This is where the interfaces that communicate with the other nodes is defined

Let’s modify this configuration file to expose a topic that sends a “hello world” message.

peppy.json5
{
schema_version: 1,
manifest: {
name: "hello_world",
tag: "0.1.0",
language: "python",
},
process: {
add_cmd: [
"uv",
"sync"
],
start_cmd: [
"uv",
"run",
"hello_world"
]
},
interfaces: {
exposes: {
topics: [
{
name: "message_stream",
qos_profile: "sensor_data",
message_format: {
message: "string"
},
}
],
}
}
}

To apply these changes, the node needs to synchronize with the cache in the .peppy folder. Run:

Terminal window
peppy node sync

This command will generate the interfaces to use in our source code. We can now modify the code and access those interfaces:

src/hello_world/__main__.py
import asyncio
from peppygen import NodeBuilder, NodeRunner
from peppygen.parameters import Parameters
from peppygen.exposed_topics import message_stream
async def emit_hello_world_loop(node_runner: NodeRunner):
counter = 0
while True:
counter += 1
message = f"hello world count {counter}"
print(message, flush=True)
await message_stream.emit(node_runner, message)
await asyncio.sleep(3)
async def setup(params: Parameters, node_runner: NodeRunner) -> list[asyncio.Task]:
return [asyncio.create_task(emit_hello_world_loop(node_runner))]
def main():
NodeBuilder().run(setup)
if __name__ == "__main__":
main()

As you can see, the topic interface has already been generated. We didn’t need to implement serialization, encoding, or communication layers—PeppyOS handles all of that automatically.

Additionally, the tag and name assigned to your node provide version isolation. If the node’s interfaces change in a future update, dependent nodes won’t break—they continue using the previous version until explicitly migrated.