001package jmri.jmrix.nce;
002
003import java.text.DecimalFormat;
004import java.util.Date;
005
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
010import jmri.InstanceManager;
011import jmri.Timebase;
012import jmri.implementation.DefaultClockControl;
013
014/**
015 * Implementation of the Hardware Fast Clock for NCE.
016 * <p>
017 * This module is based on the LocoNet version as worked over by David Duchamp
018 * based on original work by Bob Jacobsen and Alex Shepherd. It implements the
019 * sync logic to keep the Nce clock in sync with the internal clock or keeps the
020 * internal in sync to the Nce clock. The following of the Nce clock is better
021 * than the other way around due to the fine tuning available on the internal
022 * clock while the Nce clock doesn't.
023 * <br>
024 * <hr>
025 * This file is part of JMRI.
026 * <p>
027 * JMRI is free software; you can redistribute it and/or modify it under the
028 * terms of version 2 of the GNU General Public License as published by the Free
029 * Software Foundation. See the "COPYING" file for a copy of this license.
030 * <p>
031 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
032 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
033 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
034 *
035 * @author Ken Cameron Copyright (C) 2007, 2023
036 * @author Dave Duchamp Copyright (C) 2007
037 * @author Bob Jacobsen, Alex Shepherd
038 */
039public class NceClockControl extends DefaultClockControl implements NceListener {
040
041    /**
042     * Create a ClockControl object for a NCE clock.
043     *
044     * @param tc traffic controller for connection
045     * @param prefix system connection prefix
046     */
047    public NceClockControl(NceTrafficController tc, String prefix) {
048        super();
049        this.tc = tc;
050
051        // Create a timebase listener for the Minute change events
052        internalClock = InstanceManager.getNullableDefault(jmri.Timebase.class);
053        if (internalClock == null) {
054            log.error("No Timebase Instance");
055            return;
056        }
057        minuteChangeListener = new java.beans.PropertyChangeListener() {
058            @Override
059            public void propertyChange(java.beans.PropertyChangeEvent e) {
060                newInternalMinute();
061            }
062        };
063        internalClock.addMinuteChangeListener(minuteChangeListener);
064    }
065
066    private NceTrafficController tc = null;
067
068    /* constants, variables, etc */
069    private static final boolean DEBUG_SHOW_PUBLIC_CALLS = true; // enable debug for each public interface
070    private static final boolean DEBUG_SHOW_SYNC_CALLS = false; // enable debug for sync logic
071
072    public static final int CS_CLOCK_MEM_SIZE = 0x10;
073    public static final int CS_CLOCK_SCALE = 0x00;
074    public static final int CS_CLOCK_TICK = 0x01;
075    public static final int CS_CLOCK_SECONDS = 0x02;
076    public static final int CS_CLOCK_MINUTES = 0x03;
077    public static final int CS_CLOCK_HOURS = 0x04;
078    public static final int CS_CLOCK_AMPM = 0x05;
079    public static final int CS_CLOCK_1224 = 0x06;
080    public static final int CS_CLOCK_STATUS = 0x0D;
081    public static final int CMD_CLOCK_SET_TIME_SIZE = 0x03;
082    public static final int CMD_CLOCK_SET_PARAM_SIZE = 0x02;
083    public static final int CMD_CLOCK_SET_RUN_SIZE = 0x01;
084    public static final int CMD_CLOCK_SET_REPLY_SIZE = 0x01;
085    public static final int CMD_MEM_SET_REPLY_SIZE = 0x01;
086    public static final int MAX_ERROR_ARRAY = 4;
087    public static final double TARGET_SYNC_DELAY = 55;
088    public static final int SYNCMODE_OFF = 0;    //0 - clocks independent
089    public static final int SYNCMODE_NCE_MASTER = 1;  //1 - NCE sets Internal
090    public static final int SYNCMODE_INTERNAL_MASTER = 2; //2 - Internal sets NCE
091    public static final int WAIT_CMD_EXECUTION = 1000;
092
093    DecimalFormat fiveDigits = new DecimalFormat("0.00000");
094    DecimalFormat fourDigits = new DecimalFormat("0.0000");
095    DecimalFormat threeDigits = new DecimalFormat("0.000");
096    DecimalFormat twoDigits = new DecimalFormat("0.00");
097
098    private int waiting = 0;
099    private final int clockMode = SYNCMODE_OFF;
100    private boolean waitingForCmdRead = false;
101    private boolean waitingForCmdStop = false;
102    private boolean waitingForCmdStart = false;
103    private boolean waitingForCmdRatio = false;
104    private boolean waitingForCmdTime = false;
105    private boolean waitingForCmd1224 = false;
106    private NceReply lastClockReadPacket = null;
107    //private Date lastClockReadAtTime;
108    private int nceLastHour;
109    private int nceLastMinute;
110    private int nceLastSecond;
111    private int nceLastRatio;
112    private boolean nceLastAmPm;
113    private boolean nceLast1224;
114    //private boolean nceLastRunning;
115    //private double internalLastRatio;
116    //private boolean internalLastRunning;
117    //private double syncInterval = TARGET_SYNC_DELAY;
118    //private int internalSyncInitStateCounter = 0;
119    //private int internalSyncRunStateCounter = 0;
120    private boolean issueDeferredGetTime = false;
121    //private boolean issueDeferredGetRate = false;
122    //private boolean initNeverCalledBefore = true;
123
124    private final int nceSyncInitStateCounter = 0; // NCE master sync initialzation state machine
125    private final int nceSyncRunStateCounter = 0; // NCE master sync runtime state machine
126    //private int alarmDisplayStateCounter = 0; // manages the display update from the alarm
127
128    Timebase internalClock;
129    javax.swing.Timer alarmSyncUpdate = null;
130    java.beans.PropertyChangeListener minuteChangeListener;
131
132    //  ignore replies
133    @Override
134    public void message(NceMessage m) {
135        log.error("message received: {}", m);
136    }
137
138    @Override
139    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT",
140                                                        justification="I18N of log message")
141    public void reply(NceReply r) {
142        log.trace("NceReply(len {}) waiting: {} watingForRead: {} waitingForCmdTime: {} waitingForCmd1224: {} waitingForCmdRatio: {} waitingForCmdStop: {} waitingForCmdStart: {}", r.getNumDataElements(), waiting, waitingForCmdRead, waitingForCmdTime, waitingForCmd1224, waitingForCmdRatio, waitingForCmdStop, waitingForCmdStart);
143
144        if (waiting <= 0) {
145            log.error("{}", Bundle.getMessage("LogReplyEnexpected"));
146            return;
147        }
148        waiting--;
149        if (waitingForCmdRead && r.getNumDataElements() == CS_CLOCK_MEM_SIZE) {
150            readClockPacket(r);
151            waitingForCmdRead = false;
152            return;
153        }
154        if (waitingForCmdTime) {
155            if (r.getNumDataElements() != CMD_CLOCK_SET_REPLY_SIZE) {
156                log.error("{}{}", Bundle.getMessage("LogNceClockReplySizeError"), r.getNumDataElements());
157                return;
158            } else {
159                waitingForCmdTime = false;
160                if (r.getElement(0) != NceMessage.NCE_OKAY) {
161                    log.error("NCE set clock replied: {}", r.getElement(0));
162                }
163                return;
164            }
165        }
166        if (r.getNumDataElements() != CMD_CLOCK_SET_REPLY_SIZE) {
167            log.error("{}{}", Bundle.getMessage("LogNceClockReplySizeError"), r.getNumDataElements());
168            return;
169        } else {
170            if (waitingForCmd1224) {
171                waitingForCmd1224 = false;
172                if (r.getElement(0) != NceMessage.NCE_OKAY) {
173                    log.error("{}{}", Bundle.getMessage("LogNceClock1224CmdError"), r.getElement(0));
174                }
175                return;
176            }
177            if (waitingForCmdRatio) {
178                waitingForCmdRatio = false;
179                if (r.getElement(0) != NceMessage.NCE_OKAY) {
180                    log.error("{}{}", Bundle.getMessage("LogNceClockRatioCmdError"), r.getElement(0));
181                }
182                return;
183            }
184            if (waitingForCmdStop) {
185                waitingForCmdStop = false;
186                if (r.getElement(0) != NceMessage.NCE_OKAY) {
187                    log.error("{}{}", Bundle.getMessage("LogNceClockStopCmdError"), r.getElement(0));
188                }
189                return;
190            }
191            if (waitingForCmdStart) {
192                waitingForCmdStart = false;
193                if (r.getElement(0) != NceMessage.NCE_OKAY) {
194                    log.error("waitingForCmdStart: {}{}", Bundle.getMessage("LogNceClockStartCmdError"), r.getElement(0));
195                }
196                return;
197            }
198        }
199        // unhandled reply, nothing to do about it
200        if (log.isDebugEnabled()) {
201            StringBuffer buf = new StringBuffer();
202            if (waiting > 0) {
203                buf.append("waiting: ").append(waiting);
204            }
205            if (waitingForCmdRead) {
206                buf.append("waitingForCmdRead: ").append(waitingForCmdRead);
207            }
208            if (waitingForCmdTime) {
209                buf.append("waitingForCmdTime: ").append(waitingForCmdTime);
210            }
211            if (waitingForCmd1224) {
212                buf.append("waitingForCmd1224: ").append(waitingForCmd1224);
213            }
214            if (waitingForCmdRatio) {
215                buf.append("waitingForCmdRatio: ").append(waitingForCmdRatio);
216            }
217            if (waitingForCmdStop) {
218                buf.append("waitingForCmdStop: ").append(waitingForCmdStop);
219            }
220            if (waitingForCmdStart) {
221                buf.append("waitingForCmdStart: ").append(waitingForCmdStart);
222            }
223            log.debug("NceReply(len {}) {}", r.getNumDataElements(), buf);
224            buf = new StringBuffer();
225            for (int i = 0; i < r.getNumDataElements(); i++) {
226                buf.append(" ").append(r.getElement(i));
227            }
228            log.debug("{}:{}", Bundle.getMessage("LogReplyUnexpected"), buf );
229        }
230    }
231
232    /**
233     * name of Nce clock
234     */
235    @Override
236    public String getHardwareClockName() {
237        if (DEBUG_SHOW_PUBLIC_CALLS ) {
238            log.debug("getHardwareClockName");
239        }
240        return ("Nce Fast Clock");
241    }
242
243    /**
244     * Nce clock runs stable enough
245     */
246    @Override
247    public boolean canCorrectHardwareClock() {
248        if (DEBUG_SHOW_PUBLIC_CALLS ) {
249            log.debug("getHardwareClockName");
250        }
251        return false;
252    }
253
254    /**
255     * Nce clock supports 12/24 operation
256     */
257    @Override
258    public boolean canSet12Or24HourClock() {
259        if (DEBUG_SHOW_PUBLIC_CALLS ) {
260            log.debug("canSet12Or24HourClock");
261        }
262        return true;
263    }
264
265    /**
266     * Set Nce clock speed, must be 1 to 15.
267     */
268    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT",
269        justification="I18N of log message")
270    @Override
271    public void setRate(double newRate) {
272        if (DEBUG_SHOW_PUBLIC_CALLS ) {
273            log.debug("setRate: {}", newRate);
274        }
275        int newRatio = (int) newRate;
276        if (newRatio < 1 || newRatio > 15) {
277            log.error("{}", Bundle.getMessage("LogNceClockRatioRangeError"));
278        } else {
279            issueClockRatio(newRatio);
280        }
281    }
282
283    /**
284     * NCE only supports integer rates.
285     */
286    @Override
287    public boolean requiresIntegerRate() {
288        if (DEBUG_SHOW_PUBLIC_CALLS ) {
289            log.debug("requiresIntegerRate");
290        }
291        return true;
292    }
293
294    /**
295     * Get last known ratio from Nce clock.
296     */
297    @Override
298    public double getRate() {
299        issueReadOnlyRequest(); // get the current rate
300        //issueDeferredGetRate = true;
301        if (DEBUG_SHOW_PUBLIC_CALLS ) {
302            log.debug("getRate: {}", nceLastRatio);
303        }
304        return (nceLastRatio);
305    }
306
307    /**
308     * Set the time, the date part is ignored.
309     */
310    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
311    @Override
312    public void setTime(Date now) {
313        if (DEBUG_SHOW_PUBLIC_CALLS ) {
314            log.debug("setTime: {}", now);
315        }
316        issueClockSet(now.getHours(), now.getMinutes(), now.getSeconds());
317    }
318
319    /**
320     * Get the current Nce time, does not have a date component.
321     */
322    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
323    @Override
324    public Date getTime() {
325        issueReadOnlyRequest(); // go get the current time value
326        issueDeferredGetTime = true;
327        Date now = internalClock.getTime();
328        if (lastClockReadPacket != null) {
329            if (nceLast1224) { // is 24 hour mode
330                now.setHours(nceLastHour);
331            } else {
332                if (nceLastAmPm) { // is AM
333                    now.setHours(nceLastHour);
334                } else {
335                    now.setHours(nceLastHour + 12);
336                }
337            }
338            now.setMinutes(nceLastMinute);
339            now.setSeconds(nceLastSecond);
340        }
341        if (DEBUG_SHOW_PUBLIC_CALLS ) {
342            log.debug("getTime returning: {}", now);
343        }
344        return (now);
345    }
346
347    /**
348     * Set Nce clock and start clock.
349     */
350    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
351    @Override
352    public void startHardwareClock(Date now) {
353        if (DEBUG_SHOW_PUBLIC_CALLS ) {
354            log.debug("startHardwareClock: {}", now);
355        }
356        issueClockSet(now.getHours(), now.getMinutes(), now.getSeconds());
357        issueClockStart();
358    }
359
360    /**
361     * Stop the Nce Clock.
362     */
363    @Override
364    public void stopHardwareClock() {
365        if (DEBUG_SHOW_PUBLIC_CALLS ) {
366            log.debug("stopHardwareClock");
367        }
368        issueClockStop();
369    }
370
371    /**
372     * not sure when or if this gets called, but will issue a read to get latest
373     * time
374     */
375    public void initiateRead() {
376        if (DEBUG_SHOW_PUBLIC_CALLS ) {
377            log.debug("initiateRead");
378        }
379        issueReadOnlyRequest();
380    }
381
382    /**
383     * Stop any sync, removes listeners.
384     */
385    public void dispose() {
386
387        // Remove ourselves from the timebase minute rollover event
388        if (minuteChangeListener != null) {
389            internalClock.removeMinuteChangeListener(minuteChangeListener);
390            minuteChangeListener = null;
391        }
392    }
393
394    /**
395     * Handles minute notifications for NCE Clock Monitor/Synchronizer
396     */
397    public void newInternalMinute() {
398        if (DEBUG_SHOW_SYNC_CALLS) {
399            log.debug("newInternalMinute clockMode: {} nceInit: {} nceRun: {}",
400                clockMode, nceSyncInitStateCounter, nceSyncRunStateCounter );
401        }
402    }
403
404    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
405    private void readClockPacket(NceReply r) {
406        //NceReply priorClockReadPacket = lastClockReadPacket;
407        //int priorNceRatio = nceLastRatio;
408        //boolean priorNceRunning = nceLastRunning;
409        lastClockReadPacket = r;
410        //lastClockReadAtTime = internalClock.getTime();
411        //log.debug("readClockPacket - at time: " + lastClockReadAtTime);
412        nceLastHour = r.getElement(CS_CLOCK_HOURS) & 0xFF;
413        nceLastMinute = r.getElement(CS_CLOCK_MINUTES) & 0xFF;
414        nceLastSecond = r.getElement(CS_CLOCK_SECONDS) & 0xFF;
415        if (r.getElement(CS_CLOCK_1224) == 1) {
416            nceLast1224 = true;
417        } else {
418            nceLast1224 = false;
419        }
420        if (r.getElement(CS_CLOCK_AMPM) == 'A') {
421            nceLastAmPm = true;
422        } else {
423            nceLastAmPm = false;
424        }
425        if (issueDeferredGetTime) {
426            issueDeferredGetTime = false;
427            Date now = internalClock.getTime();
428            if (nceLast1224) { // is 24 hour mode
429                now.setHours(nceLastHour);
430            } else {
431                if (nceLastAmPm) { // is AM
432                    now.setHours(nceLastHour);
433                } else {
434                    now.setHours(nceLastHour + 12);
435                }
436            }
437            now.setMinutes(nceLastMinute);
438            now.setSeconds(nceLastSecond);
439            internalClock.userSetTime(now);
440        }
441        int sc = r.getElement(CS_CLOCK_SCALE) & 0xFF;
442        if (sc > 0) {
443            nceLastRatio = 250 / sc;
444        }
445    }
446
447    private void issueClockRatio(int r) {
448        log.debug("sending ratio {} to nce cmd station", r);
449        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accSetClockRatio(r);
450        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
451        waiting++;
452        waitingForCmdRatio = true;
453        tc.sendNceMessage(cmdNce, this);
454    }
455
456    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
457    private void issueClock1224(boolean mode) {
458        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accSetClock1224(mode);
459        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
460        waiting++;
461        waitingForCmd1224 = true;
462        tc.sendNceMessage(cmdNce, this);
463    }
464
465    private void issueClockStop() {
466        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accStopClock();
467        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
468        waiting++;
469        waitingForCmdStop = true;
470        tc.sendNceMessage(cmdNce, this);
471    }
472
473    private void issueClockStart() {
474        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accStartClock();
475        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
476        waiting++;
477        waitingForCmdStart = true;
478        tc.sendNceMessage(cmdNce, this);
479    }
480
481    private void issueReadOnlyRequest() {
482        if (!waitingForCmdRead) {
483            byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accMemoryRead(tc.csm.getClockAddr());
484            NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CS_CLOCK_MEM_SIZE);
485            waiting++;
486            waitingForCmdRead = true;
487            tc.sendNceMessage(cmdNce, this);
488            //   log.debug("issueReadOnlyRequest at " + internalClock.getTime());
489        }
490    }
491
492    private void issueClockSet(int hh, int mm, int ss) {
493        issueClockSetMem(hh, mm, ss);
494    }
495
496    private void issueClockSetMem(int hh, int mm, int ss) {
497        byte[] b = new byte[3];
498        b[0] = (byte) ss;
499        b[1] = (byte) mm;
500        b[2] = (byte) hh;
501        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accMemoryWriteN(tc.csm.getClockAddr() + CS_CLOCK_SECONDS, b);
502        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_MEM_SET_REPLY_SIZE);
503        waiting++;
504        waitingForCmdTime = true;
505        tc.sendNceMessage(cmdNce, this);
506    }
507
508    @SuppressWarnings({"deprecation"}) // Date.getHours, getMinutes, getSeconds
509    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
510    private Date getNceDate() {
511        Date now = internalClock.getTime();
512        if (lastClockReadPacket != null) {
513            now.setHours(lastClockReadPacket.getElement(CS_CLOCK_HOURS));
514            now.setMinutes(lastClockReadPacket.getElement(CS_CLOCK_MINUTES));
515            now.setSeconds(lastClockReadPacket.getElement(CS_CLOCK_SECONDS));
516        }
517        return (now);
518    }
519
520    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
521    private double getNceTime() {
522        double nceTime = 0;
523        if (lastClockReadPacket != null) {
524            nceTime = (lastClockReadPacket.getElement(CS_CLOCK_HOURS) * 3600)
525                    + (lastClockReadPacket.getElement(CS_CLOCK_MINUTES) * 60)
526                    + lastClockReadPacket.getElement(CS_CLOCK_SECONDS)
527                    + (lastClockReadPacket.getElement(CS_CLOCK_TICK) * 0.25);
528        }
529        return (nceTime);
530    }
531
532    @SuppressWarnings({"deprecation"}) // Date.getHours, getMinutes, getSeconds
533    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
534    private double getIntTime() {
535        Date now = internalClock.getTime();
536        int ms = (int) (now.getTime() % 1000);
537        int ss = now.getSeconds();
538        int mm = now.getMinutes();
539        int hh = now.getHours();
540        log.trace("getIntTime: {}:{}:{}.{}", hh, mm, ss, ms);
541        return ((hh * 60 * 60) + (mm * 60) + ss + (ms / 1000));
542    }
543
544    private final static Logger log = LoggerFactory.getLogger(NceClockControl.class);
545
546}