""" A simulator for the SRv4 Servo Board. Provides a message parser that simulates the behavior of a servo board. Based on the Servo Board v4.4 firmware. """ from __future__ import annotations import logging from sbot_interface.devices.servo import MAX_POSITION, MIN_POSITION, BaseServo LOGGER = logging.getLogger(__name__) class ServoBoard: """ A simulator for the SRv4 Servo Board. :param servos: A list of simulated servos connected to the servo board. The list is indexed by the servo number. :param asset_tag: The asset tag to report for the servo board. :param software_version: The software version to report for the servo board. """ def __init__(self, servos: list[BaseServo], asset_tag: str, software_version: str = '4.4'): self.servos = servos self.asset_tag = asset_tag self.software_version = software_version self.watchdog_fail = False self.pgood = True def handle_command(self, command: str) -> str: """ Process a command string and return the response. Executes the appropriate action on any specified servos automatically. :param command: The command string to process. :return: The response to the command. """ args = command.split(':') if args[0] == '*IDN?': return f'Student Robotics:SBv4B:{self.asset_tag}:{self.software_version}' elif args[0] == '*STATUS?': return f"{self.watchdog_fail:d}:{self.pgood:d}" elif args[0] == '*RESET': LOGGER.info(f'Resetting servo board {self.asset_tag}') for servo in self.servos: servo.disable() return 'ACK' elif args[0] == 'SERVO': if len(args) < 2: return 'NACK:Missing servo number' if args[1] == 'I?': return str(self.current()) elif args[1] == 'V?': return '5000' try: servo_number = int(args[1]) except ValueError: return 'NACK:Invalid servo number' if not (0 <= servo_number < len(self.servos)): return 'NACK:Invalid servo number' if len(args) < 3: return 'NACK:Missing servo command' if args[2] == 'DISABLE': LOGGER.info(f'Disabling servo {servo_number} on board {self.asset_tag}') self.servos[servo_number].disable() return 'ACK' elif args[2] == 'GET?': return str(self.servos[servo_number].get_position()) elif args[2] == 'SET': if len(args) < 4: return 'NACK:Missing servo setpoint' try: setpoint = int(args[3]) except ValueError: return 'NACK:Invalid servo setpoint' if not (MIN_POSITION <= setpoint <= MAX_POSITION): return 'NACK:Invalid servo setpoint' LOGGER.info( f'Setting servo {servo_number} on board {self.asset_tag} to {setpoint}' ) self.servos[servo_number].set_position(setpoint) return 'ACK' else: return 'NACK:Unknown servo command' else: return f'NACK:Unknown command {command.strip()}' return 'NACK:Command failed' def current(self) -> int: """ Get the total current draw of all servos. :return: The total current draw of all servos in mA. """ return sum(servo.get_current() for servo in self.servos)