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}