""" A wrapper for the Webots servo device. The servo will apply a small amount of variation to the power setting to simulate inaccuracies in the servo. """ from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING from sbot_interface.devices.util import ( WebotsDevice, add_jitter, get_globals, get_robot_device, map_to_range, ) if TYPE_CHECKING: from controller import PositionSensor MAX_POSITION = 4000 MIN_POSITION = 500 SERVO_MAX = 2000 SERVO_MIN = 1000 class BaseServo(ABC): """The base class for all the servos that can be connected to the Servo board.""" @abstractmethod def disable(self) -> None: """Disable the servo.""" pass @abstractmethod def set_position(self, value: int) -> None: """ Set the position of the servo. Position is the pulse width in microseconds. """ pass @abstractmethod def get_position(self) -> int: """Return the current position of the servo.""" pass @abstractmethod def get_current(self) -> int: """Return the current draw of the servo in mA.""" pass @abstractmethod def enabled(self) -> bool: """Return whether the servo is enabled.""" pass class NullServo(BaseServo): """A servo that does nothing. Used for servos that are not connected.""" def __init__(self) -> None: self.position = 1500 self._enabled = False def disable(self) -> None: """Disable the servo.""" self._enabled = False def set_position(self, value: int) -> None: """ Set the position of the servo. Position is the pulse width in microseconds. """ self.position = value self._enabled = True def get_position(self) -> int: """ Return the current position of the servo. Position is the pulse width in microseconds. """ return self.position def get_current(self) -> int: """Return the current draw of the servo in mA.""" return 0 def enabled(self) -> bool: """Return whether the servo is enabled.""" return self._enabled class Servo(BaseServo): """A servo connected to the Servo board.""" def __init__(self, device_name: str) -> None: self.position = (SERVO_MAX + SERVO_MIN) // 2 # TODO use setAvailableForce to simulate disabled self._enabled = False g = get_globals() self._device = get_robot_device(g.robot, device_name, WebotsDevice.Motor) self._pos_sensor: PositionSensor | None = self._device.getPositionSensor() # type: ignore[no-untyped-call] self._max_position = self._device.getMaxPosition() self._min_position = self._device.getMinPosition() if self._pos_sensor is not None: self._pos_sensor.enable(g.timestep) def disable(self) -> None: """Disable the servo.""" self._enabled = False def set_position(self, value: int) -> None: """ Set the position of the servo. Position is the pulse width in microseconds. """ # Apply a small amount of variation to the power setting to simulate # inaccuracies in the servo value = int(add_jitter(value, (SERVO_MIN, SERVO_MAX), std_dev_percent=0.5)) self._device.setPosition(map_to_range( value, (SERVO_MIN, SERVO_MAX), (self._min_position + 0.001, self._max_position - 0.001), )) self.position = value self._enabled = True def get_position(self) -> int: """ Return the current position of the servo. Position is the pulse width in microseconds. """ if self._pos_sensor is not None: self.position = int(map_to_range( self._pos_sensor.getValue(), (self._min_position + 0.001, self._max_position - 0.001), (MIN_POSITION, MAX_POSITION), )) return self.position def get_current(self) -> int: """Return the current draw of the servo in mA.""" # TODO calculate from torque feedback return 0 def enabled(self) -> bool: """Return whether the servo is enabled.""" return self._enabled