Skip to content
Test Types & Methods

Motor and Actuator Testing with TofuPilot

Learn how to automate motor and actuator production tests, capture torque, speed, and vibration data, and track results with TofuPilot.

JJulien Buteau
intermediate12 min readMarch 14, 2026

Motor and Actuator Testing with TofuPilot

Every motor that leaves your line needs to spin at the right speed, draw the right current, and not vibrate itself apart. Manual spot checks don't scale. You need automated tests that capture torque curves, speed profiles, and vibration signatures for every unit.

TofuPilot and OpenHTF let you build those tests in Python and track every result across production.

What Motor Tests Cover

Production motor testing typically includes:

Test TypeWhat It MeasuresWhy It Matters
No-load speedRPM at rated voltage, no torqueWinding and magnet integrity
Stall torqueMaximum torque at zero RPMMechanical strength
Current drawAmps at no-load, rated load, stallEfficiency, winding shorts
Back-EMFGenerated voltage when spun externallyMagnet and winding quality
VibrationAcceleration spectrum at operating speedBearing health, balance
Insulation resistanceMegohm reading winding-to-caseSafety, dielectric integrity

Prerequisites

  • Python 3.8+ with openhtf and tofupilot installed
  • A dynamometer or torque sensor with serial/GPIB/USB interface
  • A programmable power supply for motor drive
  • Optional: accelerometer or vibration sensor

Step 1: Set Up the No-Load Test

The no-load test is your first gate. A motor that can't reach rated speed with no load has a fundamental problem.

motor_noload_test.py
import openhtf as htf
from openhtf.util import units
import time

@htf.measures(
    htf.Measurement("no_load_speed_rpm")
        .with_units(units.HERTZ)  # RPM stored as value
        .in_range(2850, 3150)
        .doc("No-load speed at rated voltage"),
    htf.Measurement("no_load_current_a")
        .with_units(units.AMPERE)
        .at_most(0.5)
        .doc("No-load current draw"),
    htf.Measurement("startup_time_ms")
        .with_units(units.MILLISECOND)
        .at_most(200)
        .doc("Time to reach 90% rated speed"),
)
def no_load_test(test, psu, tachometer):
    """Run motor at rated voltage with no load attached."""
    psu.set_voltage(24.0)
    psu.output_on()

    start = time.time()
    # Wait for motor to reach steady state
    time.sleep(2.0)

    speed = tachometer.read_rpm()
    current = psu.measure_current()
    startup = tachometer.get_startup_time_ms()

    test.measurements.no_load_speed_rpm = speed
    test.measurements.no_load_current_a = current
    test.measurements.startup_time_ms = startup

    psu.output_off()

Step 2: Capture Torque-Speed Curves

The torque-speed curve characterizes motor performance across its operating range. Capture it as a multi-dimensional measurement.

motor_torque_curve.py
import openhtf as htf
from openhtf.util import units
import numpy as np

@htf.measures(
    htf.Measurement("torque_speed_curve")
        .with_dimensions(units.HERTZ)  # RPM as dimension
        .doc("Torque vs speed characteristic curve"),
    htf.Measurement("rated_torque_nm")
        .in_range(0.45, 0.55)
        .doc("Torque at rated speed"),
    htf.Measurement("efficiency_at_rated")
        .in_range(0.80, 1.0)
        .doc("Efficiency at rated operating point"),
)
def torque_curve_test(test, psu, dyno):
    """Sweep load from no-load to stall, capture torque at each point."""
    psu.set_voltage(24.0)
    psu.output_on()
    time.sleep(1.0)

    torque_points = []
    speed_points = []

    for load_pct in range(0, 105, 5):
        dyno.set_load_percent(load_pct)
        time.sleep(0.5)  # Settle time

        torque = dyno.read_torque_nm()
        speed = dyno.read_rpm()

        torque_points.append(torque)
        speed_points.append(speed)

        test.measurements.torque_speed_curve[speed] = torque

    # Find rated operating point (closest to rated speed)
    rated_idx = np.argmin(np.abs(np.array(speed_points) - 3000))
    rated_torque = torque_points[rated_idx]
    rated_speed = speed_points[rated_idx]

    test.measurements.rated_torque_nm = rated_torque

    # Calculate efficiency: P_mech / P_elec
    p_mech = rated_torque * rated_speed * 2 * np.pi / 60
    p_elec = 24.0 * psu.measure_current()
    test.measurements.efficiency_at_rated = p_mech / p_elec

    dyno.set_load_percent(0)
    psu.output_off()

Step 3: Add Vibration Analysis

Vibration testing catches bearing defects, rotor imbalance, and misalignment before they become field failures.

motor_vibration_test.py
import openhtf as htf
from openhtf.util import units
import numpy as np

@htf.measures(
    htf.Measurement("vibration_rms_g")
        .at_most(0.5)
        .doc("Overall vibration RMS in g"),
    htf.Measurement("vibration_spectrum")
        .with_dimensions(units.HERTZ)
        .doc("Vibration frequency spectrum"),
    htf.Measurement("dominant_frequency_hz")
        .with_units(units.HERTZ)
        .doc("Highest amplitude frequency component"),
)
def vibration_test(test, psu, accelerometer):
    """Measure vibration at rated speed."""
    psu.set_voltage(24.0)
    psu.output_on()
    time.sleep(3.0)  # Wait for steady state

    # Capture 1 second of acceleration data at 10 kHz
    raw_data = accelerometer.capture(duration_s=1.0, sample_rate=10000)

    # RMS vibration
    rms = np.sqrt(np.mean(raw_data ** 2))
    test.measurements.vibration_rms_g = rms

    # FFT for spectrum
    fft_vals = np.abs(np.fft.rfft(raw_data))
    freqs = np.fft.rfftfreq(len(raw_data), d=1/10000)

    for freq, amplitude in zip(freqs[1:500], fft_vals[1:500]):
        test.measurements.vibration_spectrum[freq] = amplitude

    # Dominant frequency
    peak_idx = np.argmax(fft_vals[1:]) + 1
    test.measurements.dominant_frequency_hz = freqs[peak_idx]

    psu.output_off()

Step 4: Build the Full Test Sequence

Combine all motor tests into a single production sequence:

motor_production_test.py
import openhtf as htf
from tofupilot import TofuPilotClient

def main():
    test = htf.Test(
        no_load_test,
        torque_curve_test,
        vibration_test,
    )

    test.add_output_callbacks(
        TofuPilotClient().as_openhtf_callback(
            procedure_id="motor-fct",
            procedure_name="Motor Production FCT",
        )
    )

    test.execute(test_start=htf.PhaseDescriptor.wrap(
        lambda test: setattr(
            test, "dut_id", input("Scan motor serial number: ")
        )
    ))

if __name__ == "__main__":
    main()

Step 5: Monitor Production Trends

Once tests are running, use TofuPilot to catch drift before it causes failures:

  • No-load speed trending down: Possible demagnetization or increased friction
  • Current draw creeping up: Winding insulation degradation or bearing wear
  • Vibration RMS increasing: Bearing failure progression
  • Efficiency dropping: Look at both electrical and mechanical subsystems

Set measurement limits with margin. If your spec is 2850 to 3150 RPM, consider tighter production limits of 2900 to 3100 to catch drift early.

TofuPilot's control charts show these trends automatically. A shift in the mean or increasing standard deviation shows up before parts start failing.

Actuator-Specific Considerations

Linear actuators and servo motors need additional tests:

Actuator TypeAdditional Tests
Linear actuatorStroke length, extension force, retraction speed
Servo motorPosition accuracy, settling time, step response
Stepper motorHolding torque, step accuracy, resonance points
SolenoidPull-in voltage, drop-out voltage, response time

The test structure is the same. Define measurements with limits, capture the data, let TofuPilot track everything.

More Guides

Put this guide into practice