From d202491e9a201355995c7ba48517ae58cf7f66f1 Mon Sep 17 00:00:00 2001 From: Samuel Sadok Date: Tue, 10 Apr 2018 15:41:34 -0700 Subject: [PATCH] make tests more flexible --- docs/testing.md | 23 ++++++++++++++ tools/odrive/tests.py | 17 ++++++++--- tools/run_tests.py | 71 +++++++++++++++++++++++++++---------------- tools/test-rig.yaml | 16 ++++++---- 4 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 docs/testing.md diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..347db2b1 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,23 @@ +# Automated Testing + +This section describes how to use the automated testing facilities. +You don't have to do this as an end user. + +They test the following aspects: + - System functions (communication interfaces, configuration storage) + - Functionality of the motor controller and state machine + - High speed and high load conditions + +The testing facility consists of the following components: + * **Test rig:** In the simplest case this can be a single ODrive with a single motor and encoder pair. Can also be multiple ODrives with multiple axes, some of which may be mechanically coupled. + * **Test host:** The PC on which the test script runs. All ODrives must be connected to the test host via USB. + * **test-rig.yaml:** Describes your test rig. Make sure all values are correct. Incorrect values may physically break or fry your test setup. + * **run_tests.py:** This is the main script that runs all the tests. + +## How to run + +Example: + +``` +./run_tests.py --skip-boring-tests --ignore top-odrive.yellow bottom-odrive.yellow +``` diff --git a/tools/odrive/tests.py b/tools/odrive/tests.py index 64237768..6e4d9cd6 100644 --- a/tools/odrive/tests.py +++ b/tools/odrive/tests.py @@ -26,7 +26,7 @@ class ODriveTestContext(): self.name = name self.axes = [] for axis_idx, axis_yaml in enumerate(yaml['axes']): - axis_name = axis_yaml['name'] if 'name' in axis_yaml else '{}.axis{}'.format(name, axis_idx) + axis_name = (name + "." + axis_yaml['name']) if 'name' in axis_yaml else '{}.axis{}'.format(name, axis_idx) self.axes.append(AxisTestContext(axis_name, axis_yaml, self)) def rediscover(self): @@ -388,7 +388,6 @@ class TestHighVelocity(AxisTest): # V_bus and motor KV rating max_rpm = axis_ctx.odrv_ctx.yaml['vbus-voltage'] * axis_ctx.yaml['motor-kv'] rated_limit = max_rpm / 60 * axis_ctx.yaml['encoder-cpr'] - rated_limit *= 0.5 # TODO: remove this later, but for now we want to stay away from the modulation depth limit expected_limit = rated_limit # The KV-rating assumes square-waves on the motor phases (hexagonal space vector trajectory) @@ -399,6 +398,10 @@ class TestHighVelocity(AxisTest): # The ODrive only goes to 80% modulation depth in order to save some time for the ADC measurements. # See FOC_current in motor.cpp. expected_limit *= 0.8 + + # TODO: remove the following two lines, but for now we want to stay away from the modulation depth limit + expected_limit *= 0.8 + rated_limit = expected_limit # Add a 10% margin to account for expected_limit *= 0.9 @@ -465,6 +468,10 @@ class TestHighVelocityInViscousFluid(DualAxisTest): Runs TestHighVelocity on one motor while using the other motor as a load. The load is created by running velocity control with setpoint 0. """ + def __init__(self, load_current=10, driver_current=20): + self._load_current = load_current + self._driver_current = driver_current + def run_test(self, axis0_ctx: AxisTestContext, axis1_ctx: AxisTestContext, logger): load_ctx = axis0_ctx driver_ctx = axis1_ctx @@ -474,12 +481,14 @@ class TestHighVelocityInViscousFluid(DualAxisTest): load_ctx.handle.controller.config.vel_integrator_gain = 0 load_ctx.handle.controller.vel_integrator_current = 0 load_ctx.handle.controller.config.vel_limit = 20000 # this is not really relevant - load_ctx.handle.motor.config.current_lim = 20 + load_ctx.handle.motor.config.current_lim = self._load_current load_ctx.odrv_ctx.handle.config.brake_resistance = 0 # disable brake resistance, the power will go into the bus load_ctx.handle.controller.set_vel_setpoint(0, 0) request_state(load_ctx, AXIS_STATE_CLOSED_LOOP_CONTROL) - driver_test = TestHighVelocity(override_current_limit=40, load_current=20, brake=False) + driver_test = TestHighVelocity( + override_current_limit=self._driver_current, + load_current=self._load_current, brake=False) driver_test.check_preconditions(driver_ctx, logger) driver_test.run_test(driver_ctx, logger) diff --git a/tools/run_tests.py b/tools/run_tests.py index 14185cf0..6931dd49 100755 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -12,52 +12,67 @@ import os import sys import threading import traceback +import argparse from odrive.tests import * from odrive.utils import Logger, for_all_parallel, Event +script_path=os.path.dirname(os.path.realpath(__file__)) -all_tests = [ -# TestFlashAndErase(), -# TestSetup(), -# TestMotorCalibration(), -# # TODO: test encoder index search -# TestEncoderOffsetCalibration(), -# # TODO: hold down one motor while the other one does an index search (should fail) -# TestClosedLoopControl(), -# TestStoreAndReboot(), -# TestEncoderOffsetCalibration(), # need to find offset _or_ index after reboot -# TestClosedLoopControl(), - TestDiscoverAndGotoIdle(), # for testing - TestEncoderOffsetCalibration(pass_if_ready=True), -# TestHighVelocity(), - TestHighVelocityInViscousFluid(), -# TestVelCtrlVsPosCtrl() - # TODO: test step/dir - # TODO: test sensorless - # TODO: test ASCII protocol - # TODO: test protocol over UART -] +parser = argparse.ArgumentParser(description='ODrive automated test tool\n') +parser.add_argument("--skip-boring-tests", action="store_true", + help="Skip the boring tests and go right to the high power tests") +parser.add_argument("--ignore", metavar='DEVICE', action='store', nargs='+', + help="Ignore one or more ODrives or axes") +parser.add_argument("--test-rig-yaml", type=argparse.FileType('r'), + help="test rig YAML file") +parser.set_defaults(test_rig_yaml=script_path + '/test-rig.yaml') +parser.set_defaults(ignore=[]) +args = parser.parse_args() +all_tests = [] +if not args.skip_boring_tests: + all_tests.append(TestFlashAndErase()) + all_tests.append(TestSetup()) + all_tests.append(TestMotorCalibration()) + # # TODO: test encoder index search + all_tests.append(TestEncoderOffsetCalibration()) + # # TODO: hold down one motor while the other one does an index search (should fail) + all_tests.append(TestClosedLoopControl()) + all_tests.append(TestStoreAndReboot()) + all_tests.append(TestEncoderOffsetCalibration()) # need to find offset _or_ index after reboot + all_tests.append(TestClosedLoopControl()) +else: + all_tests.append(TestDiscoverAndGotoIdle()) + all_tests.append(TestEncoderOffsetCalibration(pass_if_ready=True)) +#all_tests.append(TestHighVelocity()) +all_tests.append(TestHighVelocityInViscousFluid(load_current=20, driver_current=40)) +#all_tests.append(TestVelCtrlVsPosCtrl()) +# TODO: test step/dir +# TODO: test sensorless +# TODO: test ASCII protocol +# TODO: test protocol over UART + +print(str(args.ignore)) logger = Logger() -script_path=os.path.dirname(os.path.realpath(__file__)) -with open(script_path + '/test-rig.yaml', 'r') as file_stream: - test_rig_yaml = yaml.load(file_stream) +test_rig_yaml = yaml.load(args.test_rig_yaml) os.chdir(script_path + '/../Firmware') # Build a dictionary of odrive test contexts by name odrives_by_name = {} for odrv_idx, odrv_yaml in enumerate(test_rig_yaml['odrives']): name = odrv_yaml['name'] if 'name' in odrv_yaml else 'odrive{}'.format(odrv_idx) - odrives_by_name[name] = ODriveTestContext(name, odrv_yaml) + if not name in args.ignore: + odrives_by_name[name] = ODriveTestContext(name, odrv_yaml) # Build a dictionary of axis test contexts by name (e.g. odrive0.axis0) axes_by_name = {} for odrv_ctx in odrives_by_name.values(): for axis_idx, axis_ctx in enumerate(odrv_ctx.axes): - axes_by_name[axis_ctx.name] = axis_ctx + if not axis_ctx.name in args.ignore: + axes_by_name[axis_ctx.name] = axis_ctx # Ensure mechanical couplings are valid couplings = [] @@ -65,7 +80,9 @@ if test_rig_yaml['couplings'] is None: test_rig_yaml['couplings'] = {} else: for coupling in test_rig_yaml['couplings']: - couplings.append([axes_by_name[axis_name] for axis_name in coupling]) + c = [axes_by_name[axis_name] for axis_name in coupling if (axis_name in axes_by_name)] + if len(c) > 1: + couplings.append(c) app_shutdown_token = Event() diff --git a/tools/test-rig.yaml b/tools/test-rig.yaml index 2c05cc9d..092bed42 100644 --- a/tools/test-rig.yaml +++ b/tools/test-rig.yaml @@ -11,14 +11,16 @@ odrives: vbus-voltage: 12 # [V] max-brake-power: 150 # [W] axes: - - motor-phase-resistance: 0.0245 + - name: 'yellow' + motor-phase-resistance: 0.0245 motor-phase-inductance: 2.03e-05 motor-pole-pairs: 7 motor-direction: -1 motor-kv: 190 motor-max-current: 50 encoder-cpr: 8192 - - motor-phase-resistance: 0.028 + - name: 'black' + motor-phase-resistance: 0.028 motor-phase-inductance: 1.6e-05 motor-pole-pairs: 7 motor-direction: -1 @@ -35,14 +37,16 @@ odrives: vbus-voltage: 12 # [V] max-brake-power: 150 # [W] axes: - - motor-phase-resistance: 0.0253 + - name: 'black' + motor-phase-resistance: 0.0253 motor-phase-inductance: 1.6e-05 motor-pole-pairs: 7 motor-direction: 1 motor-kv: 270 motor-max-current: 50 encoder-cpr: 8192 - - motor-phase-resistance: 0.0245 + - name: 'yellow' + motor-phase-resistance: 0.0245 motor-phase-inductance: 2.03e-05 motor-pole-pairs: 7 motor-direction: -1 @@ -52,5 +56,5 @@ odrives: # Mechanical couplings couplings: - - [ top-odrive.axis0, bottom-odrive.axis1 ] - - [ top-odrive.axis1, bottom-odrive.axis0 ] + - [ top-odrive.yellow, bottom-odrive.yellow ] + - [ top-odrive.black, bottom-odrive.black ]