Skip to content

Camera Arm

This example demonstrates how to use 3D coordinate frames to model a camera arm.

#!/usr/bin/env python3
import math
from typing import Any

from nicegui import ui

from rosys import config, persistence
from rosys.driving import Driver, Odometer, Steerer, joystick, keyboard_control
from rosys.geometry import Frame3d, Pose3d, Rotation, axes_object
from rosys.hardware import RobotSimulation, WheelsSimulation

wheels = WheelsSimulation()
robot = RobotSimulation([wheels])
steerer = Steerer(wheels)
odometer = Odometer(wheels)
driver = Driver(wheels, odometer)


class Link(persistence.Persistable):

    def __init__(self, name: str, parent_frame: Frame3d, *, length: float) -> None:
        super().__init__()
        self.name = name
        self.length = length
        self.base = Pose3d().in_frame(parent_frame).as_frame(f'{name}_base')
        self.end = Pose3d(z=length).as_frame(f'{name}_end').in_frame(self.base)

    def pitch(self, angle: float) -> None:
        self.base.rotation = Rotation.from_euler(0, angle, 0)
        self.request_backup()

    def backup_to_dict(self) -> dict[str, Any]:
        return {
            'name': self.name,
            'base': persistence.to_dict(self.base),
        }

    def restore_from_dict(self, data: dict[str, Any]) -> None:
        self.name = data['name']
        self.base = persistence.from_dict(Frame3d, data['base'])
        self.end = Pose3d(z=self.length) \
            .as_frame(f'{self.name}_end') \
            .in_frame(self.base)


class Cam(persistence.Persistable):

    def __init__(self, parent_frame: Frame3d) -> None:
        super().__init__()
        self.pose = Pose3d(z=0.05).in_frame(parent_frame)

    def pitch(self, angle: float) -> None:
        self.pose.rotation = Rotation.from_euler(0, angle, 0)
        self.request_backup()

    def backup_to_dict(self) -> dict[str, Any]:
        return {'pose': persistence.to_dict(self.pose)}

    def restore_from_dict(self, data: dict[str, Any]) -> None:
        self.pose = persistence.from_dict(Frame3d, data['pose'])


anchor_frame = Pose3d(z=0.3).as_frame('anchor').in_frame(odometer.prediction_frame)
arm1 = Link('arm1', anchor_frame, length=0.3).persistent(key='arm1')
arm2 = Link('arm2', arm1.end, length=0.3).persistent(key='arm2')
cam = Cam(arm2.end).persistent(key='cam')


@ui.page('/')
def page():
    def update_scene():
        chassis.move(*odometer.prediction_frame.resolve().translation)
        chassis.rotate_R(odometer.prediction_frame.resolve().rotation.R)
        segment1.move(*arm1.base.resolve().translation)
        segment1.rotate_R(arm1.base.resolve().rotation.R)
        segment2.move(*arm2.base.resolve().translation)
        segment2.rotate_R(arm2.base.resolve().rotation.R)
        camera_box.move(*cam.pose.resolve().translation)
        camera_box.rotate_R(cam.pose.resolve().rotation.R)
    ui.timer(config.ui_update_interval, update_scene)

    keyboard_control(steerer)
    with ui.row():
        with ui.scene() as scene:
            with scene.group() as chassis:
                scene.box(width=1.0, height=0.5, depth=0.3) \
                    .move(z=0.15).material(color='gray')
            with scene.group() as segment1:
                scene.box(width=0.1, height=0.1, depth=arm1.length) \
                    .move(z=arm1.length / 2)
            with scene.group() as segment2:
                scene.box(width=0.1, height=0.1, depth=arm2.length) \
                    .move(z=arm2.length / 2)
            with scene.group() as camera_box:
                scene.box(width=0.1, height=0.1, depth=0.1) \
                    .material(color='SteelBlue')
            scene.move_camera(y=-1, z=1, look_at_z=0.5)
        with ui.column():
            joystick(steerer, size=50, color='blue')
            ui.slider(min=-math.pi / 2, max=math.pi / 2, step=0.01, value=0,
                      on_change=lambda e: arm1.pitch(e.value))
            ui.slider(min=-math.pi / 2, max=math.pi / 2, step=0.01, value=0,
                      on_change=lambda e: arm2.pitch(e.value))
            ui.slider(min=-math.pi / 4, max=math.pi / 4, step=0.01, value=0,
                      on_change=lambda e: cam.pitch(e.value))
        with ui.scene() as scene2:
            axes_object(anchor_frame, length=0.15)
            axes_object(arm1.base, name='Arm 1 Base', length=0.15)
            axes_object(arm2.base, name='Arm 2 Base', length=0.15)
            axes_object(cam.pose, name='Camera', length=0.15)
            scene2.move_camera(y=-1, z=1, look_at_z=0.5)


ui.run(title='Camera Arm')