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++ 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++ 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++ 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++ 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 091 // Default feedback mode is DIRECT 092 _activeFeedbackType = DIRECT; 093 094 setModeInformation(_validFeedbackNames, _validFeedbackModes); 095 096 // set the mode names and values based on the static values. 097 _validFeedbackNames = getModeNames(); 098 _validFeedbackModes = getModeValues(); 099 100 // Register to get property change information from the superclass 101 _stateListener = new DCCppTurnoutStateListener(this); 102 this.addPropertyChangeListener(_stateListener); 103 // Finally, request the current state from the layout. 104 tc.getTurnoutReplyCache().requestCachedStateFromLayout(this); 105 } 106 107 //Set the mode information for DCC++ Turnouts. 108 synchronized static private void setModeInformation(String[] feedbackNames, int[] feedbackModes) { 109 // if it hasn't been done already, create static arrays to hold 110 // the DCC++ specific feedback information. 111 if (modeNames == null) { 112 if (feedbackNames.length != feedbackModes.length) { 113 log.error("int and string feedback arrays different length"); 114 } 115 // NOTE: What we are doing here is tacking extra modes to the list 116 // *beyond* the defaults of DIRECT, ONESENSOR and TWOSENSOR 117 modeNames = new String[feedbackNames.length + 2]; 118 modeValues = new int[feedbackNames.length + 2]; 119 for (int i = 0; i < feedbackNames.length; i++) { 120 modeNames[i] = feedbackNames[i]; 121 modeValues[i] = feedbackModes[i]; 122 } 123 modeNames[feedbackNames.length] = "BSTURNOUT"; 124 modeValues[feedbackNames.length] = MONITORING; 125 modeNames[feedbackNames.length+1] = "BSOUTPUT"; 126 modeValues[feedbackNames.length+1] = EXACT; 127 } 128 } 129 130 static int[] getModeValues() { 131 return modeValues; 132 } 133 134 static String[] getModeNames() { 135 return modeNames; 136 } 137 138 public int getNumber() { 139 return mNumber; 140 } 141 142 /** 143 * Set the Commanded State. 144 * This method overides {@link jmri.implementation.AbstractTurnout#setCommandedState(int)}. 145 */ 146 @Override 147 public void setCommandedState(int s) { 148 log.debug("set commanded state for turnout {} to {}", getSystemName(), s); 149 150 synchronized (this) { 151 newCommandedState(s); 152 } 153 forwardCommandChangeToLayout(s); 154 // Only set the known state to inconsistent if we actually expect a response 155 // from the Base Station 156 if (_activeFeedbackType == EXACT || _activeFeedbackType == MONITORING) { 157 synchronized (this) { 158 newKnownState(INCONSISTENT); 159 } 160 } else if( _activeFeedbackType == DIRECT ){ 161 synchronized (this) { 162 newKnownState(s); 163 } 164 } 165 } 166 167 /** 168 * {@inheritDoc} 169 * Sends a DCC++ command. 170 */ 171 @Override 172 synchronized protected void forwardCommandChangeToLayout(int s) { 173 DCCppMessage msg; 174 if (s != CLOSED && s != THROWN) { 175 log.warn("Turnout {}: state {} not forwarded to layout.", mNumber, s); 176 return; 177 } 178 // newState = TRUE if s == THROWN ... 179 // ... unless we are inverted, then newState = TRUE if s == CLOSED 180 boolean newState = (s == THROWN); 181 if (getInverted()) { 182 newState = !newState; 183 } 184 switch (_activeFeedbackType) { 185 case EXACT: // Use <z ... > command 186 // mNumber is the index ID into the Base Station's internal table of outputs. 187 // Convert the integer Turnout value to boolean for DCC++ internal code. 188 // Assume if it's not THROWN (true), it must be CLOSED (false). 189 // Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED 190 // As defined in DCC++ Base Station SerialCommand.cpp, so newstate 191 // is inverted when making the message 192 msg = DCCppMessage.makeOutputCmdMsg(mNumber, !newState); 193 internalState = COMMANDSENT; 194 break; 195 case MONITORING: // Use <T ... > command 196 // mNumber is the index ID into the Base Station's internal table of Turnouts. 197 // Convert the integer Turnout value to boolean for DCC++ internal code. 198 // Assume if it's not THROWN (true), it must be CLOSED (false). 199 msg = DCCppMessage.makeTurnoutCommandMsg(mNumber, newState); 200 internalState = COMMANDSENT; 201 break; 202 default: // DIRECT -- use <a ... > command 203 // mNumber is the DCC address of the device. 204 // Convert the integer Turnout value to boolean for DCC++ internal code. 205 // Assume if it's not THROWN (true), it must be CLOSED (false). 206 msg = DCCppMessage.makeAccessoryDecoderMsg(mNumber, newState); 207 internalState = IDLE; 208 break; 209 } 210 log.debug("Sending Message: '{}'", msg); 211 tc.sendDCCppMessage(msg, null); // status returned via manager 212 } 213 214 @Override 215 protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) { 216 log.debug("Send command to {} Pushbutton {}T{}", (_pushButtonLockout ? "Lock" : "Unlock"), _prefix, mNumber); 217 } 218 219 /** 220 * request an update on status by sending a DCC++ message 221 */ 222 @Override 223 public void requestUpdateFromLayout() { 224 // This will handle query for ONESENSOR and TWOSENSOR feedback modes. 225 super.requestUpdateFromLayout(); 226 // (02/2017) Yes it does... using the <s> command or possibly 227 // some others. TODO: Plumb this in... IFF it is needed. 228 /* 229 // DCCppMessage msg = DCCppMessage.getFeedbackRequestMsg(mNumber, 230 // ((mNumber - 1) % 4) < 2); 231 // synchronized (this) { 232 // internalState = STATUSREQUESTSENT; 233 // } 234 // tc.sendDCCppMessage(msg, null); //status is returned via the manager. 235 */ 236 237 } 238 239 @Override 240 public boolean canInvert() { 241 return true; 242 } 243 244 /** 245 * initmessage is a package proteceted class which allows the Manger to send 246 * a feedback message at initialization without changing the state of the 247 * turnout with respect to whether or not a feedback request was sent. This 248 * is used only when the turnout is created by on layout feedback. 249 * 250 * @param l Init message 251 */ 252 synchronized void initmessage(DCCppReply l) { 253 int oldState = internalState; 254 message(l); 255 internalState = oldState; 256 } 257 258 /* 259 * Handle an incoming message from the DCC++ 260 */ 261 @Override 262 synchronized public void message(DCCppReply l) { 263 //if this is a turnout definition message, copy the defining properties from message to turnout 264 if (l.isTurnoutDefDCCReply() || l.isTurnoutDefServoReply() || l.isTurnoutDefVpinReply() || l.isTurnoutDefLCNReply() ) { 265 l.getProperties().forEach((key, value) -> { 266 this.setProperty(key, value); //copy the properties 267 }); 268 } 269 270 switch (getFeedbackMode()) { 271 case EXACT: 272 handleExactModeFeedback(l); 273 break; 274 case MONITORING: 275 handleMonitoringModeFeedback(l); 276 break; 277 case DIRECT: 278 default: 279 // Default is direct mode - we should never get here, actually. 280 } 281 } 282 283 // Listen for the outgoing messages (to the command station) 284 @Override 285 public void message(DCCppMessage l) { 286 } 287 288 // Handle a timeout notification 289 @Override 290 public void notifyTimeout(DCCppMessage msg) { 291 log.debug("Notified of timeout on message '{}'", msg); 292 } 293 294 /* 295 * With Monitoring Mode feedback, if we see a feedback message, we 296 * interpret that message and use it to display our feedback. 297 * <p> 298 * After we send a request to operate a turnout, We ask the command 299 * station to stop sending information to the stationary decoder 300 * when the either a feedback message or an "OK" message is received. 301 * 302 * @param l a {@link DCCppReply} message 303 */ 304 synchronized private void handleMonitoringModeFeedback(DCCppReply l) { 305 log.debug("Handle Message for turnout {} in MONITORING feedback mode", mNumber); 306 if (l.isTurnoutReply() && (l.getTOIDInt() == mNumber)) { 307 if (l.getTOIsThrown()) { 308 log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False")); 309 synchronized (this) { 310 newCommandedState(getInverted() ? CLOSED : THROWN); 311 newKnownState(getCommandedState()); 312 } 313 } else if (l.getTOIsClosed()) { 314 log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False")); 315 synchronized (this) { 316 newCommandedState(getInverted() ? THROWN : CLOSED); 317 newKnownState(getCommandedState()); 318 } 319 } 320 internalState = IDLE; 321 } 322 return; 323 } 324 325 synchronized private void handleExactModeFeedback(DCCppReply l) { 326 /* 327 Note for Outputs (EXACT mode), LOW is THROWN, HIGH is CLOSED 328 As defined in DCC++ Base Station SerialCommand.cpp 329 */ 330 log.debug("Handle Message for turnout {} in EXACT feedback mode", mNumber); 331 if (l.isOutputCmdReply() && (l.getOutputNumInt() == mNumber)) { 332 if (l.getOutputIsLow()) { 333 log.debug("Turnout is Thrown. Inverted = {}", (getInverted() ? "True" : "False")); 334 synchronized (this) { 335 newCommandedState(getInverted() ? CLOSED : THROWN); 336 newKnownState(getCommandedState()); 337 } 338 } else if (l.getOutputIsHigh()) { 339 log.debug("Turnout is Closed. Inverted = {}", (getInverted() ? "True" : "False")); 340 synchronized (this) { 341 newCommandedState(getInverted() ? THROWN : CLOSED); 342 newKnownState(getCommandedState()); 343 } 344 } 345 internalState = IDLE; 346 } 347 return; 348 } 349 350 @Override 351 public void dispose() { 352 this.removePropertyChangeListener(_stateListener); 353 super.dispose(); 354 } 355 356 // Internal class to use for listening to state changes 357 private static class DCCppTurnoutStateListener implements java.beans.PropertyChangeListener { 358 359 DCCppTurnout _turnout = null; 360 361 DCCppTurnoutStateListener(DCCppTurnout turnout) { 362 _turnout = turnout; 363 } 364 365 /* 366 * If we're not using DIRECT feedback mode, we need to listen for 367 * state changes to know when to send an OFF message after we set the 368 * known state 369 * If we're using DIRECT mode, all of this is handled from the 370 * outgoing Messages 371 */ 372 @Override 373 public void propertyChange(java.beans.PropertyChangeEvent event) { 374 log.debug("propertyChange called"); 375 // If we're using DIRECT feedback mode, we don't care what we see here 376 if (_turnout.getFeedbackMode() != DIRECT) { 377 if (log.isDebugEnabled()) { 378 log.debug("propertyChange Not Direct Mode property: {} old value {} new value {}", event.getPropertyName(), event.getOldValue(), event.getNewValue()); 379 } 380 if (event.getPropertyName().equals("KnownState")) { 381 // Check to see if this is a change in the status 382 // triggered by a device on the layout, or a change in 383 // status we triggered. 384 int oldKnownState = ((Integer) event.getOldValue()).intValue(); 385 int curKnownState = ((Integer) event.getNewValue()).intValue(); 386 log.debug("propertyChange KnownState - old value {} new value {}", oldKnownState, curKnownState); 387 if (curKnownState != INCONSISTENT 388 && _turnout.getCommandedState() == oldKnownState) { 389 // This was triggered by feedback on the layout, change 390 // the commanded state to reflect the new Known State 391 if (log.isDebugEnabled()) { 392 log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState()); 393 } 394 _turnout.newCommandedState(curKnownState); 395 } else { 396 // Since we always set the KnownState to 397 // INCONSISTENT when we send a command, If the old 398 // known state is INCONSISTENT, we just want to send 399 // an off message 400 if (oldKnownState == INCONSISTENT) { 401 if (log.isDebugEnabled()) { 402 log.debug("propertyChange CommandedState: {}", _turnout.getCommandedState()); 403 } 404 } 405 } 406 } 407 } 408 } 409 410 } 411 412 // data members 413 protected int mNumber; // turnout number 414 DCCppTurnoutStateListener _stateListener; // Internal class object 415 416 private final static Logger log = LoggerFactory.getLogger(DCCppTurnout.class); 417 418}