Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
da4966d
move docs over
pascalzauberzeug Nov 19, 2025
8dde3d1
Merge remote-tracking branch 'origin/main' into mkdocs
pascalzauberzeug Dec 17, 2025
3748b2f
add image
pascalzauberzeug Dec 17, 2025
ea10a5a
update README
pascalzauberzeug Dec 17, 2025
69f25be
update docs
pascalzauberzeug Dec 17, 2025
7f9dae3
Merge remote-tracking branch 'origin/main' into mkdocs
pascalzauberzeug Jan 21, 2026
0201fdc
switch to uv
pascalzauberzeug Jan 21, 2026
723e577
fix reload
pascalzauberzeug Jan 21, 2026
55f14bd
rewrite index
pascalzauberzeug Jan 21, 2026
94fe9d8
add livesync
pascalzauberzeug Jan 21, 2026
c739ed8
cleanup
pascalzauberzeug Jan 21, 2026
b38cfc6
add example
pascalzauberzeug Jan 21, 2026
7b38477
fix docu
pascalzauberzeug Jan 21, 2026
30809b0
add more docs
pascalzauberzeug Jan 21, 2026
62b938a
fix wording
pascalzauberzeug Jan 21, 2026
71ea556
add getting started
pascalzauberzeug Jan 21, 2026
0dfab7f
fix pylint
pascalzauberzeug Jan 21, 2026
e1f9f00
fix docs
pascalzauberzeug Jan 21, 2026
adb4635
fix pre-commit
pascalzauberzeug Jan 21, 2026
8859b0d
typo
pascalzauberzeug Jan 21, 2026
34eb9d4
add mkdocs commands to makefile
pascalzauberzeug Jan 21, 2026
abd2804
remove unnecessary script
pascalzauberzeug Jan 21, 2026
698366b
naming
pascalzauberzeug Jan 21, 2026
eca1ec5
remote site folder after deployment
pascalzauberzeug Jan 21, 2026
fbb2c76
fix page setup
pascalzauberzeug Jan 23, 2026
e638b5b
bring back drive segment todo
pascalzauberzeug Jan 23, 2026
6d14cdf
update RoSys
pascalzauberzeug Jan 23, 2026
c195173
add uv lock
pascalzauberzeug Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Publish release

on:
workflow_dispatch:
push:
tags:
- v**

jobs:
docs:
name: Build and publish docs
runs-on: ubuntu-latest
needs: pypi
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --group docs
- name: Build docs
run: mkdocs build -v
- name: Deploy gh-pages
uses: JamesIves/github-pages-deploy-action@v4.7.3
with:
folder: site
# TODO: restructure in another PR
# - name: set version
# run: |
# VERSION=${{ env.VERSION }}
# - name: Create GitHub release entry
# uses: softprops/action-gh-release@v2
# id: create_release
# with:
# draft: false
# prerelease: false
# name: ${{ env.VERSION }}
# tag_name: ${{ env.VERSION }}
# env:
# GITHUB_TOKEN: ${{ github.token }}
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help sync install-ci mypy pylint pre-commit
.PHONY: help sync install-ci mypy pylint pre-commit docs-serve docs-deploy

default: help

Expand Down Expand Up @@ -36,3 +36,11 @@ pre-commit:

## check Run all code checks (mypy, pre-commit, pylint).
check: mypy pre-commit pylint

## docs-serve Serve documentation locally.
docs-serve:
uv run --active mkdocs serve

## docs-deploy Deploy documentation to GitHub Pages.
docs-deploy:
uv run --active mkdocs gh-deploy --force && rm -rf site
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Feldfreund DevKit
<img src="https://github.com/zauberzeug/feldfreund_devkit/raw/main/assets/feldfreund.webp" alt="Feldfreund rendering" width="40%" align="right" />

A sturdy development platform for autonomous outdoor robotics.
# Feldfreund Dev Kit

## Tech Stack
This is the source code of the [Feldfreund Dev Kit](https://zauberzeug.com/products/field-friend-dev-kit) for autonomous outdoor robotics made by [Zauberzeug](https://zauberzeug.com/).
The software is based on [RoSys](https://rosys.io) and [NiceGUI](https://nicegui.io/).
The micro controller is programmed with [Lizard](https://github.com/zauberzeug/lizard).

- Python 3.11+
- [NiceGUI](https://nicegui.io) for web interface
- [uv](https://astral.sh/uv) for dependency management
- [RoSys](https://rosys.io) framework
- [Copier](https://copier.readthedocs.io/) for template configuration (from [nicegui-template](https://github.com/zauberzeug/nicegui-template))
Our agricultural weeding robot [Feldfreund](https://zauberzeug.com/feldfreund) is based on this platform and is intended to advance organic and regenerative agriculture.
There is also a [ROS2 implementation](https://github.com/zauberzeug/feldfreund_devkit_ros) based on this repository.

Please see the [documentation](https://docs.feldfreund.de) for details on installation, setup and usage.

## Development

Expand Down
Binary file added assets/feldfreund.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docs.feldfreund.de
132 changes: 132 additions & 0 deletions docs/generate_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import importlib
import inspect
import logging
import re
import sys
from pathlib import Path
from types import ModuleType

import mkdocs_gen_files

nav = mkdocs_gen_files.Nav()


def extract_events(filepath: str) -> dict[str, str]:
with open(filepath, encoding='utf-8') as f:
lines = f.read().splitlines()
events_: dict[str, str] = {}
for i, line in enumerate(lines):
if re.search(r'= Event(\[.*?\])?\(\)$', line):
event_name_ = line.strip().split()[0].removeprefix('self.').rstrip(':')
event_doc_ = lines[i + 1].split('"""')[1]
events_[event_name_] = event_doc_
return events_


def format_type(hint) -> str:
if hint is None:
return ''
if hint is type(None):
return 'None'
if hasattr(hint, '__name__'):
return f'`{hint.__name__}`'
type_str = str(hint).replace('typing.', '').replace('NoneType', 'None')
return f'`{type_str}`'


def extract_properties(cls: type) -> dict[str, tuple[str, str]]:
props: dict[str, tuple[str, str]] = {}
for name, obj in inspect.getmembers(cls):
if name.startswith('_'):
continue
if isinstance(obj, property) and obj.fget is not None:
doc = obj.fget.__doc__ or ''
doc = doc.strip().split('\n')[0] if doc else ''
type_hint = obj.fget.__annotations__.get('return')
props[name] = (format_type(type_hint), doc)
return props


def extract_instance_attributes(filepath: str) -> dict[str, tuple[str, str]]:
"""Extract instance attributes with their inline docstrings from source."""
with open(filepath, encoding='utf-8') as f:
lines = f.read().splitlines()
attrs: dict[str, tuple[str, str]] = {}
for i, line in enumerate(lines):
match = re.match(r'\s+self\.(\w+)(?::\s*(\S+))?\s*=', line)
if match:
attr_name = match.group(1)
attr_type = match.group(2) or ''
if attr_name.startswith('_'):
continue
if i + 1 < len(lines) and '"""' in lines[i + 1]:
doc = lines[i + 1].split('"""')[1]
type_str = f'`{attr_type}`' if attr_type else ''
attrs[attr_name] = (type_str, doc)
return attrs


for path in sorted(Path('feldfreund_devkit').rglob('__init__.py')):
identifier = str(path.parent).replace('/', '.')
if identifier == 'feldfreund_devkit':
continue

try:
module = importlib.import_module(identifier)
except Exception:
logging.exception('Failed to import %s', identifier)
sys.exit(1)

doc_path = path.parent.with_suffix('.md')
found_something = False
for name in getattr(module, '__all__', dir(module)):
if name.startswith('_'):
continue
cls = getattr(module, name)
if isinstance(cls, ModuleType):
continue
if not inspect.isclass(cls):
continue
if not cls.__doc__:
continue
source_file = inspect.getfile(cls)
events = extract_events(source_file)
properties = extract_properties(cls)
instance_attrs = extract_instance_attributes(source_file)
if cls.__name__ != name:
cls_module = cls.__module__
doc_identifier = f'{cls_module}.{cls.__name__}'
else:
doc_identifier = f'{identifier}.{name}'
properties = {k: v for k, v in properties.items() if v[1]}
instance_attrs = {k: v for k, v in instance_attrs.items() if v[1] and k not in events}
members = {**instance_attrs, **properties}
filters = list(events.keys()) + list(members.keys())
with mkdocs_gen_files.open(Path('reference', doc_path), 'a') as fd:
print(f'::: {doc_identifier}', file=fd)
if filters:
print(' options:', file=fd)
print(' filters:', file=fd)
for filter_name in filters:
print(f' - "!{filter_name}"', file=fd)
if members:
print('### Attributes & Properties', file=fd)
print('Name | Type | Description', file=fd)
print('- | - | -', file=fd)
for member_name, (member_type, member_doc) in members.items():
print(f'`{member_name}` | {member_type} | {member_doc}', file=fd)
print('', file=fd)
if events:
print('### Events', file=fd)
print('Name | Description', file=fd)
print('- | -', file=fd)
for event_name, event_doc in events.items():
print(f'`{event_name}` | {event_doc}', file=fd)
print('', file=fd)
found_something = True

if found_something:
nav[path.parent.parts[1:]] = doc_path.as_posix()

with mkdocs_gen_files.open('reference/SUMMARY.md', 'w') as nav_file:
nav_file.writelines(nav.build_literate_nav())
57 changes: 57 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Getting Started

## Installation

Clone the repository and install dependencies with [uv](https://docs.astral.sh/uv/):

```bash
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:

```bash
uv run main.py
```

Open [http://localhost:8080](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

```python
--8<-- "main.py"
```

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`:

```python
--8<-- "config/example.py"
```

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](tutorials/tutorials.md) for hardware calibration guides
- See [Troubleshooting](troubleshooting.md) for common issues
17 changes: 17 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# About

The **Feldfreund Dev Kit** is an open-source platform for autonomous outdoor robotics developed by [Zauberzeug GmbH](https://zauberzeug.com).
Built on [RoSys](https://rosys.io), an all-Python robot system framework with built-in simulation, hardware abstraction, and a real-time web interface via [NiceGUI](https://nicegui.io/).
Time-critical and safety-critical behavior runs on the microcontroller via [Lizard](https://lizard.dev), a domain-specific language for embedded hardware control.

Our agricultural weeding robot [Feldfreund](https://zauberzeug.com/feldfreund) is based on this platform and is intended to advance organic and regenerative agriculture.

**This documentation is currently work in progress while we expand this library.**

Stay tuned and please [report any issues on Github](https://github.com/zauberzeug/feldfreund_devkit/issues).

## Features

- **Open Source** — modify and enhance the software to fit your specific needs.
- **Modular Design** — equip with tools from Zauberzeug, third-party solutions, or custom developments.
- **ROS2 Support** — a [ROS2 implementation](https://github.com/zauberzeug/feldfreund_devkit_ros) is available.
27 changes: 27 additions & 0 deletions docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
:root {
--md-primary-fg-color: #6e93d6;
--md-default-fg-color: #3a3e42;
--md-accent-fg-color: #53b689;
}

:root > * {
--md-code-hl-color: #d8e5fa;
}

h2.doc-heading {
border-bottom: 1pt solid lightgray;
}

img {
width: 70%;
display: block;
margin-left: auto;
margin-right: auto;
border-radius: 5px;
box-shadow: rgba(0, 0, 0, 0.25) 0 5px 10px;
}

/* Hide "Defaults" section from mkdocstrings */
details.defaults {
display: none;
}
26 changes: 26 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Troubleshooting

This section provides guidance for diagnosing and resolving issues during operation.
If your issue is not listed here, you can also check the [RoSys documentation](https://rosys.io/troubleshooting/).

## Logs

Check the logs for warnings or errors. Archived logs are stored in the `~/.rosys` directory.

For more detail, enable debug-level logging on the [RoSys Logging Page](https://rosys.io/reference/rosys/analysis/#rosys.analysis.logging_page.LoggingPage) available at [/logging](http://localhost:8080/logging).

## Permission denied directly after startup

If you get the `[Errno 13] Permission denied` error message right after you started `main.py`, your system is probably blocking the default port 80.
Set a custom port via environment variable:

```bash
PORT=8080 uv run ./main.py
```

Or add it to a `.env` file:

```
ROBOT_ID=my_robot
PORT=8080
```
39 changes: 39 additions & 0 deletions docs/tutorials/imu_calibration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Imu Calibration

The IMU is not installed in its intended orientation, that is why we have to find the correct values.
The standard coordinate frame of our robots is a right-handed one where X is forward, Y is left and Z is upward.
The built-in IMUs orientation is X-left, Y-down and Z-backward, therefore a roll rotation of -90° and a yaw rotation of 90° is needed for a Robot Brain in its default configuration, where the socket connectors show backwards.
Older robots like U4 will need an additional yaw rotation of 90° afterwards, because their Robot Brains are built in sideways.

Here is a short Python script to generate the needed configuration:

```python
#!/usr/bin/env python3
import numpy as np
from rosys.geometry import Rotation

base_rotation = Rotation.from_euler(np.deg2rad(-90), 0, np.deg2rad(90))
print(f'{base_rotation=}')

roll = np.deg2rad(0.0)
pitch = np.deg2rad(0.0)
correction = Rotation.from_euler(roll, pitch, 0.0)
print(f'{correction=}')

complete_correction = correction * base_rotation
print(f'{complete_correction=}')

print('\nCode snippet for your robot\'s configuration:')
print(f'imu=Imu(offset_rotation=Rotation.from_euler({complete_correction.roll:.6f}, {complete_correction.pitch:.6f}, {complete_correction.yaw:.6f}))')
```

Steps:

1. Find the correct base orientation of your IMU.
If your Feldfreund has the standard configuration, you can use this as a starting point:
`Imu(offset_rotation=Rotation.from_euler(-1.570796, -0.000000, 1.570796))`
2. Roll and pitch your robot manually to check if the configuration is correct and the axes are correctly rotated.
3. Place your robot on a level surface and check the IMU's current values on the [development page](http://192.168.42.2/dev)
4. Put the shown values for roll and pitch in the script above.
That will generate your final configuration values.
Note, that IMU values have some noise, so your configuration won't be perfect.
Loading