001package jmri.jmrix.dccpp; 002 003import jmri.implementation.AbstractTurnout; 004import org.slf4j.Logger; 005import org.slf4j.LoggerFactory; 006 007/** 008 * Extends jmri.AbstractTurnout for DCCpp layouts 009 * <p> 010 * Turnouts on DCC-EX are controlled (as of V1.5 Firmware) 011 * with unidirectional Stationary Decoder commands, or with bidirectional 012 * (predefined) Turnout commands, or with bidirectional (predefined) Output 013 * commands. 014 * 015 * DCC-EX Has three ways to activate a turnout (output) 016 * <ul> 017 * <li> Accessory Command "a" : sends a DCC packet to a stationary decoder 018 * out there on the bus somewhere. NO RETURN VALUE to JMRI. 019 * </li> 020 * <li> Turnout Command "T" : Looks up a DCC address from an internal table 021 * in the Base Station and sends that Stationary Decoder a packet. Returns 022 * a (basically faked) "H" response to JMRI indicating the (supposed) 023 * current state of the turnout. Or "X" if the indexed turnout is not in 024 * the list. 025 * </li> 026 * <li> Output Command "z" : Looks up a Base Station Arduino Pin number from 027 * an internal lookup table, and sets/toggles the state of that pin. 028 * Returns a "Y" response indicating the actual state of the pin. Or "X" 029 * if the indexed pin is not in the list. 030 * </li> 031 * </ul> 032 * 033 * The DCCppTurnout supports three types of feedback: 034 * <ul> 035 * <li> DIRECT: No actual feedback, uses Stationary Decoder command and 036 * fakes the response. 037 * </li> 038 * <li> MONITORING: Uses the Turnout command, lets the Base Station 039 * fake the response :) 040 * </li> 041 * <li> EXACT: Uses the Output command to directly address an Arduino pin. 042 * </li> 043 * </ul> 044 * 045 * It also supports "NO FEEDBACK" by treating it like "DIRECT". 046 * 047 * Turnout operation on DCC-EX based systems goes through the following 048 * sequence: 049 * <ul> 050 * <li> set the commanded state, and, Send request to command station to start 051 * sending DCC operations packet to track</li> 052 * </ul> 053 * 054 * @author Bob Jacobsen Copyright (C) 2001 055 * @author Paul Bender Copyright (C) 2003-2010 056 * @author Mark Underwood Copyright (C) 2015 057 * 058 * Based on lenz.XNetTurnout by Bob Jacobsen and Paul Bender 059 */ 060public class DCCppTurnout extends AbstractTurnout implements DCCppListener { 061 062 /* State information */ 063 protected static final int COMMANDSENT = 2; 064 protected static final int STATUSREQUESTSENT = 4; 065 protected static final int IDLE = 0; 066 protected int internalState = IDLE; 067 068 /* Static arrays to hold DCC-EX specific feedback mode information */ 069 static String[] modeNames = null; 070 static int[] modeValues = null; 071 072 //@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC") 073 //protected int _mThrown = jmri.Turnout.THROWN; 074 //@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC") 075 //protected int _mClosed = jmri.Turnout.CLOSED; 076 077 protected String _prefix = "D"; // default 078 protected DCCppTrafficController tc = null; 079 080 public DCCppTurnout(String prefix, int pNumber, DCCppTrafficController controller) { // a human-readable turnout number must be specified! 081 super(prefix + "T" + pNumber); 082 tc = controller; 083 _prefix = prefix; 084 mNumber = pNumber; // this is the address. 085 086 /* Add additional feedback types information */ 087 // Note DIRECT, ONESENSOR and TWOSENSOR are already OR'ed in. 088 _validFeedbackTypes |= MONITORING; // uses the Turnout command <T...> 089 _validFeedbackTypes |= EXACT; // uses the Output command <Z...> 090 _validFeedbackTypes |= CS_VPIN; // uses the pin control command <z...> 091 092 // Default feedback mode is DIRECT 093 _activeFeedbackType = DIRECT; 094 095 setModeInformation(_validFeedbackNames, _validFeedbackModes); 096 097 // set the mode names and values based on the static values. 098 _validFeedbackNames = getModeNames(); 099 _validFeedbackModes = getModeValues(); 100 101 // Register to get property change information from the superclass 102 _stateListener = new DCCppTurnoutStateListener(this); 103 this.addPropertyChangeListener(_stateListener); 104 // Finally, request the current state from the layout. 105 tc.getTurnoutReplyCache().requestCachedStateFromLayout(this); 106 } 107 108 //Set the mode information for DCC-EX Turnouts. 109 synchronized private static void setModeInformation(String[] feedbackNames, int[] feedbackModes) { 110 // if it hasn't been done already, create static arrays to hold 111 // the DCC-EX specific feedback information. 112 if (modeNames == null) { 113 if (feedbackNames.length != feedbackModes.length) { 114 log.error("int and string feedback arrays different length"); 115 } 116 // NOTE: What we are doing here is tacking extra modes to the list 117 // *beyond* the defaults of DIRECT, ONESENSOR and TWOSENSOR 118 modeNames = new String[feedbackNames.length + 3]; 119 modeValues = new int[feedbackNames.length + 3]; 120 for (int i = 0; i < feedbackNames.length; i++) { 121 modeNames[i] = feedbackNames[i]; 122 modeValues[i] = feedbackModes[i]; 123 } 124 modeNames[feedbackNames.length] = "BSTURNOUT"; 125 modeValues[feedbackNames.length] = MONITORING; 126 modeNames[feedbackNames.length+1] = "BSOUTPUT"; 127 modeValues[feedbackNames.length+1] = EXACT; 128 modeNames[feedbackNames.length+2] = "CS VPIN"; 129 modeValues[feedbackNames.length+2] = CS_VPIN; 130 } 131 } 132 133 static int[] getModeValues() { 134 return modeValues; 135 } 136 137 static String[] getModeNames() { 138 return modeNames; 139 } 140 141 public int getNumber() { 142 return mNumber; 143 } 144 145 /** 146 * Set the Commanded State. 147 * This method overides {@link jmri.implementation.AbstractTurnout#setCommandedState(int)}. 148 */ 149 @Override 150 public void setCommandedState(int s) { 151 log.debug("set commanded state for turnout {} to {}", getSystemName(), s); 152 153 synchronized (this) { 154 newCommandedState(s); 155 } 156 myOperator = getTurnoutOperator(); // MUST set myOperator before starting the thread 157 if (myOperator == null) { 158 forwardCommandChangeToLayout(s); 159 // Only set the known state to inconsistent if we actually expect a response 160 // from the Base Station 161 if (_activeFeedbackType == EXACT || _activeFeedbackType == MONITORING) { 162 synchronized (this) { 163 newKnownState(INCONSISTENT); 164 } 165 } else if (_activeFeedbackType == DIRECT || _activeFeedbackType == CS_VPIN) { 166 // CS_VPIN: no guaranteed reply from <z>; update optimistically. 167 // An EXRAIL-broadcast <Y> reply may also update KnownState later. 168 synchronized (this) { 169 newKnownState(s); 170 } 171 } 172 } else { 173 myOperator.start(); 174 } 175 } 176 177 /** 178 * {@inheritDoc} 179 * Sends a DCC-EX command. 180 */ 181 @Override 182 synchronized protected void forwardCommandChangeToLayout(int s) { 183 DCCppMessage msg; 184 if (s != CLOSED && s != THROWN) { 185 log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s); 186 return; 187 } 188 // newState = TRUE if s == THROWN ... 189 // ... unless we are inverted, then newState = TRUE if s == CLOSED 190 boolean newState = (s == THROWN); 191 if (getInverted()) { 192 newState = !newState; 193 } 194 switch (_activeFeedbackType) { 195 case EXACT: // Use <Z ... > command 196 // mNumber is the index ID into the Base Station's internal table of outputs. 197 // Convert the integer Turnout value to boolean for DCC-EX internal code. 198 // Assume if it's not THROWN (true), it must be CLOSED (false). 199 // Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED 200 // As defined in DCC-EX Base Station SerialCommand.cpp, so newstate 201 // is inverted when making the message 202 msg = DCCppMessage.makeOutputCmdMsg(mNumber, !newState); 203 internalState = COMMANDSENT; 204 break; 205 case CS_VPIN: // Use <z vpin> / <z -vpin> pin control command 206 // mNumber is the DCC-EX vpin number; no pre-definition required. 207 // Polarity matches EXACT: LOW = THROWN, HIGH = CLOSED — invert newState. 208 msg = DCCppMessage.makeOutputCmdMsgLC(mNumber, !newState); 209 internalState = IDLE; // no guaranteed reply 210 break; 211 case MONITORING: // Use <T ... > command 212 // mNumber is the index ID into the Base Station's internal table of Turnouts. 213 // Convert the integer Turnout value to boolean for DCC-EX internal code. 214 // Assume if it's not THROWN (true), it must be CLOSED (false). 215 msg = DCCppMessage.makeTurnoutCommandMsg(mNumber, newState); 216 internalState = COMMANDSENT; 217 break; 218 default: // DIRECT -- use <a ... > command 219 // mNumber is the DCC address of the device. 220 // Convert the integer Turnout value to boolean for DCC-EX internal code. 221 // Assume if it's not THROWN (true), it must be CLOSED (false). 222 msg = DCCppMessage.makeAccessoryDecoderMsg(mNumber, newState); 223 internalState = IDLE; 224 break; 225 } 226 log.debug("Sending Message: '{}'", msg); 227 tc.sendDCCppMessage(msg, null); // status returned via manager 228 } 229 230 @Override 231 protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) { 232 log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber); 233 } 234 235 /** 236 * request an update on status by sending a DCC-EX message 237 */ 238 @Override 239 public void requestUpdateFromLayout() { 240 // This will handle query for ONESENSOR and TWOSENSOR feedback modes. 241 super.requestUpdateFromLayout(); 242 // (02/2017) Yes it does... using the <s> command or possibly 243 // some others. TODO: Plumb this in... IFF it is needed. 244 /* 245 // DCCppMessage msg = DCCppMessage.getFeedbackRequestMsg(mNumber, 246 // ((mNumber - 1) % 4) < 2); 247 // synchronized (this) { 248 // internalState = STATUSREQUESTSENT; 249 // } 250 // tc.sendDCCppMessage(msg, null); //status is returned via the manager. 251 */ 252 253 } 254 255 @Override 256 public boolean canInvert() { 257 return true; 258 } 259 260 /** 261 * initmessage is a package proteceted class which allows the Manger to send 262 * a feedback message at initialization without changing the state of the 263 * turnout with respect to whether or not a feedback request was sent. This 264 * is used only when the turnout is created by on layout feedback. 265 * 266 * @param l Init message 267 */ 268 synchronized void initmessage(DCCppReply l) { 269 int oldState = internalState; 270 message(l); 271 internalState = oldState; 272 } 273 274 /* 275 * Handle an incoming message from the DCC-EX 276 */ 277 @Override 278 synchronized public void message(DCCppReply l) { 279 //if this is a turnout definition message, copy the defining properties from message to turnout 280 if (l.isTurnoutDefDCCReply() || l.isTurnoutDefServoReply() || l.isTurnoutDefVpinReply() || l.isTurnoutDefLCNReply() ) { 281 l.getProperties().forEach((key, value) -> { 282 this.setProperty(key, value); //copy the properties 283 }); 284 } 285 286 switch (getFeedbackMode()) { 287 case EXACT: 288 handleExactModeFeedback(l); 289 break; 290 case MONITORING: 291 handleMonitoringModeFeedback(l); 292 break; 293 case DIRECT: 294 default: 295 // Default is direct mode - we should never get here, actually. 296 } 297 } 298 299 // Listen for the outgoing messages (to the command station) 300 @Override 301 public void message(DCCppMessage l) { 302 } 303 304 // Handle a timeout notification 305 @Override 306 public void notifyTimeout(DCCppMessage msg) { 307 log.debug("Notified of timeout on message '{}'", msg); 308 } 309 310 /* 311 * With Monitoring Mode feedback, if we see a feedback message, we 312 * interpret that message and use it to display our feedback. 313 * <p> 314 * After we send a request to operate a turnout, We ask the command 315 * station to stop sending information to the stationary decoder 316 * when the either a feedback message or an "OK" message is received. 317 * 318 * @param l a {@link DCCppReply} message 319 */ 320 synchronized private void handleMonitoringModeFeedback(DCCppReply l) { 321 log.debug("Handle Message for turnout {} in MONITORING feedback mode", mNumber); 322 if (l.isTurnoutReply() && (l.getTOIDInt() == mNumber)) { 323 if (l.getTOIsThrown()) { 324 log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False")); 325 synchronized (this) { 326 newCommandedState(getInverted() ? CLOSED : THROWN); 327 newKnownState(getCommandedState()); 328 } 329 } else if (l.getTOIsClosed()) { 330 log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False")); 331 synchronized (this) { 332 newCommandedState(getInverted() ? THROWN : CLOSED); 333 newKnownState(getCommandedState()); 334 } 335 } 336 internalState = IDLE; 337 } 338 return; 339 } 340 341 synchronized private void handleExactModeFeedback(DCCppReply l) { 342 /* 343 Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED 344 As defined in DCC-EX Base Station SerialCommand.cpp 345 */ 346 log.debug("Handle Message for turnout {} in EXACT feedback mode", mNumber); 347 if (l.isOutputCmdReply() && (l.getOutputNumInt() == mNumber)) { 348 if (l.getOutputIsLow()) { 349 log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False")); 350 synchronized (this) { 351 newCommandedState(getInverted() ? CLOSED : THROWN); 352 newKnownState(getCommandedState()); 353 } 354 } else if (l.getOutputIsHigh()) { 355 log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False")); 356 synchronized (this) { 357 newCommandedState(getInverted() ? THROWN : CLOSED); 358 newKnownState(getCommandedState()); 359 } 360 } 361 internalState = IDLE; 362 } 363 return; 364 } 365 366 @Override 367 public void dispose() { 368 this.removePropertyChangeListener(_stateListener); 369 super.dispose(); 370 } 371 372 // Internal class to use for listening to state changes 373 private static class DCCppTurnoutStateListener implements java.beans.PropertyChangeListener { 374 375 DCCppTurnout _turnout = null; 376 377 DCCppTurnoutStateListener(DCCppTurnout turnout) { 378 _turnout = turnout; 379 } 380 381 /* 382 * If we're not using DIRECT feedback mode, we need to listen for 383 * state changes to know when to send an OFF message after we set the 384 * known state 385 * If we're using DIRECT mode, all of this is handled from the 386 * outgoing Messages 387 */ 388 @Override 389 public void propertyChange(java.beans.PropertyChangeEvent event) { 390 log.debug("propertyChange called"); 391 // If we're using DIRECT feedback mode, we don't care what we see here 392 if (_turnout.getFeedbackMode() != DIRECT) { 393 if (log.isDebugEnabled()) { 394 log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue()); 395 } 396 if (event.getPropertyName().equals("KnownState")) { 397 // Check to see if this is a change in the status 398 // triggered by a device on the layout, or a change in 399 // status we triggered. 400 int oldKnownState = ((Integer) event.getOldValue()).intValue(); 401 int curKnownState = ((Integer) event.getNewValue()).intValue(); 402 log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState); 403 if (curKnownState != INCONSISTENT 404 && _turnout.getCommandedState() == oldKnownState) { 405 // This was triggered by feedback on the layout, change 406 // the commanded state to reflect the new Known State 407 if (log.isDebugEnabled()) { 408 log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState()); 409 } 410 _turnout.newCommandedState(curKnownState); 411 } else { 412 // Since we always set the KnownState to 413 // INCONSISTENT when we send a command, If the old 414 // known state is INCONSISTENT, we just want to send 415 // an off message 416 if (oldKnownState == INCONSISTENT) { 417 if (log.isDebugEnabled()) { 418 log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState()); 419 } 420 } 421 } 422 } 423 } 424 } 425 426 } 427 428 // data members 429 protected int mNumber; // turnout number 430 DCCppTurnoutStateListener _stateListener; // Internal class object 431 432 private static final Logger log = LoggerFactory.getLogger(DCCppTurnout.class); 433 434}