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 boolean pending = false; 236 237 /** 238 * Set the turnout DCC C bit to OFF. This is typically used to set a C bit 239 * that was set ON to OFF after a timeout. 240 * 241 * @param state the turnout state 242 */ 243 void sendSetOffMessage(int state) { 244 sendOpcSwReqMessage(adjustStateForInversion(state), false); 245 } 246 247 private void handleReceivedOpSwAckReq(LocoNetMessage l) { 248 int sw2 = l.getElement(2); 249 if (myAddress(l.getElement(1), sw2)) { 250 251 log.debug("SW_REQ received with valid address"); 252 //sort out states 253 int state; 254 state = ((sw2 & LnConstants.OPC_SW_REQ_DIR) != 0) ? CLOSED : THROWN; 255 state = adjustStateForInversion(state); 256 257 newCommandedState(state); 258 computeKnownStateOpSwAckReq(sw2, state); 259 } 260 } 261 262 private void computeKnownStateOpSwAckReq(int sw2, int state) { 263 boolean on = ((sw2 & LnConstants.OPC_SW_REQ_OUT) != 0); 264 switch (getFeedbackMode()) { 265 case MONITORING: 266 if ((!on) || (!_useOffSwReqAsConfirmation)) { 267 newKnownState(state); 268 } 269 break; 270 case DIRECT: 271 newKnownState(state); 272 break; 273 default: 274 break; 275 } 276 277 } 278 private void setKnownStateFromOutputStateClosedReport() { 279 newCommandedState(CLOSED); 280 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 281 newKnownState(CLOSED); 282 } else if (getFeedbackMode() == LNALTERNATE) { 283 newKnownState(adjustStateForInversion(CLOSED)); 284 } 285 } 286 287 private void setKnownStateFromOutputStateThrownReport() { 288 newCommandedState(THROWN); 289 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 290 newKnownState(THROWN); 291 } else if (getFeedbackMode() == LNALTERNATE) { 292 newKnownState(adjustStateForInversion(THROWN)); 293 } 294 } 295 296 private void setKnownStateFromOutputStateOddReport() { 297 newCommandedState(CLOSED + THROWN); 298 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 299 newKnownState(CLOSED + THROWN); 300 } 301 } 302 303 private void setKnownStateFromOutputStateReallyOddReport() { 304 newCommandedState(0); 305 if (getFeedbackMode() == MONITORING || getFeedbackMode() == DIRECT) { 306 newKnownState(0); 307 } else if (getFeedbackMode() == LNALTERNATE) { 308 newKnownState(INCONSISTENT); 309 } 310 } 311 312 private void computeFromOutputStateReport(int sw2) { 313 // LnConstants.OPC_SW_REP_INPUTS not set, these report outputs 314 // sort out states 315 int state; 316 state = sw2 317 & (LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN); 318 state = adjustStateForInversion(state); 319 320 switch (state) { 321 case LnConstants.OPC_SW_REP_CLOSED: 322 setKnownStateFromOutputStateClosedReport(); 323 break; 324 case LnConstants.OPC_SW_REP_THROWN: 325 setKnownStateFromOutputStateThrownReport(); 326 break; 327 case LnConstants.OPC_SW_REP_CLOSED | LnConstants.OPC_SW_REP_THROWN: 328 setKnownStateFromOutputStateOddReport(); 329 break; 330 default: 331 setKnownStateFromOutputStateReallyOddReport(); 332 break; 333 } 334 } 335 336 private void computeFeedbackFromSwitchReport(int sw2) { 337 // Switch input report 338 if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) { 339 computeFeedbackFromSwitchOffReport(); 340 } else { 341 computeFeedbackFromSwitchOnReport(); 342 } 343 } 344 345 private void computeFeedbackFromSwitchOffReport() { 346 // switch input closed (off) 347 if (getFeedbackMode() == EXACT) { 348 // reached closed state 349 newKnownState(adjustStateForInversion(CLOSED)); 350 } else if (getFeedbackMode() == INDIRECT) { 351 // reached closed state 352 newKnownState(adjustStateForInversion(CLOSED)); 353 } else if (!feedbackDeliberatelySet) { 354 // don't have a defined feedback mode, but know we've reached closed state 355 log.debug("setting CLOSED with !feedbackDeliberatelySet"); 356 newKnownState(adjustStateForInversion(CLOSED)); 357 } 358 } 359 360 private void computeFeedbackFromSwitchOnReport() { 361 // switch input thrown (input on) 362 if (getFeedbackMode() == EXACT) { 363 // leaving CLOSED on way to THROWN, go INCONSISTENT if not already THROWN 364 if (getKnownState() != THROWN) { 365 newKnownState(INCONSISTENT); 366 } 367 } else if (getFeedbackMode() == INDIRECT) { 368 // reached thrown state 369 newKnownState(adjustStateForInversion(THROWN)); 370 } else if (!feedbackDeliberatelySet) { 371 // don't have a defined feedback mode, but know we're not in closed state, most likely is actually thrown 372 log.debug("setting THROWN with !feedbackDeliberatelySet"); 373 newKnownState(adjustStateForInversion(THROWN)); 374 } 375 } 376 377 private void computeFromSwFeedbackState(int sw2) { 378 // LnConstants.OPC_SW_REP_INPUTS set, these are feedback messages from inputs 379 // sort out states 380 if ((sw2 & LnConstants.OPC_SW_REP_SW) != 0) { 381 computeFeedbackFromSwitchReport(sw2); 382 383 } else { 384 computeFeedbackFromAuxInputReport(sw2); 385 } 386 } 387 388 private void computeFeedbackFromAuxInputReport(int sw2) { 389 // This is only valid in EXACT mode, so if we encounter it 390 // without a feedback mode set, we switch to EXACT 391 if (!feedbackDeliberatelySet) { 392 setFeedbackMode(EXACT); 393 feedbackDeliberatelySet = false; // was set when setting feedback 394 } 395 396 if ((sw2 & LnConstants.OPC_SW_REP_HI) != 0) { 397 // aux input closed (off) 398 if (getFeedbackMode() == EXACT) { 399 // reached thrown state 400 newKnownState(adjustStateForInversion(THROWN)); 401 } 402 } else { 403 // aux input thrown (input on) 404 if (getFeedbackMode() == EXACT) { 405 // leaving THROWN on the way to CLOSED, go INCONSISTENT if not already CLOSED 406 if (getKnownState() != CLOSED) { 407 newKnownState(INCONSISTENT); 408 } 409 } 410 } 411 } 412 413 private void handleReceivedOpSwRep(LocoNetMessage l) { 414 int sw1 = l.getElement(1); 415 int sw2 = l.getElement(2); 416 if (myAddress(sw1, sw2)) { 417 418 log.debug("SW_REP received with valid address"); 419 // see if its a turnout state report 420 if ((sw2 & LnConstants.OPC_SW_REP_INPUTS) == 0) { 421 computeFromOutputStateReport(sw2); 422 } else { 423 computeFromSwFeedbackState(sw2); 424 } 425 } 426 } 427 428 // implementing classes will typically have a function/listener to get 429 // updates from the layout, which will then call 430 // public void firePropertyChange(String propertyName, 431 // Object oldValue, 432 // Object newValue) 433 // _once_ if anything has changed state (or set the commanded state directly) 434 public void messageFromManager(LocoNetMessage l) { 435 // parse message type 436 switch (l.getOpCode()) { 437 case LnConstants.OPC_SW_ACK: 438 case LnConstants.OPC_SW_REQ: { 439 handleReceivedOpSwAckReq(l); 440 return; 441 } 442 case LnConstants.OPC_SW_REP: { 443 handleReceivedOpSwRep(l); 444 return; 445 } 446 default: 447 return; 448 } 449 } 450 451 @Override 452 protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) { 453 if (log.isDebugEnabled()) { 454 log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, _number); 455 } 456 } 457 458 @Override 459 public void dispose() { 460 if(meterTask!=null) { 461 meterTask.cancel(); 462 } 463 if(consistencyTask != null ) { 464 consistencyTask.cancel(); 465 } 466 super.dispose(); 467 } 468 469 // data members 470 int _number; // LocoNet Turnout number 471 472 private boolean myAddress(int a1, int a2) { 473 // the "+ 1" in the following converts to throttle-visible numbering 474 return (((a2 & 0x0f) * 128) + (a1 & 0x7f) + 1) == _number; 475 } 476 477 //ln turnouts do support inversion 478 @Override 479 public boolean canInvert() { 480 return true; 481 } 482 483 /** 484 * Take a turnout state as a parameter and adjusts it as necessary 485 * to reflect the turnout "Invert" property. 486 * 487 * @param rawState "original" turnout state before optional inverting 488 */ 489 private int adjustStateForInversion(int rawState) { 490 491 if (getInverted() && (rawState == CLOSED || rawState == THROWN)) { 492 if (rawState == CLOSED) { 493 return THROWN; 494 } else { 495 return CLOSED; 496 } 497 } else { 498 return rawState; 499 } 500 } 501 502 static final int METERINTERVAL = 100; // msec wait before closed 503 private java.util.TimerTask meterTask = null; 504 505 static final int CONSISTENCYTIMER = 3000; // msec wait for command to take effect 506 int noConsistencyTimersRunning = 0; 507 private java.util.TimerTask consistencyTask = null; 508 509 private final static Logger log = LoggerFactory.getLogger(LnTurnout.class); 510 511}