De noodrem

VERPLICHT LEZEN!!!

Op de Amiga zit een prachtige grote rode knop. Dit is de noodrem. De ‘noodrem’ REMT de robot niet, maar zorgt ervoor dat de motoren uitvallen. Dit betekent dat je de robot op de noodrem nog gewoon vooruit kan duwen, en dat als je op een heuvel staat de robot ervandoor gaat. Pas dus op!

Zet de noodrem er ALTIJD op als:

  • Je van de robot wegloopt.
  • Je de robot niet hoeft te laden rijden.

Voer ook nooit code gerelateerd aan de motoren uit als je niet fysiek in de buurt bent van de robot. De Amiga is geen speelgoed.

De E-Stop

Je zal opmerken dat je, ondanks het van de noodrem afhalen, niet altijd met de robot kan rijden. Dit komt door de ‘e-stop’, een elektronische beveiliging. Deze kan je eraf halen door op het kleine schermpje op de robot op enkele knopjes te drukken.

Als je het volgende ziet kan je nog niet rijden, en moet je op de ‘auto control’ knop drukken:

Als je het volgende ziet kan je rijden:

Tevens zie je op het dashboard rechtsboven dan ook ‘auto’ staan:

De ‘pendant’

De ‘pendant’ is de zwarte controller die bij de intelligence kit van de Amiga hoort. Met de pendant kan je de robot handmatig besturen:

Mocht je het nodig hebben kan je de staat van de pendant ook uitlezen in code. Zie hier het voorbeeld van hoe je dat kan doen.

Code

Om motoren uit te voeren moet je Twist2D berichten sturen naar de canbus. Hierin specificeer je:

  • de gewenste beweging vooruit/achteruit in meter per seconde.
  • de gewenste rotatie linksom/rechtsom in radialen per seconde.

Deze berichten moet je blijven sturen; als je enkel eenmaal een bericht stuurt zal de robot schokken en stoppen.

Hier staat een voorbeeld van Farm-ng, maar ik heb voor jullie al een kant-en-klare taak gemaakt die past bij onze custom template.

De IDriveHandler:

from abc import ABC, abstractmethod  
  
  
class IDriveHandler(ABC):  
    @abstractmethod  
    async def forward(self, speed: float):  
        """Sets the moving speed of the Amiga to the specified speed in m/s forward."""  
        pass  
  
    @abstractmethod  
    async def backward(self, speed: float):  
        """Sets the moving speed of the Amiga to the specified speed in m/s backwards."""  
        pass  
  
    @abstractmethod  
    async def turn_left(self, rad: float):  
        """Sets the turning speed of the Amiga to the specified speed in rad/s left."""  
        pass  
  
    @abstractmethod  
    async def turn_right(self, rad: float):  
        """Sets the turning speed of the Amiga to the specified speed in rad/s right."""  
        pass  
  
    @abstractmethod  
    async def stop(self):  
        """Stops the entire process of driving by setting both linear- and angular velocity to 0."""  
        pass

De DriveHandler:

from pathlib import Path  
  
from farm_ng.canbus.canbus_pb2 import Twist2d  
from farm_ng.core.event_client import EventClient  
from farm_ng.core.event_service_pb2 import EventServiceConfig  
from farm_ng.core.events_file_reader import proto_from_json_file  
  
from driver.IDriveHandler import IDriveHandler  
from logger.ILogger import ILogger  
from system.platform.EPlatform import EPlatform  
from system.platform.IPlatformHandler import IPlatformHandler  
  
  
class DriveHandler(IDriveHandler):  
    # NOTE: be careful with these values, they are in m/s and rad/s  
    DEFAULT_LINEAR_VELOCITY_MPS = 0.1  
    DEFAULT_ANGULAR_VELOCITY_RPS = 0.1  
    MAX_LINEAR_VELOCITY = 0.5  
    MAX_ANGULAR_VELOCITY = 0.5  
  
    def __init__(self, logger: ILogger, platform_handler: IPlatformHandler):  
        self.logger = logger  
        self.platform_handler = platform_handler  
  
        if self.platform_handler.get_current() == EPlatform.ROBOT:  
            service_config_path = Path() / 'src' / 'res' / 'service_config.json'  
            self.config: EventServiceConfig = proto_from_json_file(service_config_path, EventServiceConfig())  
            self.client: EventClient = EventClient(self.config)  
  
    async def _send_update(self, twist: Twist2d):  
        self.logger.info(f"Sending linear velocity: {twist.linear_velocity_x:.3f}, angular velocity: {twist.angular_velocity:.3f}")  
        await self.client.request_reply("/twist", twist)  
  
    async def _set_drive_values(self, linear_velocity_x: float, linear_velocity_y: float, angular_velocity: float):  
        if self.platform_handler.get_current() == EPlatform.ROBOT:  
            twist = Twist2d()  
            twist.linear_velocity_x = linear_velocity_x  
            twist.linear_velocity_y = linear_velocity_y  
            twist.angular_velocity = angular_velocity  
            await self._send_update(twist)  
        else:  
            self.logger.error(f"Could not set drive values because on platform {self.platform_handler.get_current()}")  
  
    async def forward(self, speed: float = DEFAULT_LINEAR_VELOCITY_MPS):  
        assert 0.0 < speed < DriveHandler.MAX_LINEAR_VELOCITY  
        await self._set_drive_values(speed, 0.0, 0.0)  
  
    async def backward(self, speed: float = DEFAULT_LINEAR_VELOCITY_MPS):  
        assert 0.0 < speed < DriveHandler.MAX_LINEAR_VELOCITY  
        await self._set_drive_values(-speed, 0.0, 0.0)  
  
    async def turn_left(self, rad: float = DEFAULT_ANGULAR_VELOCITY_RPS):  
        assert 0.0 < rad < DriveHandler.MAX_ANGULAR_VELOCITY  
        await self._set_drive_values(0.0, 0.0, rad)  
  
    async def turn_right(self, rad: float = DEFAULT_ANGULAR_VELOCITY_RPS):  
        assert 0.0 < rad < DriveHandler.MAX_ANGULAR_VELOCITY  
        await self._set_drive_values(0.0, 0.0, -rad)  
  
    async def stop(self):  
        await self._set_drive_values(0.0, 0.0, 0.0)

De EDriveState:

from enum import IntEnum  
  
  
class EDriveState(IntEnum):  
    STOP = 1,  
    FORWARD = 2,  
    BACKWARDS = 3,  
    LEFT = 4,  
    RIGHT = 5

De DriveTask:

from typing import Optional  
  
from driver.EDriveState import EDriveState  
from driver.IDriveHandler import IDriveHandler  
from system.threading.IThreadHandler import IThreadHandler  
from tasks.BaseTask import BaseTask  
  
  
class DriveTask(BaseTask):  
    drive_state: EDriveState = Optional[EDriveState] # We break on start for safety! :)  
    DEFAULT_SPEED = 0.1  
  
    def __init__(self, thread_handler: IThreadHandler, drive_handler: IDriveHandler):  
        super().__init__(thread_handler)  
        self.drive_handler = drive_handler  
  
    def can_run(self) -> bool:  
        return True  
  
    async def act(self) -> None:  
        if DriveTask.drive_state is not None:  
            if self.drive_state == EDriveState.STOP:  
                await self.drive_handler.stop()  
            elif self.drive_state == EDriveState.FORWARD:  
                await self.drive_handler.forward(DriveTask.DEFAULT_SPEED)  
            elif self.drive_state == EDriveState.LEFT:  
                await self.drive_handler.turn_left(DriveTask.DEFAULT_SPEED)  
            elif self.drive_state == EDriveState.BACKWARDS:  
                await self.drive_handler.backward(DriveTask.DEFAULT_SPEED)  
            elif self.drive_state == EDriveState.RIGHT:  
                await self.drive_handler.turn_right(DriveTask.DEFAULT_SPEED)  
  
        await self.thread_handler.sleep(0.02)  
  
    @property  
    def name(self) -> str:  
        return 'Drive'

Een voorbeeld van hoe je de robot nu vooruit kan laten rijden:

def act_drive_forward(self):  
    DriveTask.drive_state = EDriveState.FORWARD

Service config

Zoals je in bovenstaande code (en in het voorbeeld van Farm-ng) ziet moet je een service_config.json bestandje toevoegen in je res mapje. Dit is hier de inhoud van:

{  
    "name": "canbus",  
    "port": 6001,  
    "host": "localhost",  
    "log_level": "INFO"  
}

Wiskunde

Voor de mensen die niet meer weten wat ‘radian’ inhoud:

En hier een uitleg over hoe je in Python kan rekenen met graden van en naar radialen:

MethodeWat doet het?
math.radians(x)Converts degrees to radians
math.degrees(x)Converts radians to degrees