001package jmri.jmrix.nce; 002 003import org.slf4j.Logger; 004import org.slf4j.LoggerFactory; 005 006import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 007import jmri.Turnout; 008 009/** 010 * Polls NCE Command Station for turnout discrepancies 011 * <p> 012 * This implementation reads the NCE Command Station (CS) memory that stores the 013 * state of all accessories thrown by cabs or through the com port using the new 014 * binary switch command. The accessory states are stored in 256 byte array 015 * starting at address 0xEC00 (PH5 0x5400). 016 * <p> 017 * byte 0, bit 0 = ACCY 1, bit 1 = ACCY 2 byte 1, bit 0 = ACCY 9, bit 1 = ACCY 018 * 10 019 * <p> 020 * byte 255, bit 0 = ACCY 2041, bit 3 = ACCY 2044 (last valid addr) 021 * <p> 022 * ACCY bit = 0 turnout thrown, 1 = turnout closed 023 * <p> 024 * Block reads (16 bytes) of the NCE CS memory are performed to minimize impact 025 * to the NCE CS. Data from the CS is then compared to the JMRI turnout 026 * (accessory) state and if a discrepancy is discovered, the JMRI turnout state 027 * is modified to match the CS. 028 * 029 * @author Daniel Boudreau (C) 2007 030 * @author Ken Cameron Copyright (C) 2023 031 */ 032public class NceTurnoutMonitor implements NceListener, java.beans.PropertyChangeListener { 033 034 // scope constants 035 private static final int NUM_BLOCK = 16; // maximum number of memory blocks 036 private static final int BLOCK_LEN = 16; // number of bytes in a block 037 private static final int REPLY_LEN = BLOCK_LEN; // number of bytes read 038 private static final int NCE_ACCY_THROWN = 0; // NCE internal accessory "REV" 039 private static final int NCE_ACCY_CLOSED = 1; // NCE internal accessory "NORM" 040 static final int POLL_TIME = 200; // Poll NCE memory every 200 msec plus xmt time (~70 msec) 041 042 // object state 043 private int currentBlock; // used as state in scan over active blocks 044 private int numTurnouts = 0; // number of NT turnouts known by NceTurnoutMonitor 045 private int numActiveBlocks = 0; 046 private boolean feedbackChange = false; // true if feedback for a turnout has changed 047 048 // cached work fields 049 boolean[] newTurnouts = new boolean[NUM_BLOCK]; // used to sync poll turnout memory 050 boolean[] activeBlock = new boolean[NUM_BLOCK]; // When true there are active turnouts in the memory block 051 boolean[] validBlock = new boolean[NUM_BLOCK]; // When true received block from CS 052 byte[] csAccMemCopy = new byte[NUM_BLOCK * BLOCK_LEN]; // Copy of NCE CS accessory memory 053 byte[] dataBuffer = new byte[NUM_BLOCK * BLOCK_LEN]; // place to store reply messages 054 055 private boolean recData = false; // when true, valid receive data 056 057 Thread nceTurnoutMonitorThread; 058 boolean turnoutUpdateValid = true; // keep the thread running 059 private boolean sentWarnMessage = false; // used to report about early 2007 EPROM problem 060 061 // debug final 062 private NceTrafficController tc = null; 063 064 public NceTurnoutMonitor(NceTrafficController t) { 065 super(); 066 this.tc = t; 067 } 068 069 private long lastPollTime = 0; 070 071 public NceMessage pollMessage() { 072 073 if (tc.getCommandOptions() < NceTrafficController.OPTION_2006) { 074 return null; //Only 2007 CS EPROMs support polling 075 } 076 if (tc.getUsbSystem() != NceTrafficController.USB_SYSTEM_NONE) { 077 return null; //Can't poll USB! 078 } 079 if (NceTurnout.getNumNtTurnouts() == 0) { 080 return null; //No work! 081 } 082 long currentTime = java.util.Calendar.getInstance().getTimeInMillis(); 083 if (currentTime - lastPollTime < 2 * POLL_TIME) { 084 return null; 085 } else { 086 lastPollTime = currentTime; 087 } 088 089 // User can change a turnout's feedback to MONITORING, therefore we need to rescan 090 // also see if the number of turnouts now differs from the last scan 091 if (feedbackChange || numTurnouts != NceTurnout.getNumNtTurnouts()) { 092 feedbackChange = false; 093 numTurnouts = NceTurnout.getNumNtTurnouts(); 094 095 // Determine what turnouts have been defined and what blocks have active turnouts 096 for (int block = 0; block < NUM_BLOCK; block++) { 097 098 newTurnouts[block] = true; // Block may be active, but new turnouts may have been loaded 099 if (activeBlock[block] == false) { // no need to scan once known to be active 100 101 for (int i = 0; i < 128; i++) { // Check 128 turnouts per block 102 int NTnum = 1 + i + (block * 128); 103 Turnout mControlTurnout = tc.getAdapterMemo().getTurnoutManager() 104 .getBySystemName(tc.getAdapterMemo().getSystemPrefix() + "T" + NTnum); 105 if (mControlTurnout != null) { 106 // remove listener in case we're already listening 107 mControlTurnout.removePropertyChangeListener(this); 108 109 if (mControlTurnout.getFeedbackMode() == Turnout.MONITORING) { 110 activeBlock[block] = true; // turnout found, block is active forever 111 numActiveBlocks++; 112 break; // don't check rest of block 113 } else { 114 // turnout feedback isn't monitoring, but listen in case it changes 115 mControlTurnout.addPropertyChangeListener(this); 116 log.trace("add turnout to listener NT{} Feed back mode: {}", NTnum, 117 mControlTurnout.getFeedbackMode()); 118 } 119 } 120 } 121 } 122 123 } 124 } 125 126 // See if there's any poll messages needed 127 if (numActiveBlocks <= 0) { 128 return null; // to avoid immediate infinite loop 129 } 130 131 // Set up a separate thread to notify state changes in turnouts 132 // This protects pollMessage (xmt) and reply threads if there's lockup! 133 if (nceTurnoutMonitorThread == null) { 134 nceTurnoutMonitorThread = new Thread(new Runnable() { 135 @Override 136 public void run() { 137 turnoutUpdate(); 138 } 139 }); 140 nceTurnoutMonitorThread.setName("NCE Turnout Monitor"); 141 nceTurnoutMonitorThread.setPriority(Thread.MIN_PRIORITY); 142 nceTurnoutMonitorThread.start(); 143 } 144 145 // now try to build a poll message if there are any defined turnouts to scan 146 while (true) { // will break out when next block to poll is found 147 currentBlock++; 148 if (currentBlock >= NUM_BLOCK) { 149 currentBlock = 0; 150 } 151 152 if (activeBlock[currentBlock]) { 153 log.trace("found turnouts block {}", currentBlock); 154 155 // Read NCE CS memory 156 int nceAccAddress = tc.csm.getAccyMemAddr() + currentBlock * BLOCK_LEN; 157 byte[] bl = NceBinaryCommand.accMemoryRead(nceAccAddress); 158 NceMessage m = NceMessage.createBinaryMessage(tc, bl, REPLY_LEN); 159 return m; 160 } 161 } 162 } 163 164 @Override 165 public void message(NceMessage m) { 166 if (log.isDebugEnabled()) { 167 log.debug("unexpected message"); 168 } 169 } 170 171 @SuppressFBWarnings(value = "NN_NAKED_NOTIFY") // notify not naked, command station is shared state 172 @Override 173 public void reply(NceReply r) { 174 if (r.getNumDataElements() == REPLY_LEN) { 175 176 log.trace("memory poll reply received for memory block {}: {}", currentBlock, r); 177 // Copy receive data into buffer and process later 178 for (int i = 0; i < REPLY_LEN; i++) { 179 dataBuffer[i + currentBlock * BLOCK_LEN] = (byte) r.getElement(i); 180 } 181 validBlock[currentBlock] = true; 182 recData = true; 183 //wake up turnout monitor thread 184 synchronized (this) { 185 notify(); 186 } 187 } else { 188 log.warn("wrong number of read bytes for memory poll"); 189 } 190 } 191 192 // Thread to process turnout changes, protects receive and xmt threads 193 // there are two loops, one to update turnout CommandedState 194 // and the second to update turnout KnownState 195 private void turnoutUpdate() { 196 while (turnoutUpdateValid) { 197 // if nothing to do, sleep 198 if (!recData) { 199 synchronized (this) { 200 try { 201 wait(POLL_TIME * 5); 202 } catch (InterruptedException e) { 203 Thread.currentThread().interrupt(); // retain if needed later 204 } 205 } 206 // process rcv buffer and update turnouts 207 } else { 208 recData = false; 209 // scan all valid replys from CS 210 for (int block = 0; block < NUM_BLOCK; block++) { 211 if (validBlock[block]) { 212 // Compare NCE CS memory to local copy, change commanded state if 213 // necessary 128 turnouts checked per NCE CS memory read (block) 214 for (int byteIndex = 0; byteIndex < REPLY_LEN; byteIndex++) { 215 // CS memory byte 216 byte recMemByte = dataBuffer[byteIndex + block * BLOCK_LEN]; 217 if (recMemByte != csAccMemCopy[byteIndex + block * BLOCK_LEN] || 218 newTurnouts[block] == true) { 219 220 // search this byte for active turnouts 221 for (int i = 0; i < 8; i++) { 222 int NTnum = 1 + i + byteIndex * 8 + (block * 128); 223 224 // Nasty bug in March 2007 EPROM, accessory 225 // bit 3 is shared by two accessories and 7 226 // MSB isn't used and the bit map is skewed 227 // by one bit, ie accy num 2 is in bit 0, 228 // should have been in bit 1. 229 if (tc.isNceEpromMarch2007() && !tc.isSimulatorRunning()) { 230 // bit 3 is shared by two accessories!!!! 231 if (i == 3) { 232 monitorActionCommanded(NTnum - 3, 233 recMemByte, i); 234 } 235 236 NTnum++; // skew fix 237 if (i == 7) { 238 break; // bit 7 is not used!!! 239 } 240 } 241 monitorActionCommanded(NTnum, recMemByte, i); 242 } 243 } 244 } 245 // this wait is used to add some animation to the panel displays 246 // it does not slow down the rate that this thread can process 247 // turnout changes, it only delays the response by the POLL_TIME 248 synchronized (this) { 249 try { 250 wait(POLL_TIME); 251 } catch (InterruptedException e) { 252 } 253 } 254 // now process again but for turnout KnownState 255 for (int byteIndex = 0; byteIndex < REPLY_LEN; byteIndex++) { 256 // CS memory byte 257 byte recMemByte = dataBuffer[byteIndex + block * BLOCK_LEN]; 258 if (recMemByte != csAccMemCopy[byteIndex + block * BLOCK_LEN] || 259 newTurnouts[block] == true) { 260 261 // load copy into local memory 262 csAccMemCopy[byteIndex + block * BLOCK_LEN] = recMemByte; 263 264 // search this byte for active turnouts 265 for (int i = 0; i < 8; i++) { 266 int NTnum = 1 + i + byteIndex * 8 + (block * 128); 267 268 // Nasty bug in March 2007 EPROM, accessory 269 // bit 3 is shared by two accessories and 7 270 // MSB isn't used and the bit map is skewed 271 // by one bit, ie accy num 2 is in bit 0, 272 // should have been in bit 1. 273 if (tc.isNceEpromMarch2007() && !tc.isSimulatorRunning()) { 274 if (!sentWarnMessage) { 275 log.warn( 276 "The installed NCE Command Station EPROM has problems when using turnout MONITORING feedback"); 277 sentWarnMessage = true; 278 } 279 // bit 3 is shared by two accessories!!!! 280 if (i == 3) { 281 monitorActionKnown(NTnum - 3, 282 recMemByte, i); 283 } 284 285 NTnum++; // skew fix 286 if (i == 7) { 287 break; // bit 7 is not used!!! 288 } 289 } 290 monitorActionKnown(NTnum, recMemByte, i); 291 } 292 } 293 } 294 newTurnouts[block] = false; 295 } 296 } 297 } 298 } 299 } 300 301 // update turnout's CommandedState if necessary 302 private void monitorActionCommanded(int NTnum, int recMemByte, int bit) { 303 304 NceTurnout rControlTurnout = (NceTurnout) tc.getAdapterMemo().getTurnoutManager() 305 .getBySystemName(tc.getAdapterMemo().getSystemPrefix() + "T" + NTnum); 306 if (rControlTurnout == null) { 307 log.debug("Nce turnout number: {} system prefix: {} doesn't exist", NTnum, 308 tc.getAdapterMemo().getSystemPrefix()); 309 return; 310 } 311 312 int tCommandedState = rControlTurnout.getCommandedState(); 313 314 // don't update commanded state if turnout locked unless the turnout state is unknown 315 if (rControlTurnout.getLocked(Turnout.CABLOCKOUT) && tCommandedState != Turnout.UNKNOWN) { 316 return; 317 } 318 319 int nceAccyThrown = NCE_ACCY_THROWN; 320 int nceAccyClosed = NCE_ACCY_CLOSED; 321 if (rControlTurnout.getInverted()) { 322 nceAccyThrown = NCE_ACCY_CLOSED; 323 nceAccyClosed = NCE_ACCY_THROWN; 324 } 325 326 log.trace("turnout exists NT{} state: {} Feed back mode: {}", NTnum, tCommandedState, 327 rControlTurnout.getFeedbackMode()); 328 329 // Show the byte read from NCE CS 330 log.trace("memory byte: {}", Integer.toHexString(recMemByte & 0xFF)); 331 332 // test for closed or thrown, normally 0 = closed, 1 = thrown 333 int nceAccState = (recMemByte >> bit) & 0x01; 334 if (nceAccState == nceAccyThrown && tCommandedState != Turnout.THROWN) { 335 336 log.debug("turnout discrepancy, NT{} CommandedState is now THROWN", NTnum); 337 338 // change JMRI's knowledge of the turnout state to match observed 339 rControlTurnout.setCommandedStateFromCS(Turnout.THROWN); 340 } 341 342 if (nceAccState == nceAccyClosed && tCommandedState != Turnout.CLOSED) { 343 344 log.debug("turnout discrepancy, NT{} CommandedState is now CLOSED", NTnum); 345 346 // change JMRI's knowledge of the turnout state to match observed 347 rControlTurnout.setCommandedStateFromCS(Turnout.CLOSED); 348 } 349 } 350 351 // update turnout's KnownState if necessary 352 private void monitorActionKnown(int NTnum, int recMemByte, int bit) { 353 354 NceTurnout rControlTurnout = (NceTurnout) tc.getAdapterMemo().getTurnoutManager() 355 .getBySystemName(tc.getAdapterMemo().getSystemPrefix() + "T" + NTnum); 356 357 if (rControlTurnout == null) { 358 return; 359 } 360 361 int tKnownState = rControlTurnout.getKnownState(); 362 int tCommandedState = rControlTurnout.getCommandedState(); 363 364 int nceAccyThrown = NCE_ACCY_THROWN; 365 int nceAccyClosed = NCE_ACCY_CLOSED; 366 if (rControlTurnout.getInverted()) { 367 nceAccyThrown = NCE_ACCY_CLOSED; 368 nceAccyClosed = NCE_ACCY_THROWN; 369 } 370 371 log.trace("turnout exists NT{} state: {} Feed back mode: {}", NTnum, tKnownState, 372 rControlTurnout.getFeedbackMode()); 373 374 // Show the byte read from NCE CS 375 log.trace("memory byte: {}", Integer.toHexString(recMemByte & 0xFF)); 376 377 // test for closed or thrown, normally 0 = closed, 1 = thrown 378 int nceAccState = (recMemByte >> bit) & 0x01; 379 if (nceAccState == nceAccyThrown && tKnownState != Turnout.THROWN) { 380 381 if (rControlTurnout.getLocked(Turnout.CABLOCKOUT) && tCommandedState == Turnout.CLOSED) { 382 383 log.debug("Turnout NT{} is locked, will negate THROW turnout command from layout", NTnum); 384 rControlTurnout.forwardCommandChangeToLayout(Turnout.CLOSED); 385 386 if (rControlTurnout.getReportLocked()) { 387 log.info("Turnout NT{} is locked, JMRI has canceled THROW turnout command from cab", NTnum); 388 } 389 390 } else { 391 392 log.debug("turnout discrepancy, NT{} KnownState is now THROWN", NTnum); 393 // change JMRI's knowledge of the turnout state to match observed 394 rControlTurnout.setKnownStateFromCS(Turnout.THROWN); 395 } 396 } 397 398 if (nceAccState == nceAccyClosed && tKnownState != Turnout.CLOSED) { 399 400 if (rControlTurnout.getLocked(Turnout.CABLOCKOUT) && tCommandedState == Turnout.THROWN) { 401 402 log.debug("Turnout NT{} is locked, will negate CLOSE turnout command from layout", NTnum); 403 rControlTurnout.forwardCommandChangeToLayout(Turnout.THROWN); 404 405 if (rControlTurnout.getReportLocked()) { 406 log.info("Turnout NT{} is locked, JMRI has canceled CLOSE turnout command from cab", NTnum); 407 } 408 409 } else { 410 411 log.debug("turnout discrepancy, NT{} KnownState is now CLOSED", NTnum); 412 // change JMRI's knowledge of the turnout state to match observed 413 rControlTurnout.setKnownStateFromCS(Turnout.CLOSED); 414 } 415 } 416 } 417 418 @Override 419 public void propertyChange(java.beans.PropertyChangeEvent e) { 420 if (e.getPropertyName().equals("feedbackchange")) { 421 if (((Integer) e.getNewValue()) == Turnout.MONITORING) { 422 feedbackChange = true; 423 } 424 } 425 } 426 427 private final static Logger log = LoggerFactory.getLogger(NceTurnoutMonitor.class); 428 429}