Skip to content

Commit

Permalink
fix(hardware): change the format of the motor usage response (#12541)
Browse files Browse the repository at this point in the history
* add ids for motor usage types

* refactor the motor usage message, fields, and write a custom parser

* format

* throw in a cheeky little sleep at the end of the dump-eeprom script so that last few responses have time to process before the script exits

* bit shift motor usage values right if they're smaller than 8 bytes so the final value is accurate

* print out the motor usage value type description pretty

* don't pop the num_elements just access it when deserializing

* reduce the size of motorUsageField by 1 byte so we can pack in 5 values per can message

* format

* fix parsing the smaller length value
  • Loading branch information
ryanthecoder authored Apr 21, 2023
1 parent f22a294 commit 5efec97
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 3 deletions.
9 changes: 9 additions & 0 deletions hardware/opentrons_hardware/firmware_bindings/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,12 @@ class MoveStopCondition(int, Enum):
encoder_position = 0x4
gripper_force = 0x8
stall = 0x10


@unique
class MotorUsageValueType(int, Enum):
"""Type of motor Usage value types."""

linear_motor_distance = 0x0
left_gear_motor_distance = 0x1
right_gear_motor_distance = 0x2
68 changes: 68 additions & 0 deletions hardware/opentrons_hardware/firmware_bindings/messages/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ErrorSeverity,
MoveStopCondition,
GearMotorId,
MotorUsageValueType,
)

from opentrons_hardware.firmware_bindings.binary_constants import (
Expand Down Expand Up @@ -419,3 +420,70 @@ def __repr__(self) -> str:
except ValueError:
action = str(self.value)
return f"{self.__class__.__name__}(value={action})"


class MotorUsageTypeField(utils.BinaryFieldBase[bytes]):
"""A struct for an individual motor usage key, length, value field."""

NUM_BYTES = 11
FORMAT = f"{NUM_BYTES}s"

@property
def value(self) -> bytes:
"""The value."""
val = b""
val = val + self.key.to_bytes(2, "big")
val = val + self.length.to_bytes(1, "big")
val = val + self.usage_value.to_bytes(8, "big")
return val

@value.setter
def value(self, val: bytes) -> None:
self._key, self._length, self._usage_value = self._parse(val)

@classmethod
def _usage_field_from_bytes(cls, data: bytes, start: int, end: int) -> int:
try:
present_bytes = data[start:end]
except IndexError:
present_bytes = b"\x00"
return int.from_bytes(present_bytes, "big")

@classmethod
def _parse(cls, data: bytes) -> Tuple[int, int, int]:
key = cls._usage_field_from_bytes(data, 0, 2)
length = cls._usage_field_from_bytes(data, 2, 3)
usage_value = cls._usage_field_from_bytes(data, 3, 11)
if length < 8:
usage_value = usage_value >> (8 - length) * 8
return key, length, usage_value

@classmethod
def build(cls, data: bytes) -> "MotorUsageTypeField":
"""Create a Motor Usage Type Field from bytes"""
return cls(*cls._parse(data))

def __init__(self, key: int, length: int, usage_value: int) -> None:
"""Build."""
self._key = key
self._length = length
self._usage_value = usage_value

@property
def key(self) -> int:
"""Key that describes what kind of usage this is"""
return self._key

@property
def length(self) -> int:
"""Length of the usage data value field"""
return self._length

@property
def usage_value(self) -> int:
"""Value for this particular type of usage"""
return self._usage_value

def __repr__(self) -> str:
"""Repr."""
return f"{self.__class__.__name__}(key={MotorUsageValueType(self.key).name}, length={self.length}, usage_data={self.usage_value})"
37 changes: 34 additions & 3 deletions hardware/opentrons_hardware/firmware_bindings/messages/payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# from __future__ import annotations
from dataclasses import dataclass, field, asdict
from . import message_definitions
from typing import Iterator
from typing import Iterator, List

from .fields import (
FirmwareShortSHADataField,
Expand All @@ -27,6 +27,7 @@
MoveStopConditionField,
GearMotorIdField,
OptionalRevisionField,
MotorUsageTypeField,
)
from .. import utils

Expand Down Expand Up @@ -571,7 +572,37 @@ class SerialNumberPayload(EmptyPayload):


@dataclass(eq=False)
class GetMotorUsageResponsePayload(EmptyPayload):
class _GetMotorUsageResponsePayloadBase(EmptyPayload):
num_elements: utils.UInt8Field


@dataclass(eq=False)
class GetMotorUsageResponsePayload(_GetMotorUsageResponsePayloadBase):
"""A payload with motor lifetime usage."""

distance_um: utils.UInt64Field
@classmethod
def build(cls, data: bytes) -> "GetMotorUsageResponsePayload":
"""Build a response payload from incoming bytes.
This override is required to handle responses with multiple values.
"""
consumed = _GetMotorUsageResponsePayloadBase.get_size()
superdict = asdict(_GetMotorUsageResponsePayloadBase.build(data))
num_elements = superdict["num_elements"]
message_index = superdict.pop("message_index")

usage_values: List[MotorUsageTypeField] = []

for i in range(num_elements.value):
usage_values.append(
MotorUsageTypeField.build(
data[consumed : consumed + MotorUsageTypeField.NUM_BYTES]
)
)
consumed = consumed + MotorUsageTypeField.NUM_BYTES

inst = cls(**superdict, usage_elements=usage_values)
inst.message_index = message_index
return inst

usage_elements: List[MotorUsageTypeField]
1 change: 1 addition & 0 deletions hardware/opentrons_hardware/scripts/dump_eeprom.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def handle_read_resp(
TARGETS[args.target],
256 if args.old_version else 16384,
)
await asyncio.sleep(3)


async def dump_eeprom(messenger: CanMessenger, node: NodeId, limit: int) -> None:
Expand Down

0 comments on commit 5efec97

Please sign in to comment.