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
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_ADDR = 0xDC00;
073    public static final int CS_CLOCK_MEM_SIZE = 0x10;
074    public static final int CS_CLOCK_SCALE = 0x00;
075    public static final int CS_CLOCK_TICK = 0x01;
076    public static final int CS_CLOCK_SECONDS = 0x02;
077    public static final int CS_CLOCK_MINUTES = 0x03;
078    public static final int CS_CLOCK_HOURS = 0x04;
079    public static final int CS_CLOCK_AMPM = 0x05;
080    public static final int CS_CLOCK_1224 = 0x06;
081    public static final int CS_CLOCK_STATUS = 0x0D;
082    public static final int CMD_CLOCK_SET_TIME_SIZE = 0x03;
083    public static final int CMD_CLOCK_SET_PARAM_SIZE = 0x02;
084    public static final int CMD_CLOCK_SET_RUN_SIZE = 0x01;
085    public static final int CMD_CLOCK_SET_REPLY_SIZE = 0x01;
086    public static final int CMD_MEM_SET_REPLY_SIZE = 0x01;
087    public static final int MAX_ERROR_ARRAY = 4;
088    public static final double TARGET_SYNC_DELAY = 55;
089    public static final int SYNCMODE_OFF = 0;    //0 - clocks independent
090    public static final int SYNCMODE_NCE_MASTER = 1;  //1 - NCE sets Internal
091    public static final int SYNCMODE_INTERNAL_MASTER = 2; //2 - Internal sets NCE
092    public static final int WAIT_CMD_EXECUTION = 1000;
093
094    DecimalFormat fiveDigits = new DecimalFormat("0.00000");
095    DecimalFormat fourDigits = new DecimalFormat("0.0000");
096    DecimalFormat threeDigits = new DecimalFormat("0.000");
097    DecimalFormat twoDigits = new DecimalFormat("0.00");
098
099    private int waiting = 0;
100    private final int clockMode = SYNCMODE_OFF;
101    private boolean waitingForCmdRead = false;
102    private boolean waitingForCmdStop = false;
103    private boolean waitingForCmdStart = false;
104    private boolean waitingForCmdRatio = false;
105    private boolean waitingForCmdTime = false;
106    private boolean waitingForCmd1224 = false;
107    private NceReply lastClockReadPacket = null;
108    //private Date lastClockReadAtTime;
109    private int nceLastHour;
110    private int nceLastMinute;
111    private int nceLastSecond;
112    private int nceLastRatio;
113    private boolean nceLastAmPm;
114    private boolean nceLast1224;
115    //private boolean nceLastRunning;
116    //private double internalLastRatio;
117    //private boolean internalLastRunning;
118    //private double syncInterval = TARGET_SYNC_DELAY;
119    //private int internalSyncInitStateCounter = 0;
120    //private int internalSyncRunStateCounter = 0;
121    private boolean issueDeferredGetTime = false;
122    //private boolean issueDeferredGetRate = false;
123    //private boolean initNeverCalledBefore = true;
124
125    private final int nceSyncInitStateCounter = 0; // NCE master sync initialzation state machine
126    private final int nceSyncRunStateCounter = 0; // NCE master sync runtime state machine
127    //private int alarmDisplayStateCounter = 0; // manages the display update from the alarm
128
129    Timebase internalClock;
130    javax.swing.Timer alarmSyncUpdate = null;
131    java.beans.PropertyChangeListener minuteChangeListener;
132
133    //  ignore replies
134    @Override
135    public void message(NceMessage m) {
136        log.error("message received: {}", m);
137    }
138
139    @Override
140    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT",
141                                                        justification="I18N of log message")
142    public void reply(NceReply r) {
143        log.trace("NceReply(len {}) waiting: {} watingForRead: {} waitingForCmdTime: {} waitingForCmd1224: {} waitingForCmdRatio: {} waitingForCmdStop: {} waitingForCmdStart: {}", r.getNumDataElements(), waiting, waitingForCmdRead, waitingForCmdTime, waitingForCmd1224, waitingForCmdRatio, waitingForCmdStop, waitingForCmdStart);
144
145        if (waiting <= 0) {
146            log.error("{}", Bundle.getMessage("LogReplyEnexpected"));
147            return;
148        }
149        waiting--;
150        if (waitingForCmdRead && r.getNumDataElements() == CS_CLOCK_MEM_SIZE) {
151            readClockPacket(r);
152            waitingForCmdRead = false;
153            return;
154        }
155        if (waitingForCmdTime) {
156            if (r.getNumDataElements() != CMD_CLOCK_SET_REPLY_SIZE) {
157                log.error("{}{}", Bundle.getMessage("LogNceClockReplySizeError"), r.getNumDataElements());
158                return;
159            } else {
160                waitingForCmdTime = false;
161                if (r.getElement(0) != NceMessage.NCE_OKAY) {
162                    log.error("NCE set clock replied: {}", r.getElement(0));
163                }
164                return;
165            }
166        }
167        if (r.getNumDataElements() != CMD_CLOCK_SET_REPLY_SIZE) {
168            log.error("{}{}", Bundle.getMessage("LogNceClockReplySizeError"), r.getNumDataElements());
169            return;
170        } else {
171            if (waitingForCmd1224) {
172                waitingForCmd1224 = false;
173                if (r.getElement(0) != NceMessage.NCE_OKAY) {
174                    log.error("{}{}", Bundle.getMessage("LogNceClock1224CmdError"), r.getElement(0));
175                }
176                return;
177            }
178            if (waitingForCmdRatio) {
179                waitingForCmdRatio = false;
180                if (r.getElement(0) != NceMessage.NCE_OKAY) {
181                    log.error("{}{}", Bundle.getMessage("LogNceClockRatioCmdError"), r.getElement(0));
182                }
183                return;
184            }
185            if (waitingForCmdStop) {
186                waitingForCmdStop = false;
187                if (r.getElement(0) != NceMessage.NCE_OKAY) {
188                    log.error("{}{}", Bundle.getMessage("LogNceClockStopCmdError"), r.getElement(0));
189                }
190                return;
191            }
192            if (waitingForCmdStart) {
193                waitingForCmdStart = false;
194                if (r.getElement(0) != NceMessage.NCE_OKAY) {
195                    log.error("waitingForCmdStart: {}{}", Bundle.getMessage("LogNceClockStartCmdError"), r.getElement(0));
196                }
197                return;
198            }
199        }
200        // unhandled reply, nothing to do about it
201        if (log.isDebugEnabled()) {
202            StringBuffer buf = new StringBuffer();
203            if (waiting > 0) {
204                buf.append("waiting: ").append(waiting);
205            }
206            if (waitingForCmdRead) {
207                buf.append("waitingForCmdRead: ").append(waitingForCmdRead);
208            }
209            if (waitingForCmdTime) {
210                buf.append("waitingForCmdTime: ").append(waitingForCmdTime);
211            }
212            if (waitingForCmd1224) {
213                buf.append("waitingForCmd1224: ").append(waitingForCmd1224);
214            }
215            if (waitingForCmdRatio) {
216                buf.append("waitingForCmdRatio: ").append(waitingForCmdRatio);
217            }
218            if (waitingForCmdStop) {
219                buf.append("waitingForCmdStop: ").append(waitingForCmdStop);
220            }
221            if (waitingForCmdStart) {
222                buf.append("waitingForCmdStart: ").append(waitingForCmdStart);
223            }
224            log.debug("NceReply(len {}) {}", r.getNumDataElements(), buf);
225            buf = new StringBuffer();
226            for (int i = 0; i < r.getNumDataElements(); i++) {
227                buf.append(" ").append(r.getElement(i));
228            }
229            log.debug("{}:{}", Bundle.getMessage("LogReplyUnexpected"), buf );
230        }
231    }
232
233    /**
234     * name of Nce clock
235     */
236    @Override
237    public String getHardwareClockName() {
238        if (DEBUG_SHOW_PUBLIC_CALLS ) {
239            log.debug("getHardwareClockName");
240        }
241        return ("Nce Fast Clock");
242    }
243
244    /**
245     * Nce clock runs stable enough
246     */
247    @Override
248    public boolean canCorrectHardwareClock() {
249        if (DEBUG_SHOW_PUBLIC_CALLS ) {
250            log.debug("getHardwareClockName");
251        }
252        return false;
253    }
254
255    /**
256     * Nce clock supports 12/24 operation
257     */
258    @Override
259    public boolean canSet12Or24HourClock() {
260        if (DEBUG_SHOW_PUBLIC_CALLS ) {
261            log.debug("canSet12Or24HourClock");
262        }
263        return true;
264    }
265
266    /**
267     * Set Nce clock speed, must be 1 to 15.
268     */
269    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT",
270        justification="I18N of log message")
271    @Override
272    public void setRate(double newRate) {
273        if (DEBUG_SHOW_PUBLIC_CALLS ) {
274            log.debug("setRate: {}", newRate);
275        }
276        int newRatio = (int) newRate;
277        if (newRatio < 1 || newRatio > 15) {
278            log.error("{}", Bundle.getMessage("LogNceClockRatioRangeError"));
279        } else {
280            issueClockRatio(newRatio);
281        }
282    }
283
284    /**
285     * NCE only supports integer rates.
286     */
287    @Override
288    public boolean requiresIntegerRate() {
289        if (DEBUG_SHOW_PUBLIC_CALLS ) {
290            log.debug("requiresIntegerRate");
291        }
292        return true;
293    }
294
295    /**
296     * Get last known ratio from Nce clock.
297     */
298    @Override
299    public double getRate() {
300        issueReadOnlyRequest(); // get the current rate
301        //issueDeferredGetRate = true;
302        if (DEBUG_SHOW_PUBLIC_CALLS ) {
303            log.debug("getRate: {}", nceLastRatio);
304        }
305        return (nceLastRatio);
306    }
307
308    /**
309     * Set the time, the date part is ignored.
310     */
311    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
312    @Override
313    public void setTime(Date now) {
314        if (DEBUG_SHOW_PUBLIC_CALLS ) {
315            log.debug("setTime: {}", now);
316        }
317        issueClockSet(now.getHours(), now.getMinutes(), now.getSeconds());
318    }
319
320    /**
321     * Get the current Nce time, does not have a date component.
322     */
323    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
324    @Override
325    public Date getTime() {
326        issueReadOnlyRequest(); // go get the current time value
327        issueDeferredGetTime = true;
328        Date now = internalClock.getTime();
329        if (lastClockReadPacket != null) {
330            if (nceLast1224) { // is 24 hour mode
331                now.setHours(nceLastHour);
332            } else {
333                if (nceLastAmPm) { // is AM
334                    now.setHours(nceLastHour);
335                } else {
336                    now.setHours(nceLastHour + 12);
337                }
338            }
339            now.setMinutes(nceLastMinute);
340            now.setSeconds(nceLastSecond);
341        }
342        if (DEBUG_SHOW_PUBLIC_CALLS ) {
343            log.debug("getTime returning: {}", now);
344        }
345        return (now);
346    }
347
348    /**
349     * Set Nce clock and start clock.
350     */
351    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
352    @Override
353    public void startHardwareClock(Date now) {
354        if (DEBUG_SHOW_PUBLIC_CALLS ) {
355            log.debug("startHardwareClock: {}", now);
356        }
357        issueClockSet(now.getHours(), now.getMinutes(), now.getSeconds());
358        issueClockStart();
359    }
360
361    /**
362     * Stop the Nce Clock.
363     */
364    @Override
365    public void stopHardwareClock() {
366        if (DEBUG_SHOW_PUBLIC_CALLS ) {
367            log.debug("stopHardwareClock");
368        }
369        issueClockStop();
370    }
371
372    /**
373     * not sure when or if this gets called, but will issue a read to get latest
374     * time
375     */
376    public void initiateRead() {
377        if (DEBUG_SHOW_PUBLIC_CALLS ) {
378            log.debug("initiateRead");
379        }
380        issueReadOnlyRequest();
381    }
382
383    /**
384     * Stop any sync, removes listeners.
385     */
386    public void dispose() {
387
388        // Remove ourselves from the timebase minute rollover event
389        if (minuteChangeListener != null) {
390            internalClock.removeMinuteChangeListener(minuteChangeListener);
391            minuteChangeListener = null;
392        }
393    }
394
395    /**
396     * Handles minute notifications for NCE Clock Monitor/Synchronizer
397     */
398    public void newInternalMinute() {
399        if (DEBUG_SHOW_SYNC_CALLS) {
400            log.debug("newInternalMinute clockMode: {} nceInit: {} nceRun: {}",
401                clockMode, nceSyncInitStateCounter, nceSyncRunStateCounter );
402        }
403    }
404
405    @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds
406    private void readClockPacket(NceReply r) {
407        //NceReply priorClockReadPacket = lastClockReadPacket;
408        //int priorNceRatio = nceLastRatio;
409        //boolean priorNceRunning = nceLastRunning;
410        lastClockReadPacket = r;
411        //lastClockReadAtTime = internalClock.getTime();
412        //log.debug("readClockPacket - at time: " + lastClockReadAtTime);
413        nceLastHour = r.getElement(CS_CLOCK_HOURS) & 0xFF;
414        nceLastMinute = r.getElement(CS_CLOCK_MINUTES) & 0xFF;
415        nceLastSecond = r.getElement(CS_CLOCK_SECONDS) & 0xFF;
416        if (r.getElement(CS_CLOCK_1224) == 1) {
417            nceLast1224 = true;
418        } else {
419            nceLast1224 = false;
420        }
421        if (r.getElement(CS_CLOCK_AMPM) == 'A') {
422            nceLastAmPm = true;
423        } else {
424            nceLastAmPm = false;
425        }
426        if (issueDeferredGetTime) {
427            issueDeferredGetTime = false;
428            Date now = internalClock.getTime();
429            if (nceLast1224) { // is 24 hour mode
430                now.setHours(nceLastHour);
431            } else {
432                if (nceLastAmPm) { // is AM
433                    now.setHours(nceLastHour);
434                } else {
435                    now.setHours(nceLastHour + 12);
436                }
437            }
438            now.setMinutes(nceLastMinute);
439            now.setSeconds(nceLastSecond);
440            internalClock.userSetTime(now);
441        }
442        int sc = r.getElement(CS_CLOCK_SCALE) & 0xFF;
443        if (sc > 0) {
444            nceLastRatio = 250 / sc;
445        }
446    }
447
448    private void issueClockRatio(int r) {
449        log.debug("sending ratio {} to nce cmd station", r);
450        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accSetClockRatio(r);
451        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
452        waiting++;
453        waitingForCmdRatio = true;
454        tc.sendNceMessage(cmdNce, this);
455    }
456
457    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
458    private void issueClock1224(boolean mode) {
459        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accSetClock1224(mode);
460        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
461        waiting++;
462        waitingForCmd1224 = true;
463        tc.sendNceMessage(cmdNce, this);
464    }
465
466    private void issueClockStop() {
467        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accStopClock();
468        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
469        waiting++;
470        waitingForCmdStop = true;
471        tc.sendNceMessage(cmdNce, this);
472    }
473
474    private void issueClockStart() {
475        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accStartClock();
476        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE);
477        waiting++;
478        waitingForCmdStart = true;
479        tc.sendNceMessage(cmdNce, this);
480    }
481
482    private void issueReadOnlyRequest() {
483        if (!waitingForCmdRead) {
484            byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accMemoryRead(CS_CLOCK_MEM_ADDR);
485            NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CS_CLOCK_MEM_SIZE);
486            waiting++;
487            waitingForCmdRead = true;
488            tc.sendNceMessage(cmdNce, this);
489            //   log.debug("issueReadOnlyRequest at " + internalClock.getTime());
490        }
491    }
492
493    private void issueClockSet(int hh, int mm, int ss) {
494        issueClockSetMem(hh, mm, ss);
495    }
496
497    private void issueClockSetMem(int hh, int mm, int ss) {
498        byte[] b = new byte[3];
499        b[0] = (byte) ss;
500        b[1] = (byte) mm;
501        b[2] = (byte) hh;
502        byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accMemoryWriteN(CS_CLOCK_MEM_ADDR + CS_CLOCK_SECONDS, b);
503        NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_MEM_SET_REPLY_SIZE);
504        waiting++;
505        waitingForCmdTime = true;
506        tc.sendNceMessage(cmdNce, this);
507    }
508
509    @SuppressWarnings({"deprecation"}) // Date.getHours, getMinutes, getSeconds
510    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
511    private Date getNceDate() {
512        Date now = internalClock.getTime();
513        if (lastClockReadPacket != null) {
514            now.setHours(lastClockReadPacket.getElement(CS_CLOCK_HOURS));
515            now.setMinutes(lastClockReadPacket.getElement(CS_CLOCK_MINUTES));
516            now.setSeconds(lastClockReadPacket.getElement(CS_CLOCK_SECONDS));
517        }
518        return (now);
519    }
520
521    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
522    private double getNceTime() {
523        double nceTime = 0;
524        if (lastClockReadPacket != null) {
525            nceTime = (lastClockReadPacket.getElement(CS_CLOCK_HOURS) * 3600)
526                    + (lastClockReadPacket.getElement(CS_CLOCK_MINUTES) * 60)
527                    + lastClockReadPacket.getElement(CS_CLOCK_SECONDS)
528                    + (lastClockReadPacket.getElement(CS_CLOCK_TICK) * 0.25);
529        }
530        return (nceTime);
531    }
532
533    @SuppressWarnings({"deprecation"}) // Date.getHours, getMinutes, getSeconds
534    @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown")
535    private double getIntTime() {
536        Date now = internalClock.getTime();
537        int ms = (int) (now.getTime() % 1000);
538        int ss = now.getSeconds();
539        int mm = now.getMinutes();
540        int hh = now.getHours();
541        log.trace("getIntTime: {}:{}:{}.{}", hh, mm, ss, ms);
542        return ((hh * 60 * 60) + (mm * 60) + ss + (ms / 1000));
543    }
544
545    private final static Logger log = LoggerFactory.getLogger(NceClockControl.class);
546
547}