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}