001/*
002 * Copyright (C) Cross The Road Electronics.  All rights reserved.
003 * License information can be found in CTRE_LICENSE.txt
004 * For support and suggestions contact support@ctr-electronics.com or file
005 * an issue tracker at https://github.com/CrossTheRoadElec/Phoenix-Releases
006 */
007package com.ctre.phoenix6.hardware;
008
009import java.util.ArrayList;
010import java.util.concurrent.locks.Lock;
011import java.util.concurrent.locks.ReentrantLock;
012
013import com.ctre.phoenix6.CANBus;
014import com.ctre.phoenix6.StatusCode;
015import com.ctre.phoenix6.StatusSignal;
016import com.ctre.phoenix6.Utils;
017import com.ctre.phoenix6.configs.MotorOutputConfigs;
018import com.ctre.phoenix6.configs.TalonFXConfiguration;
019import com.ctre.phoenix6.controls.ControlRequest;
020import com.ctre.phoenix6.controls.DutyCycleOut;
021import com.ctre.phoenix6.controls.NeutralOut;
022import com.ctre.phoenix6.controls.VoltageOut;
023import com.ctre.phoenix6.hardware.core.CoreTalonFX;
024import com.ctre.phoenix6.jni.PlatformJNI;
025import com.ctre.phoenix6.signals.InvertedValue;
026import com.ctre.phoenix6.signals.NeutralModeValue;
027import com.ctre.phoenix6.sim.DeviceType;
028import com.ctre.phoenix6.wpiutils.AutoFeedEnable;
029import com.ctre.phoenix6.wpiutils.CallbackHelper;
030import com.ctre.phoenix6.wpiutils.MotorSafetyImplem;
031import com.ctre.phoenix6.wpiutils.ReplayAutoEnable;
032
033import edu.wpi.first.hal.HAL;
034import edu.wpi.first.hal.HAL.SimPeriodicBeforeCallback;
035import edu.wpi.first.hal.HALValue;
036import edu.wpi.first.hal.SimBoolean;
037import edu.wpi.first.hal.SimDevice;
038import edu.wpi.first.hal.SimDevice.Direction;
039import edu.wpi.first.hal.SimDouble;
040import edu.wpi.first.hal.simulation.SimDeviceDataJNI;
041import edu.wpi.first.util.sendable.Sendable;
042import edu.wpi.first.util.sendable.SendableBuilder;
043import edu.wpi.first.util.sendable.SendableRegistry;
044import edu.wpi.first.wpilibj.RobotBase;
045import edu.wpi.first.wpilibj.simulation.CallbackStore;
046import edu.wpi.first.wpilibj.simulation.SimDeviceSim;
047
048/**
049 * WPILib-integrated version of {@link CoreTalonFX}
050 */
051public class TalonFX extends CoreTalonFX implements Sendable, AutoCloseable {
052    /**
053     * The StatusSignal getters are copies so that calls
054     * to the WPI interface do not update any references
055     */
056    private final StatusSignal<Double> m_dutyCycle = getDutyCycle(false).clone();
057
058    private final DutyCycleOut m_setterControl = new DutyCycleOut(0);
059    private final NeutralOut m_neutralControl = new NeutralOut();
060    private final VoltageOut m_voltageControl = new VoltageOut(0);
061
062    private final MotorOutputConfigs m_configs = new MotorOutputConfigs();
063
064    private String m_description;
065
066    private static final DeviceType kSimDeviceType = DeviceType.P6_TalonFXType;
067
068    private SimDevice m_simMotor;
069    private SimDouble m_simSupplyVoltage;
070    private SimDouble m_simDutyCycle;
071    private SimDouble m_simMotorVoltage;
072    private SimDouble m_simTorqueCurrent;
073    private SimDouble m_simSupplyCurrent;
074
075    private SimDevice m_simForwardLimit;
076    private SimBoolean m_simForwardLimitValue;
077
078    private SimDevice m_simReverseLimit;
079    private SimBoolean m_simReverseLimitValue;
080
081    private SimDevice m_simRotor;
082    private SimDouble m_simRotorPos;
083    private SimDouble m_simRotorRawPos;
084    private SimDouble m_simRotorVel;
085    private SimDouble m_simRotorAccel;
086
087    // returned registered callbacks
088    private final ArrayList<CallbackStore> m_simValueChangedCallbacks = new ArrayList<CallbackStore>();
089    private SimPeriodicBeforeCallback m_simPeriodicBeforeCallback = null;
090
091    /**
092     * The default motor safety timeout IF calling application
093     * enables the feature.
094     */
095    public static final double kDefaultSafetyExpiration = 0.1;
096
097    /**
098     * Late-constructed motor safety, which ensures feature is off unless calling
099     * applications explicitly enables it.
100     */
101    private MotorSafetyImplem m_motorSafety = null;
102    private double m_motSafeExpiration = kDefaultSafetyExpiration;
103    private final Lock m_motorSafetyLock = new ReentrantLock();
104
105    /**
106     * Constructs a new Talon FX motor controller object.
107     * <p>
108     * Constructs the device using the default CAN bus for the system:
109     * <ul>
110     *   <li>"rio" on roboRIO
111     *   <li>"can0" on Linux
112     *   <li>"*" on Windows
113     * </ul>
114     *
115     * @param deviceId ID of the device, as configured in Phoenix Tuner.
116     */
117    public TalonFX(int deviceId) {
118        this(deviceId, "");
119    }
120
121    /**
122     * Constructs a new Talon FX motor controller object.
123     *
124     * @param deviceId ID of the device, as configured in Phoenix Tuner.
125     * @param canbus   The CAN bus this device is on.
126     */
127    public TalonFX(int deviceId, CANBus canbus) {
128        this(deviceId, canbus.getName());
129    }
130
131    /**
132     * Constructs a new Talon FX motor controller object.
133     *
134     * @param deviceId ID of the device, as configured in Phoenix Tuner.
135     * @param canbus   Name of the CAN bus this device is on. Possible CAN bus
136     *                 strings are:
137     *                 <ul>
138     *                   <li>"rio" for the native roboRIO CAN bus
139     *                   <li>CANivore name or serial number
140     *                   <li>SocketCAN interface (non-FRC Linux only)
141     *                   <li>"*" for any CANivore seen by the program
142     *                   <li>empty string (default) to select the default for the
143     *                       system:
144     *                   <ul>
145     *                     <li>"rio" on roboRIO
146     *                     <li>"can0" on Linux
147     *                     <li>"*" on Windows
148     *                   </ul>
149     *                 </ul>
150     */
151    public TalonFX(int deviceId, String canbus) {
152        super(deviceId, canbus);
153
154        m_description = "Talon FX (v6) " + deviceId;
155        SendableRegistry.addLW(this, "Talon FX (v6) ", deviceId);
156
157        if (RobotBase.isSimulation()) {
158            /* run in both swsim and hwsim */
159            AutoFeedEnable.getInstance().start();
160        }
161        if (Utils.isReplay()) {
162            ReplayAutoEnable.getInstance().start();
163        }
164
165        m_simMotor = SimDevice.create("CANMotor:Talon FX (v6)", deviceId);
166
167        final String base = "Talon FX (v6)[" + deviceId + "]/";
168        m_simRotor = SimDevice.create("CANEncoder:" + base + "Rotor Sensor");
169        m_simForwardLimit = SimDevice.create("CANDIO:" + base + "Fwd Limit");
170        m_simReverseLimit = SimDevice.create("CANDIO:" + base + "Rev Limit");
171
172        if (m_simMotor != null) {
173            /* First make sure our simulated device gets enabled */
174            m_simPeriodicBeforeCallback = HAL.registerSimPeriodicBeforeCallback(this::onPeriodic);
175
176            m_simSupplyVoltage = m_simMotor.createDouble("supplyVoltage", Direction.kInput, 12.0);
177
178            m_simDutyCycle = m_simMotor.createDouble("dutyCycle", Direction.kOutput, 0);
179            m_simMotorVoltage = m_simMotor.createDouble("motorVoltage", Direction.kOutput, 0);
180            m_simTorqueCurrent = m_simMotor.createDouble("torqueCurrent", Direction.kOutput, 0);
181            m_simSupplyCurrent = m_simMotor.createDouble("supplyCurrent", Direction.kOutput, 0);
182
183            final SimDeviceSim sim = new SimDeviceSim("CANMotor:Talon FX (v6)");
184            m_simValueChangedCallbacks
185                .add(sim.registerValueChangedCallback(m_simSupplyVoltage, this::onValueChanged, true));
186        }
187        if (m_simRotor != null) {
188            m_simRotorPos = m_simRotor.createDouble("position", Direction.kOutput, 0);
189
190            m_simRotorRawPos = m_simRotor.createDouble("rawPositionInput", Direction.kInput, 0);
191            m_simRotorVel = m_simRotor.createDouble("velocity", Direction.kInput, 0);
192            m_simRotorAccel = m_simRotor.createDouble("acceleration", Direction.kInput, 0);
193
194            final SimDeviceSim sim = new SimDeviceSim("CANEncoder:" + base + "Rotor Sensor");
195            m_simValueChangedCallbacks
196                .add(sim.registerValueChangedCallback(m_simRotorRawPos, this::onValueChanged, true));
197            m_simValueChangedCallbacks
198                .add(sim.registerValueChangedCallback(m_simRotorVel, this::onValueChanged, true));
199            m_simValueChangedCallbacks
200                .add(sim.registerValueChangedCallback(m_simRotorAccel, this::onValueChanged, true));
201        }
202        if (m_simForwardLimit != null) {
203            m_simForwardLimit.createBoolean("init", Direction.kOutput, true);
204            m_simForwardLimit.createBoolean("input", Direction.kOutput, true);
205
206            m_simForwardLimitValue = m_simForwardLimit.createBoolean("value", Direction.kBidir, false);
207
208            final SimDeviceSim sim = new SimDeviceSim("CANDIO:" + base + "Fwd Limit");
209            m_simValueChangedCallbacks
210                .add(sim.registerValueChangedCallback(m_simForwardLimitValue, this::onValueChanged, true));
211        }
212        if (m_simReverseLimit != null) {
213            m_simReverseLimit.createBoolean("init", Direction.kOutput, true);
214            m_simReverseLimit.createBoolean("input", Direction.kOutput, true);
215
216            m_simReverseLimitValue = m_simReverseLimit.createBoolean("value", Direction.kBidir, false);
217
218            final SimDeviceSim sim = new SimDeviceSim("CANDIO:" + base + "Rev Limit");
219            m_simValueChangedCallbacks
220                .add(sim.registerValueChangedCallback(m_simReverseLimitValue, this::onValueChanged, true));
221        }
222    }
223
224    // ----- Callbacks for Sim ----- //
225    private void onValueChanged(String name, int handle, int direction, HALValue value) {
226        String deviceName = SimDeviceDataJNI.getSimDeviceName(SimDeviceDataJNI.getSimValueDeviceHandle(handle));
227        String physType = deviceName + ":" + name;
228        PlatformJNI.JNI_SimSetPhysicsInput(
229            kSimDeviceType.value, getDeviceID(),
230            physType, CallbackHelper.getRawValue(value)
231        );
232    }
233
234    private void onPeriodic() {
235        double value = 0;
236        int err = 0;
237
238        final int deviceID = getDeviceID();
239
240        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "SupplyVoltage");
241        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
242        if (err == 0) {
243            m_simSupplyVoltage.set(value);
244        }
245        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "DutyCycle");
246        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
247        if (err == 0) {
248            m_simDutyCycle.set(value);
249        }
250        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "MotorVoltage");
251        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
252        if (err == 0) {
253            m_simMotorVoltage.set(value);
254        }
255        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "TorqueCurrent");
256        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
257        if (err == 0) {
258            m_simTorqueCurrent.set(value);
259        }
260        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "SupplyCurrent");
261        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
262        if (err == 0) {
263            m_simSupplyCurrent.set(value);
264        }
265        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "RotorPosition");
266        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
267        if (err == 0) {
268            m_simRotorPos.set(value);
269        }
270        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "RawRotorPosition");
271        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
272        if (err == 0) {
273            m_simRotorRawPos.set(value);
274        }
275        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "RotorVelocity");
276        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
277        if (err == 0) {
278            m_simRotorVel.set(value);
279        }
280        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "RotorAcceleration");
281        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
282        if (err == 0) {
283            m_simRotorAccel.set(value);
284        }
285        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "ForwardLimit");
286        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
287        if (err == 0) {
288            m_simForwardLimitValue.set((int) value != 0);
289        }
290        value = PlatformJNI.JNI_SimGetPhysicsValue(kSimDeviceType.value, deviceID, "ReverseLimit");
291        err = PlatformJNI.JNI_SimGetLastError(kSimDeviceType.value, deviceID);
292        if (err == 0) {
293            m_simReverseLimitValue.set((int) value != 0);
294        }
295    }
296
297    // ------- AutoCloseable ----- //
298    @Override
299    public void close() {
300        SendableRegistry.remove(this);
301        if (m_simPeriodicBeforeCallback != null) {
302            m_simPeriodicBeforeCallback.close();
303            m_simPeriodicBeforeCallback = null;
304        }
305        if (m_simMotor != null) {
306            m_simMotor.close();
307            m_simMotor = null;
308        }
309        if (m_simRotor != null) {
310            m_simRotor.close();
311            m_simRotor = null;
312        }
313        if (m_simForwardLimit != null) {
314            m_simForwardLimit.close();
315            m_simForwardLimit = null;
316        }
317        if (m_simReverseLimit != null) {
318            m_simReverseLimit.close();
319            m_simReverseLimit = null;
320        }
321
322        for (var callback : m_simValueChangedCallbacks) {
323            callback.close();
324        }
325        m_simValueChangedCallbacks.clear();
326
327        AutoFeedEnable.getInstance().stop();
328        ReplayAutoEnable.getInstance().stop();
329    }
330
331    // ------ set/get routines for WPILIB interfaces ------//
332    /**
333     * Common interface for setting the speed of a motor controller.
334     *
335     * @param speed The speed to set. Value should be between -1.0 and 1.0.
336     */
337    public void set(double speed) {
338        feed();
339        setControl(m_setterControl.withOutput(speed));
340    }
341
342    /**
343     * Common interface for seting the direct voltage output of a motor controller.
344     *
345     * @param volts The voltage to output.
346     */
347    public void setVoltage(double volts) {
348        feed();
349        setControl(m_voltageControl.withOutput(volts));
350    }
351
352    /**
353     * Common interface for getting the current set speed of a motor controller.
354     *
355     * @return The current set speed. Value is between -1.0 and 1.0.
356     */
357    public double get() {
358        return m_dutyCycle.refresh().getValue();
359    }
360
361    // ---------Intercept CTRE calls for motor safety ---------//
362    @Override
363    protected StatusCode setControlPrivate(ControlRequest request) {
364        /* intercept the control setter and feed motor-safety */
365        feed();
366        return super.setControlPrivate(request);
367    }
368
369    // ----------------------- turn-motor-off routines-------------------//
370    /**
371     * Common interface for disabling a motor controller.
372     */
373    public void disable() {
374        setControl(m_neutralControl);
375    }
376
377    /**
378     * Common interface to stop motor movement until set is called again.
379     */
380    public void stopMotor() {
381        disable();
382    }
383
384    // ----------------------- Invert routines -------------------//
385    /**
386     * Common interface for inverting direction of a motor controller.
387     * <p>
388     * Since invert is a config, this API is blocking. We recommend that
389     * users avoid calling this API periodically.
390     * <p>
391     * Since invert affects the reported motor position, this API should
392     * be called before any calls to {@link #setPosition(double)}.
393     *
394     * @deprecated This API is deprecated for removal in 2026. Since invert
395     * is a config, apply the invert setting as part of a full
396     * {@link TalonFXConfiguration} object. Invert can be found in the
397     * {@link TalonFXConfiguration#MotorOutput MotorOutput} config group.
398     *
399     * @param isInverted The state of inversion, true is inverted.
400     * @return Status of refreshing and applying the invert config
401     */
402    @Deprecated(forRemoval = true)
403    public StatusCode setInverted(boolean isInverted) {
404        /* First read the configs so they're up-to-date */
405        StatusCode retval = getConfigurator().refresh(m_configs);
406        if (retval.isOK()) {
407            /* Then set the invert config to the appropriate value */
408            m_configs.Inverted = isInverted ? InvertedValue.Clockwise_Positive : InvertedValue.CounterClockwise_Positive;
409            retval = getConfigurator().apply(m_configs);
410        }
411        return retval;
412    }
413
414    /**
415     * Common interface for returning the inversion state of a motor controller.
416     * <p>
417     * Since invert is a config, this API is blocking. We recommend that
418     * users avoid calling this API periodically.
419     *
420     * @deprecated This API is deprecated for removal in 2026. Since invert
421     * is a config, read the invert setting as part of a full
422     * {@link TalonFXConfiguration} object or using a {@link MotorOutputConfigs}
423     * object. Applied invert, which may not match the invert config for followers,
424     * can also be fetched using {@link #getAppliedRotorPolarity()}.
425     *
426     * @return The state of the inversion, true is inverted.
427     */
428    @Deprecated(forRemoval = true)
429    public boolean getInverted() {
430        /* Read the configs so they're up-to-date */
431        getConfigurator().refresh(m_configs);
432        /* Return true if we're Clockwise_Positive */
433        return m_configs.Inverted == InvertedValue.Clockwise_Positive;
434    }
435
436    // -------------------- Neutral mode routines ----------------//
437    /**
438     * Sets the mode of operation when output is neutral or disabled.
439     * This is equivalent to setting the {@link MotorOutputConfigs#NeutralMode}
440     * when applying a {@link TalonFXConfiguration} to the motor.
441     * <p>
442     * Since neutral mode is a config, this API is blocking. We recommend
443     * that users avoid calling this API periodically.
444     * <p>
445     * This will wait up to 0.100 seconds (100ms) by default.
446     *
447     * @param neutralMode The state of the motor controller bridge when output is neutral or disabled
448     * @return Status of refreshing and applying the neutral mode config
449     */
450    public StatusCode setNeutralMode(NeutralModeValue neutralMode) {
451        return setNeutralMode(neutralMode, 0.100);
452    }
453
454    /**
455     * Sets the mode of operation when output is neutral or disabled.
456     * <p>
457     * Since neutral mode is a config, this API is blocking. We recommend
458     * that users avoid calling this API periodically.
459     *
460     * @param neutralMode The state of the motor controller bridge when output is neutral or disabled
461     * @param timeoutSeconds Maximum amount of time to wait when performing configuration
462     * @return Status of refreshing and applying the neutral mode config
463     */
464    public StatusCode setNeutralMode(NeutralModeValue neutralMode, double timeoutSeconds) {
465        /* First read the configs so they're up-to-date */
466        StatusCode retval = getConfigurator().refresh(m_configs, timeoutSeconds);
467        if (retval.isOK()) {
468            /* Then set the neutral mode config to the appropriate value */
469            m_configs.NeutralMode = neutralMode;
470            retval = getConfigurator().apply(m_configs, timeoutSeconds);
471        }
472        return retval;
473    }
474
475    // ----- Sendable ----- //
476    @Override
477    public void initSendable(SendableBuilder builder) {
478        builder.setSmartDashboardType("Motor Controller");
479        builder.setActuator(true);
480        builder.setSafeState(this::stopMotor);
481        builder.addDoubleProperty("Value", this::get, this::set);
482    }
483
484    /**
485     * @return Description of motor controller
486     */
487    public String getDescription() {
488        return m_description;
489    }
490
491    /* ----- Motor Safety ----- */
492    /** caller must lock appropriately */
493    private MotorSafetyImplem GetMotorSafety() {
494        if (m_motorSafety == null) {
495            /* newly created MS object */
496            m_motorSafety = new MotorSafetyImplem(this::stopMotor, getDescription());
497            /* apply the expiration timeout */
498            m_motorSafety.setExpiration(m_motSafeExpiration);
499        }
500        return m_motorSafety;
501    }
502
503    /**
504     * Feed the motor safety object.
505     * <p>
506     * Resets the timer on this object that is used to do the timeouts.
507     */
508    public void feed() {
509        m_motorSafetyLock.lock();
510        try {
511            if (m_motorSafety == null) {
512                /* do nothing, MS features were never enabled */
513            } else {
514                GetMotorSafety().feed();
515            }
516        } finally {
517            m_motorSafetyLock.unlock();
518        }
519    }
520
521    /**
522     * Set the expiration time for the corresponding motor safety object.
523     *
524     * @param expirationTime The timeout value in seconds.
525     */
526    public void setExpiration(double expirationTime) {
527        m_motorSafetyLock.lock();
528        try {
529            /* save the value for if/when we do create the MS object */
530            m_motSafeExpiration = expirationTime;
531            /* apply it only if MS object exists */
532            if (m_motorSafety == null) {
533                /* do nothing, MS features were never enabled */
534            } else {
535                /* this call will trigger creating a registered MS object */
536                GetMotorSafety().setExpiration(m_motSafeExpiration);
537            }
538        } finally {
539            m_motorSafetyLock.unlock();
540        }
541    }
542
543    /**
544     * Retrieve the timeout value for the corresponding motor safety object.
545     *
546     * @return the timeout value in seconds.
547     */
548    public double getExpiration() {
549        m_motorSafetyLock.lock();
550        try {
551            /* return the intended expiration time */
552            return m_motSafeExpiration;
553        } finally {
554            m_motorSafetyLock.unlock();
555        }
556    }
557
558    /**
559     * Determine of the motor is still operating or has timed out.
560     *
561     * @return a true value if the motor is still operating normally and hasn't
562     *         timed out.
563     */
564    public boolean isAlive() {
565        m_motorSafetyLock.lock();
566        try {
567            if (m_motorSafety == null) {
568                /* MC is alive - MS features were never enabled to neutral the MC. */
569                return true;
570            } else {
571                return GetMotorSafety().isAlive();
572            }
573        } finally {
574            m_motorSafetyLock.unlock();
575        }
576    }
577
578    /**
579     * Enable/disable motor safety for this device.
580     * <p>
581     * Turn on and off the motor safety option for this object.
582     *
583     * @param enabled True if motor safety is enforced for this object.
584     */
585    public void setSafetyEnabled(boolean enabled) {
586        m_motorSafetyLock.lock();
587        try {
588            if (m_motorSafety == null && !enabled) {
589                /*
590                 * Caller wants to disable MS,
591                 * but MS features were nevere enabled,
592                 * so it doesn't need to be disabled.
593                 */
594            } else {
595                /* MS will be created if it does not exist */
596                GetMotorSafety().setSafetyEnabled(enabled);
597            }
598        } finally {
599            m_motorSafetyLock.unlock();
600        }
601    }
602
603    /**
604     * Return the state of the motor safety enabled flag.
605     * <p>
606     * Return if the motor safety is currently enabled for this device.
607     *
608     * @return True if motor safety is enforced for this device
609     */
610    public boolean isSafetyEnabled() {
611        m_motorSafetyLock.lock();
612        try {
613            if (m_motorSafety == null) {
614                /* MS features were never enabled. */
615                return false;
616            } else {
617                return GetMotorSafety().isSafetyEnabled();
618            }
619        } finally {
620            m_motorSafetyLock.unlock();
621        }
622    }
623}