reachy_mini_metronome / docs /app_framework.md
Boopster's picture
feat: Implement initial project structure for Reachy Mini Metronome with web UI, backend logic, and documentation.
5c78d2c
# ReachyMiniApp Framework
The Reachy Mini SDK provides a base class `ReachyMiniApp` for building applications. It handles lifecycle management, stop signals, and optionally serves a settings web UI.
## Quick Reference
```python
import threading
from reachy_mini import ReachyMini, ReachyMiniApp
class MyApp(ReachyMiniApp):
custom_app_url: str | None = "http://0.0.0.0:8042"
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
while not stop_event.is_set():
# Your main loop here
pass
if __name__ == "__main__":
app = MyApp()
app.wrapped_run()
```
## Class Structure
### `ReachyMiniApp` (Abstract Base Class)
**Source:** `reachy_mini/apps/app.py`
```python
class ReachyMiniApp(ABC):
custom_app_url: str | None = None # URL for settings web UI
dont_start_webserver: bool = False # Skip web server startup
def __init__(self) -> None:
self.stop_event = threading.Event()
self.error: str = ""
self.settings_app: FastAPI | None = None
# ...
@abstractmethod
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event) -> None:
"""Main app logic - you implement this."""
pass
def wrapped_run(self, *args, **kwargs) -> None:
"""Wrapper that handles setup/teardown."""
pass
def stop(self) -> None:
"""Gracefully stop the app."""
self.stop_event.set()
```
## Key Attributes
### `custom_app_url`
If set, the app will start a FastAPI web server at this URL.
```python
custom_app_url: str | None = "http://0.0.0.0:8042" # Serve at port 8042
custom_app_url: str | None = None # No web UI
```
### `settings_app`
A FastAPI instance for adding custom API endpoints. Only available if `custom_app_url` is set.
```python
@self.settings_app.post("/my_endpoint")
def my_handler():
return {"status": "ok"}
```
### `stop_event`
A `threading.Event` that gets set when the app should stop. Check this in your main loop:
```python
while not stop_event.is_set():
# Keep running
time.sleep(0.01)
```
## Static Files
The framework automatically serves static files from `<your_app>/static/`:
```
reachy_mini_metronome/
β”œβ”€β”€ main.py # Your app code
└── static/
β”œβ”€β”€ index.html # Served at /
β”œβ”€β”€ style.css # Served at /static/style.css
└── script.js # Served at /static/script.js
```
**Source:** `reachy_mini/apps/app.py`
```python
static_dir = self._get_instance_path().parent / "static"
if static_dir.exists():
self.settings_app.mount("/static", StaticFiles(directory=static_dir), name="static")
index_file = static_dir / "index.html"
if index_file.exists():
@self.settings_app.get("/")
async def index() -> FileResponse:
return FileResponse(index_file)
```
## Adding API Endpoints
You can add FastAPI endpoints in your `run()` method:
```python
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
# State variables
bpm = 120
is_running = False
# Define endpoints
@self.settings_app.post("/bpm")
def update_bpm(data: dict):
nonlocal bpm
bpm = data.get("bpm", bpm)
return {"bpm": bpm}
@self.settings_app.post("/start")
def start():
nonlocal is_running
is_running = True
return {"running": True}
@self.settings_app.get("/status")
def status():
return {"bpm": bpm, "running": is_running}
# Main loop
while not stop_event.is_set():
if is_running:
# Do stuff
pass
time.sleep(0.01)
```
## Lifecycle
When you call `app.wrapped_run()`:
1. **Web server starts** (if `custom_app_url` is set) on a background thread
2. **ReachyMini connects** using context manager
3. **Your `run()` method executes**
4. **On stop** (Ctrl+C or `stop_event.set()`):
- ReachyMini disconnects
- Web server shuts down
```python
def wrapped_run(self, *args, **kwargs) -> None:
# Start settings server in background thread
if self.settings_app is not None:
# ... uvicorn server setup ...
settings_app_t.start()
try:
with ReachyMini(*args, **kwargs) as reachy_mini:
self.run(reachy_mini, self.stop_event)
finally:
if settings_app_t is not None:
self.stop_event.set()
settings_app_t.join()
```
## Metronome App Structure
For our metronome, the structure will be:
```python
from pydantic import BaseModel
class BpmUpdate(BaseModel):
bpm: int
class TimeSignatureUpdate(BaseModel):
beats: int
class ReachyMiniMetronome(ReachyMiniApp):
custom_app_url = "http://0.0.0.0:8042"
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
# State
bpm = 120
time_signature = 4
current_beat = 1
is_running = False
# API endpoints
@self.settings_app.post("/bpm")
def set_bpm(data: BpmUpdate):
nonlocal bpm
bpm = max(40, min(208, data.bpm))
return {"bpm": bpm}
@self.settings_app.post("/time_signature")
def set_time_signature(data: TimeSignatureUpdate):
nonlocal time_signature, current_beat
time_signature = data.beats
current_beat = 1
return {"time_signature": time_signature}
@self.settings_app.post("/start")
def start():
nonlocal is_running
is_running = True
return {"running": True}
@self.settings_app.post("/stop")
def stop():
nonlocal is_running
is_running = False
return {"running": False}
@self.settings_app.get("/status")
def get_status():
return {
"bpm": bpm,
"time_signature": time_signature,
"current_beat": current_beat,
"running": is_running
}
# Main loop
while not stop_event.is_set():
# ... metronome logic ...
time.sleep(0.01)
```