Skip to content

Getting Started

Installation

Clone the repository and install dependencies with uv:

git clone https://github.com/zauberzeug/feldfreund_devkit.git
cd feldfreund_devkit
uv sync

Running the Example

The repository includes a minimal example in main.py that demonstrates:

  • Robot simulation with keyboard control
  • Straight line navigation automation
  • Real-time 3D visualization

Start it with:

uv run main.py

Open http://localhost:8080 in your browser. Hold SHIFT and use the arrow keys to steer the robot, or use the automation controls to run a straight line navigation.

Understanding the Example

#! /usr/bin/env python
import rosys
from nicegui import app, ui
from rosys.automation import Automator, automation_controls
from rosys.driving import Driver, Steerer, keyboard_control, robot_object

import feldfreund_devkit
from feldfreund_devkit.config import FeldfreundConfiguration, Secrets, config_from_id
from feldfreund_devkit.implement import ImplementDummy
from feldfreund_devkit.navigation import (
    RecordedTrackNavigation,
    RecordedTrackProvider,
    StraightLineNavigation,
    TrackRecordingController,
    WaypointNavigation,
)


class System(feldfreund_devkit.System):
    def __init__(self, config: FeldfreundConfiguration, secrets: Secrets) -> None:
        super().__init__(config, secrets=secrets)
        self.steerer = Steerer(self.feldfreund.wheels, speed_scaling=0.25)
        self.driver = Driver(self.feldfreund.wheels, self.odometer, parameters=self.config.driver)
        self.shape = rosys.geometry.Prism.default_robot_shape()
        self.automator = Automator(self.steerer, on_interrupt=self.feldfreund.stop, notify=False)

        self.recorded_track_provider = RecordedTrackProvider().persistent()
        self.track_recording_controller = TrackRecordingController(
            self.recorded_track_provider, pose_provider=self.odometer, gnss=self.feldfreund.gnss)

        common = {'implement': ImplementDummy(), 'driver': self.driver, 'pose_provider': self.odometer}
        self.navigations: dict[str, WaypointNavigation] = {n.name: n for n in [
            StraightLineNavigation(**common),
            RecordedTrackNavigation(recorded_track_provider=self.recorded_track_provider,
                                    track_recording_controller=self.track_recording_controller,
                                    gnss=self.feldfreund.gnss,
                                    automator=self.automator,
                                    **common),
        ]}
        self._current_navigation: WaypointNavigation = next(iter(self.navigations.values()))
        self.automator.default_automation = self._current_navigation.start

    @property
    def current_navigation(self) -> WaypointNavigation:
        return self._current_navigation

    @current_navigation.setter
    def current_navigation(self, navigation: WaypointNavigation) -> None:
        self._current_navigation = navigation
        self.automator.default_automation = navigation.start


def startup() -> None:
    secrets = Secrets()
    config = config_from_id('example', secrets=secrets)
    system = System(config, secrets).persistent()

    @ui.page('/')
    def ui_content() -> None:
        keyboard_control(system.steerer)
        with ui.scene():
            robot_object(system.shape, system.odometer)

        @ui.refreshable
        def navigation_settings() -> None:
            system.current_navigation.settings_ui()

        def select_navigation(name: str) -> None:
            system.current_navigation = system.navigations[name]
            navigation_settings.refresh()

        with ui.card():
            ui.label('hold SHIFT to steer with the keyboard arrow keys or use the automation controls')
            ui.select(list(system.navigations), value=system.current_navigation.name, label='Navigation',
                      on_change=lambda e: select_navigation(e.value)).classes('w-64')
            with ui.row():
                navigation_settings()
            with ui.row():
                automation_controls(system.automator)


app.on_startup(startup)

ui.run(title='Feldfreund_devkit')

The System class extends feldfreund_devkit.System which initializes the robot hardware (or simulation) based on the configuration. Key components:

  • config: Loaded from config/example.py via config_from_id('example')
  • steerer: Manual steering control
  • driver: Path-following driver for automations
  • navigation: StraightLineNavigation drives forward for a configurable distance
  • automator: Manages automation lifecycle (play/pause/stop)

Configuration

Robot configurations live in the config/ directory. See config/example.py:

from rosys.vision import ImageSize

from feldfreund_devkit.config import (
    BluetoothConfiguration,
    BumperConfiguration,
    CameraConfiguration,
    FeldfreundConfiguration,
    FlashlightConfiguration,
    GnssConfiguration,
    ImuConfiguration,
    MjpegCameraConfig,
    ODriveTracksConfiguration,
    RobotBrainConfiguration,
    Secrets,
    UsbCameraConfig,
)


def build_config(secrets: Secrets) -> FeldfreundConfiguration:
    return FeldfreundConfiguration(
        robot_id='Example',
        bluetooth=BluetoothConfiguration(name='example', pin_code=123456),
        bumper=BumperConfiguration(pin_front_top=21, pin_front_bottom=35, pin_back=18),
        cameras=CameraConfiguration(
            main=UsbCameraConfig(camera_id='example-usb-0', image_size=ImageSize(width=1280, height=720), fps=10),
            front=MjpegCameraConfig(camera_id='example-mac-4', image_size=ImageSize(width=1280, height=720),
                                    password=secrets.MJPEG_CAMERA_PASSWORD),
            back=MjpegCameraConfig(camera_id='example-mac-3', image_size=ImageSize(width=1280, height=720),
                                   password=secrets.MJPEG_CAMERA_PASSWORD),
            right=MjpegCameraConfig(camera_id='example-mac-1', image_size=ImageSize(width=1280, height=720),
                                    password=secrets.MJPEG_CAMERA_PASSWORD),
            left=MjpegCameraConfig(camera_id='example-mac-2', image_size=ImageSize(width=1280, height=720),
                                   password=secrets.MJPEG_CAMERA_PASSWORD),
        ),
        flashlight=FlashlightConfiguration(),
        gnss=GnssConfiguration(),
        implement=None,
        imu=ImuConfiguration(),
        robot_brain=RobotBrainConfiguration(name='rbexample', nand=True),
        wheels=ODriveTracksConfiguration(is_left_reversed=True,
                                         is_right_reversed=False,
                                         left_back_can_address=0x000,
                                         left_front_can_address=0x100,
                                         right_back_can_address=0x200,
                                         right_front_can_address=0x300,
                                         odrive_version=6),
    )

In simulation mode (when no hardware is detected), mock implementations are used automatically.

Next Steps

  • Browse the Module Reference in the navigation for API documentation
  • Check the Tutorials for hardware calibration guides
  • See Troubleshooting for common issues