001package jmri.jmrix.loconet; 002 003import javax.annotation.*; 004import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 005import jmri.NmraPacket; 006import jmri.implementation.AbstractTurnout; 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009 010/** 011 * Extend jmri.AbstractTurnout for LocoNet layouts 012 * <p> 013 * This implementation implements the "SENT" feedback, where LocoNet messages 014 * originating on the layout can change both KnownState and CommandedState. We 015 * change both because we consider a LocoNet message to reflect how the turnout 016 * should be, even if it's a readback status message. E.g. if you use a DS54 017 * local input to change the state, resulting in a status message, we still 018 * consider that to be a commanded state change. 019 * <p> 020 * Adds several additional feedback modes: 021 * <ul> 022 * <li>MONITORING - listen to the LocoNet, so that commands from other LocoNet 023 * sources (e.g. throttles) are properly reflected in the turnout state. This is 024 * the default for LnTurnout objects as created. 025 * <li>INDIRECT - listen to the LocoNet for messages back from a DS54 that has a 026 * microswitch attached to its Switch input. 027 * <li>EXACT - listen to the LocoNet for messages back from a DS54 that has two 028 * microswitches, one connected to the Switch input and one to the Aux input. 029 * <li>ALTERNATE - listen to the LocoNet for messages back from a MGP decoders 030 * that has reports servo moving. 031 * </ul> 032 * Some of the message formats used in this class are Copyright Digitrax, Inc. 033 * and used with permission as part of the JMRI project. That permission does 034 * not extend to uses in other software products. If you wish to use this code, 035 * algorithm or these message formats outside of JMRI, please contact Digitrax 036 * Inc for separate permission. 037 * 038 * @author Bob Jacobsen Copyright (C) 2001 039 */ 040public class LnTurnout extends AbstractTurnout { 041 042 public LnTurnout(String prefix, int number, LocoNetInterface controller) throws IllegalArgumentException { 043 // a human-readable turnout number must be specified! 044 super(prefix + "T" + number); // can't use prefix here, as still in construction 045 _prefix = prefix; 046 log.debug("new turnout {}", number); 047 if (number < NmraPacket.accIdLowLimit || number > NmraPacket.accIdAltHighLimit) { 048 throw new IllegalArgumentException("Turnout value: " + number // NOI18N 049 + " not in the range " + NmraPacket.accIdLowLimit + " to " // NOI18N 050 + NmraPacket.accIdAltHighLimit); 051 } 052 053 this.controller = controller; 054 055 _number = number; 056 // update feedback modes 057 _validFeedbackTypes |= MONITORING | EXACT | INDIRECT | LNALTERNATE ; 058 _activeFeedbackType = MONITORING; 059 060 // if needed, create the list of feedback mode 061 // names with additional LocoNet-specific modes 062 if (modeNames == null) { 063 initFeedbackModes(); 064 } 065 _validFeedbackNames = modeNames; 066 _validFeedbackModes = modeValues; 067 } 068 069 LocoNetInterface controller; 070 protected String _prefix = "L"; // default to "L" 071 072 /** 073 * True when setFeedbackMode has specified the mode; 074 * false when the mode is just left over from initialization. 075 * This is intended to indicate (when true) that a configuration 076 * file has set the value; message-created turnouts have it false. 077 */ 078 boolean feedbackDeliberatelySet = false; // package to allow access from LnTurnoutManager 079 080 @Override 081 public void setBinaryOutput(boolean state) { 082 // TODO Auto-generated method stub 083 setProperty(LnTurnoutManager.SENDONANDOFFKEY, !state); 084 binaryOutput = state; 085 } 086 @Override 087 public void setFeedbackMode(@Nonnull String mode) throws IllegalArgumentException { 088 feedbackDeliberatelySet = true; 089 super.setFeedbackMode(mode); 090 } 091 092 @Override 093 public void setFeedbackMode(int mode) throws IllegalArgumentException { 094 feedbackDeliberatelySet = true; 095 super.setFeedbackMode(mode); 096 } 097 098 @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", 099 justification = "Only used during creation of 1st turnout") // NOI18N 100 private void initFeedbackModes() { 101 if (_validFeedbackNames.length != _validFeedbackModes.length) { 102 log.error("int and string feedback arrays different length"); 103 } 104 String[] tempModeNames = new String[_validFeedbackNames.length + 4]; 105 int[] tempModeValues = new int[_validFeedbackNames.length + 4]; 106 for (int i = 0; i < _validFeedbackNames.length; i++) { 107 tempModeNames[i] = _validFeedbackNames[i]; 108 tempModeValues[i] = _validFeedbackModes[i]; 109 } 110 tempModeNames[_validFeedbackNames.length] = "MONITORING"; // NOI18N 111 tempModeValues[_validFeedbackNames.length] = MONITORING; 112 tempModeNames[_validFeedbackNames.length + 1] = "INDIRECT"; // NOI18N 113 tempModeValues[_validFeedbackNames.length + 1] = INDIRECT; 114 tempModeNames[_validFeedbackNames.length + 2] = "EXACT"; // NOI18N 115 tempModeValues[_validFeedbackNames.length + 2] = EXACT; 116 tempModeNames[_validFeedbackNames.length + 3] = "LNALTERNATE"; // NOI18N 117 tempModeValues[_validFeedbackNames.length + 3] = LNALTERNATE; 118 119 modeNames = tempModeNames; 120 modeValues = tempModeValues; 121 } 122 123 static String[] modeNames = null; 124 static int[] modeValues = null; 125 126 public int getNumber() { 127 return _number; 128 } 129 130 boolean _useOffSwReqAsConfirmation = false; 131 132 public void setUseOffSwReqAsConfirmation(boolean state) { 133 _useOffSwReqAsConfirmation = state; 134 } 135 136 public boolean isByPassBushbyBit() { 137 Object returnVal = getProperty(LnTurnoutManager.BYPASSBUSHBYBITKEY); 138 if (returnVal == null) { 139 return false; 140 } 141 return (boolean) returnVal; 142 } 143 144 public boolean isSendOnAndOff() { 145 Object returnVal = getProperty(LnTurnoutManager.SENDONANDOFFKEY); 146 if (returnVal == null) { 147 return true; 148 } 149 return (boolean) returnVal; 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 @Override 156 protected void forwardCommandChangeToLayout(final int newstate) { 157 158 // send SWREQ for close/thrown ON 159 sendOpcSwReqMessage(adjustStateForInversion(newstate), true); 160 // schedule SWREQ for closed/thrown off, unless in basic mode 161 if (isSendOnAndOff()) { 162 meterTask = new java.util.TimerTask() { 163 int state = newstate; 164 165 @Override 166 public void run() { 167 try { 168 sendSetOffMessage(state); 169 } catch (Exception e) { 170 log.error("Exception occurred while sending delayed off to turnout", e); 171 } 172 } 173 }; 174 jmri.util.TimerUtil.schedule(meterTask, METERINTERVAL); 175 } 176 } 177 178 /** 179 * Send a single OPC_SW_REQ message for this turnout, with the CLOSED/THROWN 180 * ON/OFF state. 181 * <p> 182 * Inversion is to already have been handled. 183 * 184 * @param state the state to set 185 * @param on if true the C bit of the NMRA DCC packet is 1; if false the 186 * C bit is 0 187 */ 188 void sendOpcSwReqMessage(int state, boolean on) { 189 LocoNetMessage l = new LocoNetMessage(4); 190 l.setOpCode(isByPassBushbyBit() ? LnConstants.OPC_SW_ACK : LnConstants.OPC_SW_REQ); 191 int hiadr = ((_number - 1) / 128) & 0x7F; // compute address fields 192 l.setElement(1, ((_number - 1) - hiadr * 128) & 0x7F); 193 194 // set closed bit (Note that LocoNet cannot handle both Thrown and Closed) 195 if ((state & CLOSED) != 0) { 196 hiadr |= 0x20; 197 // thrown exception if also THROWN 198 if ((state & THROWN) != 0) { 199 log.error("LocoNet turnout logic can't handle both THROWN and CLOSED yet"); 200 } 201 } 202 203 // load On/Off 204 if (on) { 205 hiadr |= 0x10; 206 } else if (_useOffSwReqAsConfirmation) { 207 log.warn("Turnout {} is using OPC_SWREQ off as confirmation, but is sending OFF commands itself anyway", _number); 208 } 209 210 l.setElement(2, hiadr); 211 212 this.controller.sendLocoNetMessage(l); // send message 213 214 if (_useOffSwReqAsConfirmation) { 215 noConsistencyTimersRunning++; 216 startConsistencyTimerTask(); 217 } 218 } 219 220 private void startConsistencyTimerTask() { 221 // Start a timer to resend the command in a couple of seconds in case consistency is not obtained before then 222 consistencyTask = new java.util.TimerTask() { 223 @Override 224 public void run() { 225 noConsistencyTimersRunning--; 226 if (!isConsistentState() && noConsistencyTimersRunning == 0) { 227 log.debug("LnTurnout resending command for turnout {}", _number); 228 forwardCommandChangeToLayout(getCommandedState()); 229 } 230 } 231 }; 232 jmri.util.TimerUtil.schedule(consistencyTask, CONSISTENCYTIMER); 233 } 234 235 /** 236 * Set the turnout DCC C bit to OFF. This is typically used to set a C bit 237 * that was set ON to OFF after a timeout. 238 * 239 * @param state the turnout state 240 */ 241 void sendSetOffMessage(int state) { 242 sendOpcSwReqMessage(adjustStateForInversion(state), false); 243 } 244 245 private void handleReceivedOpSwAckReq(LocoNetMessage l) { 246 int sw2 = l.getElement(2); 247 if (myAddress(l.getElement(1), sw2)) { 248 249 log.debug("SW_REQ received with valid address"); 250 //sort out states 251 int state; 252 state = ((sw2 & LnConstants.OPC_SW_REQ_DIR) != 0) ? CLOSED : THROWN; 253 state = adjustStateForInversion(state); 254 255 newCommandedState(state); 256 computeKnownStateOpSwAckReq(sw2, state); 257 } 258 } 259 260 private void computeKnownStateOpSwAckReq(int sw2, int state) { 261 boolean on = ((sw2 & LnConstants.OPC_SW_REQ_OUT) != 0); 262 switch (getFeedbackMode()) { 263 case MONITORING: 264 if ((!on) || (!_useOffSwReqAsConfirmation)) { 265 newKnownState(state); 266 } 267 break; 268 case DIRECT: 269 newKnownState(state); 270 break; 271 default: 272 break; 273 } 274 275 } 276 private void setKnownStateFromOutputStateClosedReport() { 277 newCommandedState(CLOSED); 278 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 279 newKnownState(CLOSED); 280 } else if (getFeedbackMode() == LNALTERNATE) { 281 newKnownState(adjustStateForInversion(CLOSED)); 282 } 283 } 284 285 private void setKnownStateFromOutputStateThrownReport() { 286 newCommandedState(THROWN); 287 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 288 newKnownState(THROWN); 289 } else if (getFeedbackMode() == LNALTERNATE) { 290 newKnownState(adjustStateForInversion(THROWN)); 291 } 292 } 293 294 private void setKnownStateFromOutputStateOddReport() { 295 newCommandedState(CLOSED + THROWN); 296 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 297 newKnownState(CLOSED + THROWN); 298 } 299 } 300 301 private void setKnownStateFromOutputStateReallyOddReport() { 302 newCommandedState(0); 303 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 304 newKnownState(0); 305 } else if (getFeedbackMode() == LNALTERNATE) { 306 newKnownState(INCONSISTENT); 307 } 308 } 309 310 private void computeFromOutputStateReport(int sw2) { 311 // LnConstants.OPC_SW_REP_INPUTS not set, these report outputs 312 // sort out states 313 int state; 314 state = sw2 315 & (LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN); 316 state = adjustStateForInversion(state); 317 318 switch (state) { 319 case LnConstants.OPC_SW_REP_CLOSED: 320 setKnownStateFromOutputStateClosedReport(); 321 break; 322 case LnConstants.OPC_SW_REP_THROWN: 323 setKnownStateFromOutputStateThrownReport(); 324 break; 325 case LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN: 326 setKnownStateFromOutputStateOddReport(); 327 break; 328 default: 329 setKnownStateFromOutputStateReallyOddReport(); 330 break; 331 } 332 } 333 334 private void computeFeedbackFromSwitchReport(int sw2) { 335 // Switch input report 336 if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) { 337 computeFeedbackFromSwitchOffReport(); 338 } else { 339 computeFeedbackFromSwitchOnReport(); 340 } 341 } 342 343 private void computeFeedbackFromSwitchOffReport() { 344 // switch input closed (off) 345 if (getFeedbackMode() == EXACT) { 346 // reached closed state 347 newKnownState(adjustStateForInversion(CLOSED)); 348 } else if (getFeedbackMode() == INDIRECT) { 349 // reached closed state 350 newKnownState(adjustStateForInversion(CLOSED)); 351 } else if (!feedbackDeliberatelySet) { 352 // don't have a defined feedback mode, but know we've reached closed state 353 log.debug("setting CLOSED with !feedbackDeliberatelySet"); 354 newKnownState(adjustStateForInversion(CLOSED)); 355 } 356 } 357 358 private void computeFeedbackFromSwitchOnReport() { 359 // switch input thrown (input on) 360 if (getFeedbackMode() == EXACT) { 361 // leaving CLOSED on way to THROWN, go INCONSISTENT if not already THROWN 362 if (getKnownState() != THROWN) { 363 newKnownState(INCONSISTENT); 364 } 365 } else if (getFeedbackMode() == INDIRECT) { 366 // reached thrown state 367 newKnownState(adjustStateForInversion(THROWN)); 368 } else if (!feedbackDeliberatelySet) { 369 // don't have a defined feedback mode, but know we're not in closed state, most likely is actually thrown 370 log.debug("setting THROWN with !feedbackDeliberatelySet"); 371 newKnownState(adjustStateForInversion(THROWN)); 372 } 373 } 374 375 private void computeFromSwFeedbackState(int sw2) { 376 // LnConstants.OPC_SW_REP_INPUTS set, these are feedback messages from inputs 377 // sort out states 378 if ((sw2 & LnConstants.OPC_SW_REP_SW) != 0) { 379 computeFeedbackFromSwitchReport(sw2); 380 381 } else { 382 computeFeedbackFromAuxInputReport(sw2); 383 } 384 } 385 386 private void computeFeedbackFromAuxInputReport(int sw2) { 387 // This is only valid in EXACT mode, so if we encounter it 388 // without a feedback mode set, we switch to EXACT 389 if (!feedbackDeliberatelySet) { 390 setFeedbackMode(EXACT); 391 feedbackDeliberatelySet = false; // was set when setting feedback 392 } 393 394 if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) { 395 // aux input closed (off) 396 if (getFeedbackMode() == EXACT) { 397 // reached thrown state 398 newKnownState(adjustStateForInversion(THROWN)); 399 } 400 } else { 401 // aux input thrown (input on) 402 if (getFeedbackMode() == EXACT) { 403 // leaving THROWN on the way to CLOSED, go INCONSISTENT if not already CLOSED 404 if (getKnownState() != CLOSED) { 405 newKnownState(INCONSISTENT); 406 } 407 } 408 } 409 } 410 411 private void handleReceivedOpSwRep(LocoNetMessage l) { 412 int sw1 = l.getElement(1); 413 int sw2 = l.getElement(2); 414 if (myAddress(sw1, sw2)) { 415 416 log.debug("SW_REP received with valid address"); 417 // see if its a turnout state report 418 if ((sw2 & LnConstants.OPC_SW_REP_INPUTS) == 0) { 419 computeFromOutputStateReport(sw2); 420 } else { 421 computeFromSwFeedbackState(sw2); 422 } 423 } 424 } 425 426 // implementing classes will typically have a function/listener to get 427 // updates from the layout, which will then call 428 // public void firePropertyChange(String propertyName, 429 // Object oldValue, 430 // Object newValue) 431 // _once_ if anything has changed state (or set the commanded state directly) 432 public void messageFromManager(LocoNetMessage l) { 433 // parse message type 434 switch (l.getOpCode()) { 435 case LnConstants.OPC_SW_ACK: 436 case LnConstants.OPC_SW_REQ: { 437 handleReceivedOpSwAckReq(l); 438 return; 439 } 440 case LnConstants.OPC_SW_REP: { 441 handleReceivedOpSwRep(l); 442 return; 443 } 444 default: 445 return; 446 } 447 } 448 449 /** {@inheritDoc} */ 450 @Override 451 public void requestUpdateFromLayout() { 452 if (_activeFeedbackType == MONITORING || _activeFeedbackType == INDIRECT) { 453 LocoNetMessage l = new LocoNetMessage(4); 454 l.setOpCode(LnConstants.OPC_SW_STATE); 455 l.setElement(1, (_number-1) & 0x7f); 456 l.setElement(2, (_number-1) >> 7); 457 this.controller.sendLocoNetMessage(l); // send message 458 } else { 459 super.requestUpdateFromLayout(); 460 } 461 } 462 463 @Override 464 protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) { 465 if (log.isDebugEnabled()) { 466 log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, _number); 467 } 468 } 469 470 @Override 471 public void dispose() { 472 if(meterTask!=null) { 473 meterTask.cancel(); 474 } 475 if(consistencyTask != null ) { 476 consistencyTask.cancel(); 477 } 478 super.dispose(); 479 } 480 481 // data members 482 int _number; // LocoNet Turnout number 483 484 private boolean myAddress(int a1, int a2) { 485 // the "+ 1" in the following converts to throttle-visible numbering 486 return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1) == _number; 487 } 488 489 //ln turnouts do support inversion 490 @Override 491 public boolean canInvert() { 492 return true; 493 } 494 495 /** 496 * Take a turnout state as a parameter and adjusts it as necessary 497 * to reflect the turnout "Invert" property. 498 * 499 * @param rawState "original" turnout state before optional inverting 500 */ 501 private int adjustStateForInversion(int rawState) { 502 503 if (getInverted() && (rawState == CLOSED || rawState == THROWN)) { 504 if (rawState == CLOSED) { 505 return THROWN; 506 } else { 507 return CLOSED; 508 } 509 } else { 510 return rawState; 511 } 512 } 513 514 static final int METERINTERVAL = 100; // msec wait before closed 515 private java.util.TimerTask meterTask = null; 516 517 static final int CONSISTENCYTIMER = 3000; // msec wait for command to take effect 518 int noConsistencyTimersRunning = 0; 519 private java.util.TimerTask consistencyTask = null; 520 521 private final static Logger log = LoggerFactory.getLogger(LnTurnout.class); 522 523}