001package jmri.jmrix.can.cbus; 002 003import jmri.DccLocoAddress; 004import jmri.LocoAddress; 005import jmri.SpeedStepMode; 006import jmri.ThrottleManager; 007import jmri.jmrix.AbstractThrottle; 008import jmri.jmrix.can.CanSystemConnectionMemo; 009 010/** 011 * An implementation of DccThrottle via AbstractThrottle with code specific to a 012 * CBUS connection. 013 * <p> 014 * Speed in the Throttle interfaces and AbstractThrottle is a float, but in CBUS 015 * is normally an int with values from 0 to 127. 016 * <table> 017 * <caption>CBUS 128 Speed Steps</caption> 018 * <tr><td>setSpeedSetting</td><td>CBUS DSPD</td><td>Translated</td><td>Throttle</td></tr> 019 * <tr><td>0</td><td> 0 </td><td> Speed 0 </td><td> 0 % </td></tr> 020 * <tr><td>-1</td><td> 1 </td><td> E Stop </td><td> 0 % </td></tr> 021 * <tr><td>0.007937</td><td> 2 </td><td> Speed 1 </td><td> 1/126 % </td></tr> 022 * <tr><td>0.015873</td><td> 3 </td><td> Speed 2 </td><td> 2/126 % </td></tr> 023 * <tr><td>0.984127</td><td> 125 </td><td> Speed 124 </td><td> 124/126 % </td></tr> 024 * <tr><td>0.992063</td><td> 126 </td><td> Speed 125 </td><td> 125/126 % </td></tr> 025 * <tr><td>1</td><td> 127 </td><td> Speed 126 </td><td> 100 % </td></tr> 026 * </table> 027 * 028 * <table> 029 * <caption>CBUS 28 Speed Steps</caption> 030 * <tr><td>CBUS DSPD</td><td>Translated</td><td>Throttle</td></tr> 031 * <tr><td> 0 </td><td> Speed 0 Encoding 1 </td><td> 0 % </td></tr> 032 * <tr><td> 1 </td><td> Speed 0 Encoding 2 </td><td> 0 % </td></tr> 033 * <tr><td> 2 </td><td> E Stop Encoding 1 </td><td> 0 % </td></tr> 034 * <tr><td> 3 </td><td> E Stop Encoding 2 </td><td> 0 % </td></tr> 035 * <tr><td> 4 </td><td> Speed 1 </td><td> 1/28 % </td></tr> 036 * <tr><td> 5 </td><td> Speed 2 </td><td> 2/28 % </td></tr> 037 * <tr><td> 29 </td><td> Speed 26 </td><td> 26/28 % </td></tr> 038 * <tr><td> 30 </td><td> Speed 27 </td><td> 27/28 % </td></tr> 039 * <tr><td> 31 </td><td> Speed 28 </td><td> 100 % </td></tr> 040 * </table> 041 * 042 * <table> 043 * <caption>CBUS 14 Speed Steps</caption> 044 * <tr><td>CBUS DSPD</td><td>Translated</td><td>Throttle</td></tr> 045 * <tr><td> 0 </td><td> Speed 0 </td><td> 0 % </td></tr> 046 * <tr><td> 1 </td><td> E Stop </td><td> 0 % </td></tr> 047 * <tr><td> 2 </td><td> Speed 1 </td><td> 1/14 % </td></tr> 048 * <tr><td> 3 </td><td> Speed 2 </td><td> 2/14 % </td></tr> 049 * <tr><td> 13 0x0D </td><td> Speed 12 </td><td> 12/14 % </td></tr> 050 * <tr><td> 14 0x0E </td><td> Speed 13 </td><td> 13/14 % </td></tr> 051 * <tr><td> 15 0x0F </td><td> Speed 14 </td><td> 100 % </td></tr> 052 * </table> 053 * 054 * @author Andrew Crosland Copyright (C) 2009 055 */ 056public class CbusThrottle extends AbstractThrottle { 057 058 private CbusCommandStation cs = null; 059 private int _handle = -1; 060 private DccLocoAddress dccAddress = null; 061 private boolean _isStolen; 062 private int _recoveryAttempts; 063 064 /** 065 * Constructor 066 * 067 * @param memo System Connection 068 * @param address The address this throttle relates to. 069 * @param handle the Session ID for the Throttle 070 */ 071 public CbusThrottle(CanSystemConnectionMemo memo, LocoAddress address, int handle) { 072 super(memo, CbusConstants.MAX_FUNCTIONS); 073 log.debug("creating new CbusThrottle address {} handle {}",address,handle); 074 if (!( address instanceof DccLocoAddress ) ){ 075 log.error("{} is not a DccLocoAddress",address); 076 return; 077 } 078 cs = (CbusCommandStation) adapterMemo.get(jmri.CommandStation.class); 079 _handle = handle; 080 _isStolen = false; 081 _recoveryAttempts = 0; 082 083 // cache settings 084 synchronized(this) { 085 this.speedSetting = 0; 086 } 087 // Functions 0-28 default to false 088 this.dccAddress = (DccLocoAddress) address; 089 this.isForward = true; 090 this.speedStepMode = SpeedStepMode.NMRA_DCC_128; 091 092 // start periodically sending keep alives, to keep this attached 093 log.debug("Start Throttle refresh"); 094 startRefresh(); 095 } 096 097 /** 098 * Set initial throttle values as taken from PLOC reply from hardware 099 * 100 * @param speed including direction flag 101 * @param f0f4 Functions 0-4 102 * @param f5f8 Functions 5-8 103 * @param f9f12 Functions 9-12 104 */ 105 protected void throttleInit(int speed, int f0f4, int f5f8, int f9f12) { 106 log.debug("Setting throttle initial values"); 107 // the command station session state is now known, so the next 108 // speed / direction set by JMRI must always be sent to the layout 109 oldCbusSpeed = -2; 110 updateSpeedSetting( speed & 0x7f ); 111 updateIsForward ( (speed & 0x80) == 0x80 ); 112 updateFunctionGroup(1,f0f4); 113 updateFunctionGroup(2,f5f8); 114 updateFunctionGroup(3,f9f12); 115 } 116 117 /** 118 * setSpeedStepMode - set the speed step value. 119 * <p> 120 * Overridden to capture mode changes to be forwarded to the hardware. 121 * New throttles default to 128 step mode. 122 * CBUS Command stations also default to 128SS so this does not 123 * need to be sent if unchanged. 124 * 125 * @param stepMode the current speed step mode - default should be 128 126 * speed step mode in most cases 127 */ 128 @Override 129 public void setSpeedStepMode(SpeedStepMode stepMode) { 130 if (speedStepMode==stepMode){ 131 return; 132 } 133 super.setSpeedStepMode(stepMode); 134 int mode; 135 switch (speedStepMode) { 136 case NMRA_DCC_28: 137 mode = CbusConstants.CBUS_SS_28; 138 break; 139 case NMRA_DCC_14: 140 mode = CbusConstants.CBUS_SS_14; 141 break; 142 default: 143 mode = CbusConstants.CBUS_SS_128; 144 break; 145 } 146 if (hasActiveSession()) { 147 cs.setSpeedSteps(_handle, mode); 148 } else { 149 log.debug("no active session, speed step mode {} not sent to layout", stepMode); 150 } 151 } 152 153 /** 154 * Check if this Throttle currently holds a command station session which 155 * can be sent to. 156 * <p> 157 * False when the Throttle has been disposed, or when the session has been 158 * stolen and is awaiting recovery, in which case the handle is -1. 159 * Local Throttle state is still updated in these cases, but nothing is 160 * sent to the layout. 161 * @return true if a valid session handle and command station are present. 162 */ 163 private boolean hasActiveSession() { 164 return ( cs != null && _handle >= 0 && !_isStolen ); 165 } 166 167 /** 168 * Convert a CBUS speed integer to a float speed value 169 * @param lSpeed -1 to 127 170 * @return float value -1 to 1 171 */ 172 protected float floatSpeed(int lSpeed) { 173 if (this.getSpeedStepMode()== SpeedStepMode.NMRA_DCC_28) { 174 float toReturn = 0.f; 175 switch (lSpeed) { 176 case 0: 177 case 1: 178 break; 179 case 2: 180 case 3: 181 toReturn = -1.f; // estop 182 break; 183 default: 184 toReturn = ((lSpeed - 3) / (float) speedStepMode.numSteps ); 185 } 186 return Math.min(toReturn, 1.0f); // return smallest value 187 } 188 189 switch (lSpeed) { 190 case 0: 191 return 0.f; 192 case 1: 193 return -1.f; // estop 194 default: 195 return ((lSpeed - 1) / (float) speedStepMode.numSteps ); 196 } 197 } 198 199 /** 200 * Send the CBUS message to set the state of functions F0, F1, F2, F3, F4. 201 */ 202 @Override 203 protected void sendFunctionGroup1() { 204 sendFunctionGroup(1); 205 } 206 207 /** 208 * Send the CBUS message to set the state of functions F5, F6, F7, F8. 209 */ 210 @Override 211 protected void sendFunctionGroup2() { 212 sendFunctionGroup(2); 213 } 214 215 /** 216 * Send the CBUS message to set the state of functions F9, F10, F11, F12. 217 */ 218 @Override 219 protected void sendFunctionGroup3() { 220 sendFunctionGroup(3); 221 } 222 223 /** 224 * Send the CBUS message to set the state of functions F13, F14, F15, F16, 225 * F17, F18, F19, F20 226 */ 227 @Override 228 protected void sendFunctionGroup4() { 229 sendFunctionGroup(4); 230 } 231 232 /** 233 * Send the CBUS message to set the state of functions F21, F22, F23, F24, 234 * F25, F26, F27, F28 235 */ 236 @Override 237 protected void sendFunctionGroup5() { 238 sendFunctionGroup(5); 239 } 240 241 /** 242 * Send the CBUS message to set the state of functions F29 - F36 243 */ 244 @Override 245 protected void sendFunctionGroup6() { 246 sendFunctionGroup(6); 247 } 248 249 protected void sendFunctionGroup(int group) { 250 if (!hasActiveSession()) { 251 log.debug("no active session, function group {} not sent to layout", group); 252 return; 253 } 254 int totVal = 0; 255 for ( int i=0; i<CbusConstants.MAX_FUNCTIONS; i++ ){ 256 if (FUNCTION_GROUPS[i]==group && getFunction(i)){ 257 totVal += CbusConstants.CBUS_FUNCTION_BITS[i]; 258 } 259 } 260 cs.setFunctions(group, _handle, totVal); 261 } 262 263 protected void updateFunctionGroup(int group, int fns) { 264 for ( int i=0; i<CbusConstants.MAX_FUNCTIONS; i++ ){ 265 if (FUNCTION_GROUPS[i]==group){ 266 updateFunction( i, (fns & CbusConstants.CBUS_FUNCTION_BITS[i]) == CbusConstants.CBUS_FUNCTION_BITS[i] ); 267 } 268 } 269 } 270 271 /** 272 * Set the speed. 273 * <p> 274 * This intentionally skips the emergency stop value of 1. 275 * 276 * @param speed Number from 0 to 1; less than zero is emergency stop 277 */ 278 @Override 279 public synchronized void setSpeedSetting(float speed, boolean allowDuplicates, boolean allowDuplicatesOnStop) { 280 log.debug("setSpeedSetting({})", speed); 281 float oldSpeed = this.speedSetting; 282 this.speedSetting = speed; 283 if (speed < 0) { 284 this.speedSetting = -1.f; 285 } 286 287 setDispatchActive(this.speedSetting > 0); 288 289 if ( oldCbusSpeed != getCbusSpeedDirection() || allowDuplicates || ( speed==0 && allowDuplicatesOnStop ) ) { 290 sendToLayout(); 291 firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting); 292 record(this.speedSetting); // float 293 } 294 } 295 296 private synchronized int getCbusSpeedFromFloat(){ 297 switch (speedStepMode) { 298 case NMRA_DCC_28: 299 int ints = intSpeed( this.speedSetting, 29 ); 300 // speed 1 starts at cbus 4, not 2. 301 if (ints>1){ 302 ints += 2; 303 } 304 return ints; 305 case NMRA_DCC_14: 306 return intSpeed( this.speedSetting, 15 ); 307 case NMRA_DCC_128: 308 default: 309 // cbus speeds 0x00 to 0x80 , excluding 0x01 for estop gives 127 possible values including 0. 310 return intSpeed( this.speedSetting ); 311 } 312 } 313 314 private synchronized int getCbusSpeedDirection(){ 315 int speed = getCbusSpeedFromFloat(); 316 if (this.isForward) { 317 speed |= 0x80; 318 } 319 return speed; 320 } 321 322 private int oldCbusSpeed = -2; 323 324 // following a speed or direction change, sends to layout 325 private void sendToLayout() { 326 int newCbusSpeed = getCbusSpeedDirection(); 327 if (!hasActiveSession()) { 328 // disposed, or stolen and awaiting session recovery, 329 // local state is updated but nothing is sent 330 log.debug("no active session, speed/dir {} not sent to layout", newCbusSpeed); 331 return; 332 } 333 oldCbusSpeed = newCbusSpeed; 334 335 log.debug("Sending speed/dir for speed: {}",oldCbusSpeed); 336 // reset keep-alive timeout, timer may have been removed by a concurrent dispose 337 javax.swing.Timer timer = mRefreshTimer; 338 if ( timer != null ) { 339 timer.restart(); 340 } 341 cs.setSpeedDir(_handle, oldCbusSpeed); 342 } 343 344 /** 345 * Update the throttles speed setting without sending to hardware. Used to 346 * support CBUS sharing by taking speed received <b>from</b> the hardware in 347 * an OPC_DSPD message. 348 * <p> 349 * No compensation required for a direction flag 350 * @param speed integer speed value 351 */ 352 protected synchronized void updateSpeedSetting(int speed) { 353 354 log.debug("Updated speed/dir for speed:{}",speed); 355 356 float oldSpeed = this.speedSetting; 357 this.speedSetting = floatSpeed(speed); 358 if (speed < 0) { 359 this.speedSetting = -1.f; 360 } 361 362 setDispatchActive(this.speedSetting > 0); 363 364 if (Math.abs(oldSpeed - this.speedSetting) > 0.0001) { // an increment FROM CBUS will always be > 0.00793 365 firePropertyChange(SPEEDSETTING, oldSpeed, this.speedSetting); 366 record(this.speedSetting); // float 367 } 368 } 369 370 /** 371 * Set the direction and reset speed. 372 * Forwards to the layout 373 * {@inheritDoc} 374 */ 375 @Override 376 public void setIsForward(boolean forward) { 377 boolean old = this.isForward; 378 this.isForward = forward; 379 if (old != this.isForward) { 380 sendToLayout(); 381 firePropertyChange(ISFORWARD, old, isForward); 382 } 383 } 384 385 /** 386 * Update the throttles direction without sending to hardware.Used to 387 * support CBUS sharing by taking direction received <b>from</b> the 388 * hardware in an OPC_DSPD message. 389 * @param forward True if Forward, else False 390 */ 391 protected void updateIsForward(boolean forward){ 392 super.setIsForward(forward); 393 } 394 395 /** 396 * {@inheritDoc} 397 */ 398 @Override 399 public String toString() { 400 return getLocoAddress().toString(); 401 } 402 403 /** 404 * Return the handle for this throttle 405 * 406 * @return integer session handle 407 */ 408 protected int getHandle() { 409 return _handle; 410 } 411 412 /** 413 * Set the handle for this throttle 414 * <p> 415 * This is normally done on Throttle Construction but certain 416 * operations, eg. recovering from an external steal 417 * may need to change this. 418 * @param newHandle session handle 419 */ 420 protected void setHandle(int newHandle){ 421 _handle = newHandle; 422 } 423 424 /** 425 * Set Throttle Stolen Flag 426 * <p> 427 * This is false on Throttle Construction but certain 428 * operations may need to change this, eg. an external steal. 429 * <p> 430 * Sends IsAvailable Property Change Notification 431 * @param isStolen true if Throttle has been stolen, else false 432 */ 433 protected void setStolen(boolean isStolen){ 434 if (isStolen != _isStolen){ 435 boolean wasStolen = _isStolen; 436 _isStolen = isStolen; // update state before notifying listeners 437 firePropertyChange("IsAvailable", !wasStolen, !isStolen); // PCL is opposite of local boolean 438 } 439 if (isStolen){ // stop keep-alive messages 440 if ( mRefreshTimer != null ) { 441 mRefreshTimer.stop(); 442 } 443 mRefreshTimer = null; 444 } 445 else { 446 startRefresh(); // resume keep-alive messages 447 } 448 } 449 450 /** 451 * Get Throttle Stolen Flag 452 * <p> 453 * This is false on Throttle Construction but certain 454 * operations may need to change this, eg. an external steal. 455 * @return true if Throttle has been stolen, else false 456 */ 457 protected boolean isStolen(){ 458 return _isStolen; 459 } 460 461 /** 462 * Get the number of external steal recovery attempts 463 * @return Number of attempts since last reset 464 */ 465 protected int getNumRecoverAttempts(){ 466 return _recoveryAttempts; 467 } 468 469 /** 470 * Increase a count of external steal recovery attempts 471 */ 472 protected void increaseNumRecoverAttempts(){ 473 _recoveryAttempts++; 474 } 475 476 /** 477 * Reset count of recovery attempts 478 */ 479 protected void resetNumRecoverAttempts(){ 480 _recoveryAttempts = 0; 481 } 482 483 /** 484 * Release session from a command station 485 * ie. throttle with clean full dispose called from releaseThrottle 486 */ 487 protected void releaseFromCommandStation(){ 488 if ( hasActiveSession() ) { 489 cs.releaseSession(_handle); 490 } else { 491 log.debug("no active session to release for {}", this); 492 } 493 } 494 495 /** 496 * Dispose when finished with this object. After this, further usage of this 497 * Throttle object will result in a JmriException. 498 */ 499 @Override 500 public void throttleDispose() { 501 log.debug("dispose"); 502 503 finishRecord(); 504 505 notifyThrottleDisconnect(); 506 507 // stop timeout 508 if ( mRefreshTimer != null ) { 509 mRefreshTimer.stop(); 510 } 511 mRefreshTimer = null; 512 cs = null; 513 _handle = -1; 514 515 } 516 517 private javax.swing.Timer mRefreshTimer; 518 519 // CBUS command stations expect DSPD per sesison every 4s 520 protected final void startRefresh() { 521 mRefreshTimer = new javax.swing.Timer(4000, (java.awt.event.ActionEvent e) -> keepAlive()); 522 mRefreshTimer.setRepeats(true); // refresh until stopped by dispose 523 mRefreshTimer.start(); 524 } 525 526 /** 527 * Internal routine to resend the speed on a timeout 528 */ 529 private synchronized void keepAlive() { 530 // cs is null and handle -1 if terminated, handle -1 if stolen 531 if (!hasActiveSession()) { 532 return; 533 } 534 cs.sendKeepAlive(_handle); 535 536 // reset timeout, refresh until stopped by dispose 537 // timer may have been removed by a concurrent dispose or steal 538 javax.swing.Timer timer = mRefreshTimer; 539 if ( timer != null ) { 540 timer.restart(); 541 } 542 } 543 544 /** 545 * {@inheritDoc} 546 */ 547 @Override 548 public LocoAddress getLocoAddress() { 549 return dccAddress; 550 } 551 552 /** 553 * Adds extra check for num of this JMRI throttle users before notifying 554 * and makes sure these always get sent as a pair 555 * the abstracts only send to ThrottleListeners if value has been changed 556 * @param newval set true if dispatch can be enabled, else false 557 */ 558 protected void setDispatchActive( boolean newval){ 559 560 // feature disabled if command station not listed in CBUS node table, 561 // could be CANCMD v3 or in test 562 if ( cs == null ) { 563 return; 564 } 565 if ( cs.getMasterCommandStation() == null ) { 566 return; 567 } 568 569 if ( newval ) { 570 int numThrottles = adapterMemo.get(ThrottleManager.class).getThrottleUsageCount(dccAddress); 571 log.debug("numThrottles {}",numThrottles); 572 if ( numThrottles < 2 ){ 573 notifyThrottleReleaseEnabled(false); 574 notifyThrottleDispatchEnabled(true); 575 return; 576 } 577 } 578 notifyThrottleReleaseEnabled(true); 579 notifyThrottleDispatchEnabled(false); 580 } 581 582 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CbusThrottle.class); 583 584}