[testing] add motor calibration test

This commit is contained in:
Samuel Sadok
2020-01-25 15:48:50 +01:00
parent 89588dbf97
commit 6637099308
3 changed files with 175 additions and 48 deletions
@@ -0,0 +1,70 @@
import test_runner
import time
from math import pi
import os
from fibre.utils import Logger
from test_runner import AxisTestContext, MotorTestContext, test_assert_eq, test_assert_no_error, request_state
from odrive.enums import *
def modpm(val, range):
return ((val + (range / 2)) % range) - (range / 2)
class TestMotorCalibration():
"""
Runs the motor calibration and checks if the measurements match the expectation.
"""
def is_compatible(self, axis_ctx: AxisTestContext, motor_ctx: MotorTestContext):
return axis_ctx.yaml == motor_ctx.yaml['name'] # check if connected
def run_test(self, axis_ctx: AxisTestContext, motor_ctx: MotorTestContext, logger: Logger):
# reset old calibration values
axis_ctx.handle.motor.config.phase_resistance = 0.0
axis_ctx.handle.motor.config.phase_inductance = 0.0
axis_ctx.handle.clear_errors()
# run calibration
request_state(axis_ctx, AXIS_STATE_MOTOR_CALIBRATION)
time.sleep(6)
test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE)
test_assert_no_error(axis_ctx)
# check if measurements match expectation
test_assert_eq(axis_ctx.handle.motor.config.phase_resistance, float(motor_ctx.yaml['phase-resistance']), accuracy=0.2)
test_assert_eq(axis_ctx.handle.motor.config.phase_inductance, float(motor_ctx.yaml['phase-inductance']), accuracy=0.5)
class TestDisconnectedMotorCalibration():
"""
Tests if the motor calibration fails as expected if the phases are floating.
"""
def is_compatible(self, axis_ctx: AxisTestContext):
return axis_ctx.yaml == 'floating'
def run_test(self, axis_ctx: AxisTestContext, logger: Logger):
axis = axis_ctx.handle
# reset old calibration values
axis_ctx.handle.motor.config.phase_resistance = 0.0
axis_ctx.handle.motor.config.phase_inductance = 0.0
axis_ctx.handle.clear_errors()
# run test
request_state(axis_ctx, AXIS_STATE_MOTOR_CALIBRATION)
time.sleep(6)
test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE)
test_assert_eq(axis_ctx.handle.error, errors.axis.ERROR_MOTOR_FAILED)
test_assert_eq(axis_ctx.handle.motor.error, errors.motor.ERROR_PHASE_RESISTANCE_OUT_OF_RANGE)
if __name__ == '__main__':
test_runner.run([
TestMotorCalibration(),
TestDisconnectedMotorCalibration()
])
+102 -35
View File
@@ -46,6 +46,7 @@ class ODriveTestContext():
self.yaml = yaml
#self.axes = [AxisTestContext(None), AxisTestContext(None)]
self.encoders = [EncoderTestContext(self, 0, None), EncoderTestContext(self, 1, None)]
self.axes = [AxisTestContext(self, 0, None), AxisTestContext(self, 1, None)]
def __repr__(self):
return self.yaml['name']
@@ -64,6 +65,32 @@ class ODriveTestContext():
# axis_ctx.handle = self.handle.__dict__['axis{}'.format(axis_idx)]
for encoder_idx, encoder_ctx in enumerate(self.encoders):
encoder_ctx.handle = self.handle.__dict__['axis{}'.format(encoder_idx)].encoder
# TODO: distinguish between axis and motor context
for axis_idx, axis_ctx in enumerate(self.axes):
axis_ctx.handle = self.handle.__dict__['axis{}'.format(axis_idx)]
class MotorTestContext():
def __init__(self, yaml: dict):
self.yaml = yaml
def __repr__(self):
return self.yaml['name']
def make_available(self, logger: Logger):
pass
class AxisTestContext():
def __init__(self, odrv_ctx: ODriveTestContext, num: int, yaml: dict):
self.handle = None
self.yaml = odrv_ctx.yaml[f'motor{num}'] # TODO: this is bad naming
self.odrv_ctx = odrv_ctx
self.num = num
def __repr__(self):
return str(self.odrv_ctx) + '.axis' + str(self.num)
def make_available(self, logger: Logger):
self.odrv_ctx.make_available(logger)
class EncoderTestContext():
def __init__(self, odrv_ctx: ODriveTestContext, num: int, yaml: dict):
@@ -93,6 +120,35 @@ class CANTestContext():
# Helper functions ------------------------------------------------------------#
def request_state(axis_ctx: AxisTestContext, state, expect_success=True):
axis_ctx.handle.requested_state = state
time.sleep(0.001)
if expect_success:
test_assert_eq(axis_ctx.handle.current_state, state)
else:
test_assert_eq(axis_ctx.handle.current_state, AXIS_STATE_IDLE)
test_assert_eq(axis_ctx.handle.error, AXIS_ERROR_INVALID_STATE)
axis_ctx.handle.error = AXIS_ERROR_NONE # reset error
def get_errors(axis_ctx: AxisTestContext):
errors = []
if axis_ctx.handle.motor.error != 0:
errors.append("motor failed with error 0x{:04X}".format(axis_ctx.handle.motor.error))
if axis_ctx.handle.encoder.error != 0:
errors.append("encoder failed with error 0x{:04X}".format(axis_ctx.handle.encoder.error))
if axis_ctx.handle.sensorless_estimator.error != 0:
errors.append("sensorless_estimator failed with error 0x{:04X}".format(axis_ctx.handle.sensorless_estimator.error))
if axis_ctx.handle.error != 0:
errors.append("axis failed with error 0x{:04X}".format(axis_ctx.handle.error))
elif len(errors) > 0:
errors.append("and by the way: axis reports no error even though there is one")
return errors
def test_assert_no_error(axis_ctx: AxisTestContext):
errors = get_errors(axis_ctx)
if len(errors) > 0:
raise TestFailed("\n".join(errors))
def yaml_to_test_objects(test_rig_yaml: dict, logger: Logger):
available_test_objects = {}
@@ -106,10 +162,15 @@ def yaml_to_test_objects(test_rig_yaml: dict, logger: Logger):
add_component(odrv_ctx)
for enc_ctx in odrv_ctx.encoders:
add_component(enc_ctx)
for axis_ctx in odrv_ctx.axes:
add_component(axis_ctx)
elif component_yaml['type'] == 'generalpurpose':
for (k, v) in [(k, v) for (k, v) in component_yaml.items() if k.startswith("can")]:
can_ctx = CANTestContext({'id': k, 'bus': v})
add_component(can_ctx)
elif component_yaml['type'] == 'motor':
motor_ctx = MotorTestContext(component_yaml)
add_component(motor_ctx)
else:
logger.warn('test rig has unsupported component ' + component_yaml['type'])
continue
@@ -158,43 +219,49 @@ def program_teensy(hex_file_path, program_gpio: int, logger: Logger):
run_shell(["teensy_loader_cli", "-mmcu=imxrt1062", "-w", hex_file_path], logger, timeout = 5)
time.sleep(0.5) # give it some time to boot
def run(test_case):
# Parse arguments
parser = argparse.ArgumentParser(description='ODrive automated test tool\n')
parser.add_argument("--ignore", metavar='DEVICE', action='store', nargs='+',
help="Ignore (disable) one or more components of the test rig")
# TODO: implement
parser.add_argument("--test-rig-yaml", type=argparse.FileType('r'), required=True,
help="test rig YAML file")
parser.set_defaults(ignore=[])
def run(test_cases):
if not isinstance(test_cases, list):
test_cases = [test_cases]
args = parser.parse_args()
for test_case in test_cases:
# Compile a list of list of potential objects that might be compatible with this
# test
possible_parameters = []
sig = signature(test_case.is_compatible)
for param_name in sig.parameters:
param_type = sig.parameters[param_name].annotation
possible_parameters.append(available_test_objects[param_type])
# Load objects
test_rig_yaml = yaml.load(args.test_rig_yaml, Loader=yaml.BaseLoader)
logger = Logger()
available_test_objects = yaml_to_test_objects(test_rig_yaml, logger)
# Compile a list of list of potential objects that might be compatible with this
# test
possible_parameters = []
sig = signature(test_case.is_compatible)
for param_name in sig.parameters:
param_type = sig.parameters[param_name].annotation
possible_parameters.append(available_test_objects[param_type])
# For each combination, check if the test is compatible with these objects
for param_combination in itertools.product(*possible_parameters):
if not test_case.is_compatible(*param_combination):
continue
for param in param_combination:
param.make_available(logger)
logger.notify('* running {} on {}...'.format(type(test_case).__name__,
[str(p) for p in param_combination]))
test_case.run_test(*param_combination, logger)
# For each combination, check if the test is compatible with these objects
for param_combination in itertools.product(*possible_parameters):
if not test_case.is_compatible(*param_combination):
continue
for param in param_combination:
param.make_available(logger)
logger.notify('* running {} on {}...'.format(type(test_case).__name__,
[str(p) for p in param_combination]))
test_case.run_test(*param_combination, logger)
logger.success('All tests passed!')
# Load test engine ------------------------------------------------------------#
# Parse arguments
parser = argparse.ArgumentParser(description='ODrive automated test tool\n')
parser.add_argument("--ignore", metavar='DEVICE', action='store', nargs='+',
help="Ignore (disable) one or more components of the test rig")
# TODO: implement
parser.add_argument("--test-rig-yaml", type=argparse.FileType('r'), required=True,
help="test rig YAML file")
parser.set_defaults(ignore=[])
args = parser.parse_args()
# Load objects
test_rig_yaml = yaml.load(args.test_rig_yaml, Loader=yaml.BaseLoader)
logger = Logger()
available_test_objects = yaml_to_test_objects(test_rig_yaml, logger)