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 forwardCommandChangeToLayout(s); 157 // Only set the known state to inconsistent if we actually expect a response 158 // from the Base Station 159 if (_activeFeedbackType == EXACT || _activeFeedbackType == MONITORING) { 160 synchronized (this) { 161 newKnownState(INCONSISTENT); 162 } 163 } else if (_activeFeedbackType == DIRECT || _activeFeedbackType == CS_VPIN) { 164 // CS_VPIN: no guaranteed reply from <z>; update optimistically. 165 // An EXRAIL-broadcast <Y> reply may also update KnownState later. 166 synchronized (this) { 167 newKnownState(s); 168 } 169 } 170 } 171 172 /** 173 * {@inheritDoc} 174 * Sends a DCC-EX command. 175 */ 176 @Override 177 synchronized protected void forwardCommandChangeToLayout(int s) { 178 DCCppMessage msg; 179 if (s != CLOSED && s != THROWN) { 180 log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s); 181 return; 182 } 183 // newState = TRUE if s == THROWN ... 184 // ... unless we are inverted, then newState = TRUE if s == CLOSED 185 boolean newState = (s == THROWN); 186 if (getInverted()) { 187 newState = !newState; 188 } 189 switch (_activeFeedbackType) { 190 case EXACT: // Use <Z ... > command 191 // mNumber is the index ID into the Base Station's internal table of outputs. 192 // Convert the integer Turnout value to boolean for DCC-EX internal code. 193 // Assume if it's not THROWN (true), it must be CLOSED (false). 194 // Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED 195 // As defined in DCC-EX Base Station SerialCommand.cpp, so newstate 196 // is inverted when making the message 197 msg = DCCppMessage.makeOutputCmdMsg(mNumber, !newState); 198 internalState = COMMANDSENT; 199 break; 200 case CS_VPIN: // Use <z vpin> / <z -vpin> pin control command 201 // mNumber is the DCC-EX vpin number; no pre-definition required. 202 // Polarity matches EXACT: LOW = THROWN, HIGH = CLOSED — invert newState. 203 msg = DCCppMessage.makeOutputCmdMsgLC(mNumber, !newState); 204 internalState = IDLE; // no guaranteed reply 205 break; 206 case MONITORING: // Use <T ... > command 207 // mNumber is the index ID into the Base Station's internal table of Turnouts. 208 // Convert the integer Turnout value to boolean for DCC-EX internal code. 209 // Assume if it's not THROWN (true), it must be CLOSED (false). 210 msg = DCCppMessage.makeTurnoutCommandMsg(mNumber, newState); 211 internalState = COMMANDSENT; 212 break; 213 default: // DIRECT -- use <a ... > command 214 // mNumber is the DCC address of the device. 215 // Convert the integer Turnout value to boolean for DCC-EX internal code. 216 // Assume if it's not THROWN (true), it must be CLOSED (false). 217 msg = DCCppMessage.makeAccessoryDecoderMsg(mNumber, newState); 218 internalState = IDLE; 219 break; 220 } 221 log.debug("Sending Message: '{}'", msg); 222 tc.sendDCCppMessage(msg, null); // status returned via manager 223 } 224 225 @Override 226 protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) { 227 log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber); 228 } 229 230 /** 231 * request an update on status by sending a DCC-EX message 232 */ 233 @Override 234 public void requestUpdateFromLayout() { 235 // This will handle query for ONESENSOR and TWOSENSOR feedback modes. 236 super.requestUpdateFromLayout(); 237 // (02/2017) Yes it does... using the <s> command or possibly 238 // some others. TODO: Plumb this in... IFF it is needed. 239 /* 240 // DCCppMessage msg = DCCppMessage.getFeedbackRequestMsg(mNumber, 241 // ((mNumber - 1) % 4) < 2); 242 // synchronized (this) { 243 // internalState = STATUSREQUESTSENT; 244 // } 245 // tc.sendDCCppMessage(msg, null); //status is returned via the manager. 246 */ 247 248 } 249 250 @Override 251 public boolean canInvert() { 252 return true; 253 } 254 255 /** 256 * initmessage is a package proteceted class which allows the Manger to send 257 * a feedback message at initialization without changing the state of the 258 * turnout with respect to whether or not a feedback request was sent. This 259 * is used only when the turnout is created by on layout feedback. 260 * 261 * @param l Init message 262 */ 263 synchronized void initmessage(DCCppReply l) { 264 int oldState = internalState; 265 message(l); 266 internalState = oldState; 267 } 268 269 /* 270 * Handle an incoming message from the DCC-EX 271 */ 272 @Override 273 synchronized public void message(DCCppReply l) { 274 //if this is a turnout definition message, copy the defining properties from message to turnout 275 if (l.isTurnoutDefDCCReply() || l.isTurnoutDefServoReply() || l.isTurnoutDefVpinReply() || l.isTurnoutDefLCNReply() ) { 276 l.getProperties().forEach((key, value) -> { 277 this.setProperty(key, value); //copy the properties 278 }); 279 } 280 281 switch (getFeedbackMode()) { 282 case EXACT: 283 handleExactModeFeedback(l); 284 break; 285 case MONITORING: 286 handleMonitoringModeFeedback(l); 287 break; 288 case DIRECT: 289 default: 290 // Default is direct mode - we should never get here, actually. 291 } 292 } 293 294 // Listen for the outgoing messages (to the command station) 295 @Override 296 public void message(DCCppMessage l) { 297 } 298 299 // Handle a timeout notification 300 @Override 301 public void notifyTimeout(DCCppMessage msg) { 302 log.debug("Notified of timeout on message '{}'", msg); 303 } 304 305 /* 306 * With Monitoring Mode feedback, if we see a feedback message, we 307 * interpret that message and use it to display our feedback. 308 * <p> 309 * After we send a request to operate a turnout, We ask the command 310 * station to stop sending information to the stationary decoder 311 * when the either a feedback message or an "OK" message is received. 312 * 313 * @param l a {@link DCCppReply} message 314 */ 315 synchronized private void handleMonitoringModeFeedback(DCCppReply l) { 316 log.debug("Handle Message for turnout {} in MONITORING feedback mode", mNumber); 317 if (l.isTurnoutReply() && (l.getTOIDInt() == mNumber)) { 318 if (l.getTOIsThrown()) { 319 log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False")); 320 synchronized (this) { 321 newCommandedState(getInverted() ? CLOSED : THROWN); 322 newKnownState(getCommandedState()); 323 } 324 } else if (l.getTOIsClosed()) { 325 log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False")); 326 synchronized (this) { 327 newCommandedState(getInverted() ? THROWN : CLOSED); 328 newKnownState(getCommandedState()); 329 } 330 } 331 internalState = IDLE; 332 } 333 return; 334 } 335 336 synchronized private void handleExactModeFeedback(DCCppReply l) { 337 /* 338 Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED 339 As defined in DCC-EX Base Station SerialCommand.cpp 340 */ 341 log.debug("Handle Message for turnout {} in EXACT feedback mode", mNumber); 342 if (l.isOutputCmdReply() && (l.getOutputNumInt() == mNumber)) { 343 if (l.getOutputIsLow()) { 344 log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False")); 345 synchronized (this) { 346 newCommandedState(getInverted() ? CLOSED : THROWN); 347 newKnownState(getCommandedState()); 348 } 349 } else if (l.getOutputIsHigh()) { 350 log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False")); 351 synchronized (this) { 352 newCommandedState(getInverted() ? THROWN : CLOSED); 353 newKnownState(getCommandedState()); 354 } 355 } 356 internalState = IDLE; 357 } 358 return; 359 } 360 361 @Override 362 public void dispose() { 363 this.removePropertyChangeListener(_stateListener); 364 super.dispose(); 365 } 366 367 // Internal class to use for listening to state changes 368 private static class DCCppTurnoutStateListener implements java.beans.PropertyChangeListener { 369 370 DCCppTurnout _turnout = null; 371 372 DCCppTurnoutStateListener(DCCppTurnout turnout) { 373 _turnout = turnout; 374 } 375 376 /* 377 * If we're not using DIRECT feedback mode, we need to listen for 378 * state changes to know when to send an OFF message after we set the 379 * known state 380 * If we're using DIRECT mode, all of this is handled from the 381 * outgoing Messages 382 */ 383 @Override 384 public void propertyChange(java.beans.PropertyChangeEvent event) { 385 log.debug("propertyChange called"); 386 // If we're using DIRECT feedback mode, we don't care what we see here 387 if (_turnout.getFeedbackMode() != DIRECT) { 388 if (log.isDebugEnabled()) { 389 log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue()); 390 } 391 if (event.getPropertyName().equals("KnownState")) { 392 // Check to see if this is a change in the status 393 // triggered by a device on the layout, or a change in 394 // status we triggered. 395 int oldKnownState = ((Integer) event.getOldValue()).intValue(); 396 int curKnownState = ((Integer) event.getNewValue()).intValue(); 397 log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState); 398 if (curKnownState != INCONSISTENT 399 && _turnout.getCommandedState() == oldKnownState) { 400 // This was triggered by feedback on the layout, change 401 // the commanded state to reflect the new Known State 402 if (log.isDebugEnabled()) { 403 log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState()); 404 } 405 _turnout.newCommandedState(curKnownState); 406 } else { 407 // Since we always set the KnownState to 408 // INCONSISTENT when we send a command, If the old 409 // known state is INCONSISTENT, we just want to send 410 // an off message 411 if (oldKnownState == INCONSISTENT) { 412 if (log.isDebugEnabled()) { 413 log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState()); 414 } 415 } 416 } 417 } 418 } 419 } 420 421 } 422 423 // data members 424 protected int mNumber; // turnout number 425 DCCppTurnoutStateListener _stateListener; // Internal class object 426 427 private static final Logger log = LoggerFactory.getLogger(DCCppTurnout.class); 428 429}