From 18d83642adcee2b90604854624fee87f1ee5c92b Mon Sep 17 00:00:00 2001 From: Cameron Brown Date: Sat, 21 Sep 2024 18:40:04 -0400 Subject: [PATCH] docs: Add electrical_protocol page, add Fletcher's checksum implementation in docs, add documentation for ROSSerialDevice in electrical_protocol --- docs/reference/can.rst | 132 --------------- docs/reference/electrical_protocol.rst | 154 ++++++++++++++++++ docs/reference/index.rst | 3 +- .../electrical_protocol/__init__.py | 15 +- .../electrical_protocol/driver.py | 68 +++++++- .../electrical_protocol/packet.py | 5 + .../mil_usb_to_can/sub9/device.py | 22 ++- .../mil_usb_to_can/sub9/sub9_driver.py | 2 +- 8 files changed, 250 insertions(+), 151 deletions(-) create mode 100644 docs/reference/electrical_protocol.rst diff --git a/docs/reference/can.rst b/docs/reference/can.rst index 84d6b1de1..f08e7d75b 100644 --- a/docs/reference/can.rst +++ b/docs/reference/can.rst @@ -195,117 +195,6 @@ CommandPacket .. automodule:: mil_usb_to_can.sub9 .. currentmodule:: mil_usb_to_can.sub9 -Packet Format -~~~~~~~~~~~~~ -In order to reliably communicate with the USB to CAN board, a consistent packet format -must be defined. - -Below is the USBtoCAN Packet Format. This packet format wraps every message being -sent over the serial connection to the USB to CAN board from ROS. - -.. list-table:: USBtoCAN Packet Format - :header-rows: 1 - - * - Name - - Length - - Description - * - Sync character 1 (``0x37``) - - 1 byte - - First sync character indicating the start of packets. - * - Sync character 2 (``0x01``) - - 1 byte - - Second sync character indicating the start of packets. - * - Class ID - - 1 byte - - Message class. Determines the family of messages the packet belongs to. - * - Subclass ID - - 1 byte - - Message subclass. In combination with class, determines specific qualities - of message. - * - Payload Length - - 2 bytes - - Length of payload. - * - Payload - - 0-65535 bytes - - Payload. Meaning of payload is determined by specific packet class/subclass. - * - Checksum A - - 1 byte - - First byte of Fletcher's checksum. - * - Checksum B - - 1 byte - - Second byte of Fletcher's checksum. - -Checksums -~~~~~~~~~ -All messages contain a checksum to help verify data integrity. However, receiving -packets also have a special byte containing a slightly modified checksum formula. - -The checksum in all packets is found by adding up all bytes in the byte string, -including the start/end flags, and then using modulo 16 on this result. - -Packet Listing -~~~~~~~~~~~~~~ -Below is a listing of all available packets. The payload format is the format -used by the :mod:`struct` module. For more information, see the Python documentation -on the :ref:`list of format characters`, and their corresponding -byte length. - -+------------+--------------+----------------+-------------------------------------------------------------------------+ -| Message ID | Subclass ID | Payload Format | Class | -+============+==============+================+=========================================================================+ -| 0x00 | 0x00 | Empty | :class:`mil_usb_to_can.sub9.NackPacket` | -+ (Meta) +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x01 | Empty | :class:`mil_usb_to_can.sub9.AckPacket` | -+------------+--------------+----------------+-------------------------------------------------------------------------+ -| 0x01 | 0x00 | Empty | :class:`sub8_thrust_and_kill_board.HeartbeatPacket` | -+ (Sub8 +--------------+----------------+-------------------------------------------------------------------------+ -| Thrust/ | 0x01 | ``Bf`` | :class:`sub8_thrust_and_kill_board.ThrustSetPacket` | -+ Kill) +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x02 | ``B`` | :class:`sub8_thrust_and_kill_board.KillSetPacket` | -+ +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x03 | ``B`` | :class:`sub8_thrust_and_kill_board.KillReceivePacket` | -+------------+--------------+----------------+-------------------------------------------------------------------------+ -| 0x02 | 0x00 | Empty | :class:`sub9_thrust_and_kill_board.HeartbeatSetPacket` | -+ (Sub9 +--------------+----------------+-------------------------------------------------------------------------+ -| Thrust/ | 0x01 | Empty | :class:`sub9_thrust_and_kill_board.HeartbeatReceivePacket` | -+ Kill) +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x02 | ``Bf`` | :class:`sub9_thrust_and_kill_board.ThrustSetPacket` | -+ +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x03 | ``B`` | :class:`sub9_thrust_and_kill_board.KillSetPacket` | -+ +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x04 | ``B`` | :class:`sub9_thrust_and_kill_board.KillReceivePacket` | -+------------+--------------+----------------+-------------------------------------------------------------------------+ -| 0x03 | 0x00 | Empty | :class:`sub8_battery_monitor_board.BatteryPollRequestPacket` | -+ (Battery +--------------+----------------+-------------------------------------------------------------------------+ -| Monitor) | 0x01 | ``ffff`` | :class:`sub8_battery_monitor_board.BatteryPollResponsePacket` | -+------------+--------------+----------------+-------------------------------------------------------------------------+ -| 0x04 | 0x00 | ``BB`` | :class:`sub_actuator_board.ActuatorSetPacket` | -+ (Actuator +--------------+----------------+-------------------------------------------------------------------------+ -| Board) | 0x01 | Empty | :class:`sub_actuator_board.ActuatorPollRequestPacket` | -+ +--------------+----------------+-------------------------------------------------------------------------+ -| | 0x02 | ``B`` | :class:`sub_actuator_board.ActuatorPollResponsePacket` | -+------------+--------------+----------------+-------------------------------------------------------------------------+ -| 0x05 | 0x00 | Empty | :class:`sub9_system_status_board.SetLedRequestPacket` | -| (System | | | | -| Status) | | | | -+------------+--------------+----------------+-------------------------------------------------------------------------+ - -Exceptions -~~~~~~~~~~ - -Exception Hierarchy -""""""""""""""""""" -.. currentmodule:: mil_usb_to_can.sub9 - -.. exception_hierarchy:: - - - :exc:`ChecksumException` - -Exception List -""""""""""""""""""" -.. autoclass:: mil_usb_to_can.sub9.ChecksumException - :members: - CANDeviceHandle ~~~~~~~~~~~~~~~ .. attributetable:: mil_usb_to_can.sub9.CANDeviceHandle @@ -319,24 +208,3 @@ SimulatedCANDeviceHandle .. autoclass:: mil_usb_to_can.sub9.SimulatedCANDeviceHandle :members: - -Packet -~~~~~~ -.. attributetable:: mil_usb_to_can.sub9.Packet - -.. autoclass:: mil_usb_to_can.sub9.Packet - :members: - -NackPacket -~~~~~~~~~~ -.. attributetable:: mil_usb_to_can.sub9.NackPacket - -.. autoclass:: mil_usb_to_can.sub9.NackPacket - :members: - -AckPacket -~~~~~~~~~ -.. attributetable:: mil_usb_to_can.sub9.AckPacket - -.. autoclass:: mil_usb_to_can.sub9.AckPacket - :members: diff --git a/docs/reference/electrical_protocol.rst b/docs/reference/electrical_protocol.rst new file mode 100644 index 000000000..18e0488ec --- /dev/null +++ b/docs/reference/electrical_protocol.rst @@ -0,0 +1,154 @@ +:mod:`electrical_protocol` - Electrical-software communication standard +----------------------------------------------------------------------- + +.. automodule:: electrical_protocol +.. currentmodule:: electrical_protocol + +Packet Format +~~~~~~~~~~~~~ +In order to reliably communicate with an electrical board, a consistent packet format +must be defined. + +Below is the electrical protocol packet format. This packet format wraps every message being +sent over the serial connection to the USB to CAN board from ROS. + +.. list-table:: Packet Format + :header-rows: 1 + + * - Name + - Length + - Description + * - Sync character 1 (``0x37``) + - 1 byte + - First sync character indicating the start of packets. + * - Sync character 2 (``0x01``) + - 1 byte + - Second sync character indicating the start of packets. + * - Class ID + - 1 byte + - Message class. Determines the family of messages the packet belongs to. + * - Subclass ID + - 1 byte + - Message subclass. In combination with class, determines specific qualities + of message. + * - Payload Length + - 2 bytes + - Length of payload. + * - Payload + - 0-65535 bytes + - Payload. Meaning of payload is determined by specific packet class/subclass. + * - Checksum A + - 1 byte + - First byte of Fletcher's checksum. + * - Checksum B + - 1 byte + - Second byte of Fletcher's checksum. + +Checksums +~~~~~~~~~ +All messages contain a checksum to help verify data integrity. However, receiving +packets also have a special byte containing a slightly modified checksum formula. + +The checksum in all packets is found by adding up all bytes in the byte string, +including the start/end flags, and then using modulo 16 on this result. The +library checksum is implemented like so: + +.. literalinclude:: ../../mil_common/drivers/electrical_protocol/electrical_protocol/packet.py + :pyobject: Packet._calculate_checksum + +Packet Listing +~~~~~~~~~~~~~~ +Below is a listing of all available packets. The payload format is the format +used by the :mod:`struct` module. For more information, see the Python documentation +on the :ref:`list of format characters`, and their corresponding +byte length. + ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| Message ID | Subclass ID | Payload Format | Class | ++============+==============+================+=========================================================================+ +| 0x00 | 0x00 | Empty | :class:`electrical_protocol.NackPacket` | ++ (Meta) +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x01 | Empty | :class:`electrical_protocol.AckPacket` | ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| 0x01 | 0x00 | Empty | :class:`sub8_thrust_and_kill_board.HeartbeatPacket` | ++ (Sub8 +--------------+----------------+-------------------------------------------------------------------------+ +| Thrust/ | 0x01 | ``Bf`` | :class:`sub8_thrust_and_kill_board.ThrustSetPacket` | ++ Kill) +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x02 | ``B`` | :class:`sub8_thrust_and_kill_board.KillSetPacket` | ++ +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x03 | ``B`` | :class:`sub8_thrust_and_kill_board.KillReceivePacket` | ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| 0x02 | 0x00 | Empty | :class:`sub9_thrust_and_kill_board.HeartbeatSetPacket` | ++ (Sub9 +--------------+----------------+-------------------------------------------------------------------------+ +| Thrust/ | 0x01 | Empty | :class:`sub9_thrust_and_kill_board.HeartbeatReceivePacket` | ++ Kill) +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x02 | ``Bf`` | :class:`sub9_thrust_and_kill_board.ThrustSetPacket` | ++ +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x03 | ``B`` | :class:`sub9_thrust_and_kill_board.KillSetPacket` | ++ +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x04 | ``B`` | :class:`sub9_thrust_and_kill_board.KillReceivePacket` | ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| 0x03 | 0x00 | Empty | :class:`sub8_battery_monitor_board.BatteryPollRequestPacket` | ++ (Battery +--------------+----------------+-------------------------------------------------------------------------+ +| Monitor) | 0x01 | ``ffff`` | :class:`sub8_battery_monitor_board.BatteryPollResponsePacket` | ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| 0x04 | 0x00 | ``BB`` | :class:`sub_actuator_board.ActuatorSetPacket` | ++ (Actuator +--------------+----------------+-------------------------------------------------------------------------+ +| Board) | 0x01 | Empty | :class:`sub_actuator_board.ActuatorPollRequestPacket` | ++ +--------------+----------------+-------------------------------------------------------------------------+ +| | 0x02 | ``B`` | :class:`sub_actuator_board.ActuatorPollResponsePacket` | ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| 0x05 | 0x00 | Empty | :class:`sub9_system_status_board.SetLedRequestPacket` | +| (System | | | | +| Status) | | | | ++------------+--------------+----------------+-------------------------------------------------------------------------+ +| 0x10 | 0x00 | ``?B`` | :class:`navigator_pico_kill_board.KillSetPacket` | +| (NaviGator +--------------+----------------+-------------------------------------------------------------------------+ +| Temporary | 0x01 | ``?B`` | :class:`navigator_pico_kill_board.KillReceivePacket` | +| Pico Kill | | | | +| Board) | | | | ++------------+--------------+----------------+-------------------------------------------------------------------------+ + +Exceptions +~~~~~~~~~~ + +Exception Hierarchy +""""""""""""""""""" +.. currentmodule:: electrical_protocol + +.. exception_hierarchy:: + + - :exc:`ChecksumException` + +Exception List +""""""""""""""""""" +.. autoclass:: electrical_protocol.ChecksumException + :members: + +ROSSerialDevice +~~~~~~~~~~~~~~~ +.. attributetable:: electrical_protocol.ROSSerialDevice + +.. autoclass:: electrical_protocol.ROSSerialDevice + :members: + +Packet +~~~~~~ +.. attributetable:: electrical_protocol.Packet + +.. autoclass:: electrical_protocol.Packet + :members: + +NackPacket +~~~~~~~~~~ +.. attributetable:: electrical_protocol.NackPacket + +.. autoclass:: electrical_protocol.NackPacket + :members: + +AckPacket +~~~~~~~~~ +.. attributetable:: electrical_protocol.AckPacket + +.. autoclass:: electrical_protocol.AckPacket + :members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index c63581a0c..e9d85897a 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -20,13 +20,14 @@ by MIL. These subsystems relate to a variety of processes. mission/index rviz pcodar + electrical_protocol + can resources rc alarms pathplanning battery passivesonar - can vision poi pneumatic diff --git a/mil_common/drivers/electrical_protocol/electrical_protocol/__init__.py b/mil_common/drivers/electrical_protocol/electrical_protocol/__init__.py index c799e01b5..0bec1b8c3 100644 --- a/mil_common/drivers/electrical_protocol/electrical_protocol/__init__.py +++ b/mil_common/drivers/electrical_protocol/electrical_protocol/__init__.py @@ -1,2 +1,15 @@ +""" +The :mod:`electrical_protocol` module is meant to serve as a bridge between +a software library and a physical electrical device. The library is not dependent +on a particular communication standard (UART, CAN, etc.) being used. Rather, the +library just provides a packet structure that can be used by any project, and a +list of packets using this structure. + +The library also provides a simple driver for UART/serial communication with a +physical electrical device -- :class:`~.electrical_protocol.ROSSerialDevice`. +If a CAN standard is desired, the :class:`mil_usb_to_can.sub9.CANDeviceHandle` +class can be used, which supports packets built through this library. +""" + from .driver import ROSSerialDevice -from .packet import AckPacket, NackPacket, Packet +from .packet import AckPacket, ChecksumException, NackPacket, Packet diff --git a/mil_common/drivers/electrical_protocol/electrical_protocol/driver.py b/mil_common/drivers/electrical_protocol/electrical_protocol/driver.py index be282a7ad..5e7c669ae 100644 --- a/mil_common/drivers/electrical_protocol/electrical_protocol/driver.py +++ b/mil_common/drivers/electrical_protocol/electrical_protocol/driver.py @@ -17,7 +17,15 @@ class ROSSerialDevice(Generic[SendPackets, RecvPackets]): """ Represents a generic serial device, which is expected to be the main component - of an individual ROS node.""" + of an individual ROS node. + + Attributes: + port (Optional[str]): The port used for the serial connection, if provided. + baudrate (Optional[int]): The baudrate to use with the device, if provided. + device (Optional[serial.Serial]): The serial class used to communicate with + the device. + rate (float): The reading rate of the device, in Hertz. Set to `20` by default. + """ device: serial.Serial | None _recv_T: Any @@ -30,6 +38,14 @@ def is_open(self) -> bool: return bool(self.device) and self.device.is_open def __init__(self, port: str | None, baudrate: int | None) -> None: + """ + Arguments: + port (Optional[str]): The serial port to connect to. If ``None``, connection + will not be established on initialization; rather, the user can use + :meth:`~.connect` to connect later. + baudrate (Optional[int]): The baudrate to connect with. If ``None`` and + a port is specified, then 115200 is assumed. + """ self.port = port self.baudrate = baudrate if port: @@ -51,6 +67,14 @@ def __init_subclass__(cls) -> None: cls._recv_T = get_args(cls.__orig_bases__[0])[1] # type: ignore def connect(self, port: str, baudrate: int) -> None: + """ + Connects to the port with the given baudrate. If the device is already + connected, the input and output buffers will be flushed. + + Arguments: + port (str): The serial port to connect to. + baudrate (int): The baudrate to connect with. + """ self.port = port self.baudrate = baudrate self.device = serial.Serial(port, baudrate, timeout=0.1) @@ -60,7 +84,10 @@ def connect(self, port: str, baudrate: int) -> None: self.device.reset_output_buffer() def close(self) -> None: - rospy.loginfo("Closing device...") + """ + Closes the serial device. + """ + rospy.loginfo("Closing serial device...") if not self.device: raise RuntimeError("Device is not connected.") else: @@ -72,12 +99,26 @@ def close(self) -> None: ) self.device.close() - def write(self, bytes: bytes) -> None: + def write(self, data: bytes) -> None: + """ + Writes a series of raw bytes to the device. This method should rarely be + used; using :meth:`~.send_packet` is preferred because of the guarantees + it provides through the packet class. + + Arguments: + data (bytes): The data to send. + """ if not self.device: raise RuntimeError("Device is not connected.") - self.device.write(bytes) + self.device.write(data) def send_packet(self, packet: SendPackets) -> None: + """ + Sends a given packet to the device. + + Arguments: + packet (:class:`~.Packet`): The packet to send. + """ with self.lock: self.write(bytes(packet)) @@ -118,12 +159,24 @@ def _correct_type(self, provided: Any) -> bool: return isinstance(provided, self._recv_T) def adjust_read_rate(self, rate: float) -> None: + """ + Sets the reading rate to a specified amount. + + Arguments: + rate (float): The reading speed to use, in hz. + """ self.timer.shutdown() self.rate = min(rate, 1_000) rospy.logerr(f"Setting rate to {rate}") self.timer = rospy.Timer(rospy.Duration(1.0 / rate), self._process_buffer) # type: ignore def scale_read_rate(self, scale: float) -> None: + """ + Scales the reading rate of the device handle by some factor. + + Arguments: + scale (float): The amount to scale the reading rate by. + """ self.adjust_read_rate(self.rate * scale) def _read_packet(self) -> bool: @@ -170,4 +223,11 @@ def _process_buffer(self, _: rospy.timer.TimerEvent) -> None: @abc.abstractmethod def on_packet_received(self, packet: RecvPackets) -> None: + """ + Abstract method to be implemented by subclasses for handling packets + sent by the physical electrical board. + + Arguments: + packet (:class:`~.Packet`): The packet that is received. + """ pass diff --git a/mil_common/drivers/electrical_protocol/electrical_protocol/packet.py b/mil_common/drivers/electrical_protocol/electrical_protocol/packet.py index 8b5c75ac6..d69ed073b 100644 --- a/mil_common/drivers/electrical_protocol/electrical_protocol/packet.py +++ b/mil_common/drivers/electrical_protocol/electrical_protocol/packet.py @@ -151,6 +151,11 @@ def __post_init__(self): @classmethod def _calculate_checksum(cls, data: bytes) -> tuple[int, int]: + """ + Used to calculate the Fletcher's checksum for a series of bytes. When + calculating the checksum for a new packet, the start bytes/sync characters + should not be included. + """ sum1, sum2 = 0, 0 for byte in data: sum1 = (sum1 + byte) % 255 diff --git a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/device.py b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/device.py index 38db7e776..52d84680c 100644 --- a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/device.py +++ b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/device.py @@ -2,9 +2,9 @@ from typing import TYPE_CHECKING -from electrical_protocol import Packet - if TYPE_CHECKING: + import electrical_protocol + from .sub9_driver import SimulatedUSBtoCANStream, USBtoCANDriver @@ -16,15 +16,13 @@ class SimulatedCANDeviceHandle: Child classes can inherit from this class to implement a simulated CAN device. Attributes: - inbound_packets (type[Packet]): The types of packets listened to by this device. - Packets of this type will be routed to the :meth:`~.on_data` method of - the device handle. + inbound_packets (type[electrical_protocol.Packet]): The types of packets listened to by this device. Packets of this type will be routed to the :meth:`~.on_data` method of the device handle. """ def __init__( self, sim_board: SimulatedUSBtoCANStream, - inbound_packets: list[type[Packet]], + inbound_packets: list[type[electrical_protocol.Packet]], ): self._sim_board = sim_board self.inbound_packets = inbound_packets @@ -35,7 +33,7 @@ def send_data(self, data: bytes): """ self._sim_board.send_to_bus(data) - def on_data(self, packet: Packet): + def on_data(self, packet: electrical_protocol.Packet): """ Called when an relevant incoming packet is received over the serial connection. Relevant packets are those listed in :attr:`~.inbound_packets`. @@ -43,7 +41,7 @@ def on_data(self, packet: Packet): Partial data (ie, incomplete packets) are not sent through this event. Args: - packet (Packet): The incoming packet. + packet (electrical_protocol.Packet): The incoming packet. """ del packet @@ -63,7 +61,7 @@ def __init__(self, driver: USBtoCANDriver): """ self._driver = driver - def on_data(self, data: Packet): + def on_data(self, data: electrical_protocol.Packet): """ Called when a return packet is sent over the serial connection. In the USB to CAN protocol, it is assumed that packets will be returned to the @@ -74,15 +72,15 @@ def on_data(self, data: Packet): Partial data (ie, incomplete packets) are not sent through this event. Args: - packet (Packet): The incoming packet. + packet (electrical_protocol.Packet): The incoming packet. """ del data - def send_data(self, data: Packet): + def send_data(self, data: electrical_protocol.Packet): """ Sends a packet over the serial connection. Args: - data (Packet): The packet to send. + data (electrical_protocol.Packet): The packet to send. """ return self._driver.send_data(self, data) diff --git a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/sub9_driver.py b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/sub9_driver.py index 2881a8d7c..2303b0840 100755 --- a/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/sub9_driver.py +++ b/mil_common/drivers/mil_usb_to_can/mil_usb_to_can/sub9/sub9_driver.py @@ -39,7 +39,7 @@ def __init__( ): """ Args: - devices (List[Tuple[Type[SimulatedCANDeviceHandle], List[Type[Packet]]]]): List of + devices (List[Tuple[Type[SimulatedCANDeviceHandle], List[Type[electrical_protocol.Packet]]]]): List of the simulated device handles, along with the list of packets each handle is listening for. """