001package jmri.jmrix.loconet;
002
003import java.util.Date;
004import jmri.PowerManager;
005import jmri.implementation.DefaultClockControl;
006
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010/**
011 * Implementation of the Hardware Fast Clock for LocoNet.
012 * <p>
013 * This module is based on a GUI module developed by Bob Jacobsen and Alex
014 * Shepherd to correct the LocoNet fast clock rate and synchronize it with the
015 * internal JMRI fast clock Timebase. The methods that actually send, correct,
016 * or receive information from the LocoNet hardware are repackaged versions of
017 * their code.
018 * <p>
019 * The LocoNet Fast Clock is controlled by the user via the Fast Clock Setup GUI
020 * that is accessed from the JMRI Tools menu.
021 * <p>
022 * For this implementation, "synchronize" implies "correct", since the two
023 * clocks run at a different rate.
024 * <p>
025 * Some of the message formats used in this class are Copyright Digitrax, Inc.
026 * and used with permission as part of the JMRI project. That permission does
027 * not extend to uses in other software products. If you wish to use this code,
028 * algorithm or these message formats outside of JMRI, please contact Digitrax
029 * Inc for separate permission.
030 * <hr>
031 * This file is part of JMRI.
032 * <p>
033 * JMRI is free software; you can redistribute it and/or modify it under the
034 * terms of version 2 of the GNU General Public License as published by the Free
035 * Software Foundation. See the "COPYING" file for a copy of this license.
036 * <p>
037 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
038 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
039 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
040 *
041 * @author Dave Duchamp Copyright (C) 2007
042 * @author Bob Jacobsen, Alex Shepherd
043 */
044public class LnClockControl extends DefaultClockControl implements SlotListener {
045
046
047    /**
048     * Create a ClockControl object for a LocoNet clock.
049     *
050     * @param scm  the LocoNet System Connection Memo to associate with this
051     *              Clock Control object
052     */
053    public LnClockControl(LocoNetSystemConnectionMemo scm) {
054        this(scm.getSlotManager(), scm.getLnTrafficController(), scm.getPowerManager());
055    }
056
057    /**
058     * Create a ClockControl object for a LocoNet clock.
059     *
060     * @param sm the Slot Manager associated with this object
061     * @param tc the Traffic Controller associated with this object
062     * @param pm the PowerManager associated with this object
063     */
064    public LnClockControl(SlotManager sm, LnTrafficController tc, LnPowerManager pm) {
065        super();
066
067        this.sm = sm;
068        this.tc = tc;
069        this.pm = pm;
070
071        // listen for updated slot contents
072        if (sm != null) {
073            sm.addSlotListener(this);
074        } else {
075            log.error("No LocoNet connection available, LnClockControl can't function");
076        }
077
078        // Get internal timebase
079        clock = jmri.InstanceManager.getDefault(jmri.Timebase.class);
080        // Create a Timebase listener for Minute change events from the internal clock
081        minuteChangeListener = new java.beans.PropertyChangeListener() {
082            @Override
083            public void propertyChange(java.beans.PropertyChangeEvent e) {
084                newMinute();
085            }
086        };
087        clock.addMinuteChangeListener(minuteChangeListener);
088    }
089
090    final SlotManager sm;
091    final LnTrafficController tc;
092    final LnPowerManager pm;
093
094    /* Operational variables */
095    jmri.Timebase clock = null;
096    java.beans.PropertyChangeListener minuteChangeListener = null;
097    /* current values of clock variables */
098    private int curDays = 0;
099    private int curHours = 0;
100    private int curMinutes = 0;
101    private int curFractionalMinutes = 900;
102    private int curRate = 1;
103    private int savedRate = 1;
104    /* current options and flags */
105    private boolean setInternal = false;   // true if LocoNet Clock is the master
106    private boolean synchronizeWithInternalClock = false;
107    private boolean inSyncWithInternalFastClock = false;
108    private boolean timebaseErrorReported = false;
109    private boolean correctFastClock = false;
110    private boolean readInProgress = false;
111    /* constants */
112    final static long MSECPERHOUR = 3600000;
113    final static long MSECPERMINUTE = 60000;
114    final static double CORRECTION = 915.0;
115
116    /**
117     * Accessor routines
118     * @return the associated name
119     */
120    @Override
121    public String getHardwareClockName() {
122        return (Bundle.getMessage("LocoNetFastClockName"));
123    }
124
125    @Override
126    public boolean canCorrectHardwareClock() {
127        return true;
128    }
129
130    @Override
131    public void setRate(double newRate) {
132        if (curRate == 0) {
133            savedRate = (int) newRate;      // clock stopped case
134        } else {
135            curRate = (int) newRate;        // clock running case
136            savedRate = curRate;
137        }
138        setClock();
139    }
140
141    @Override
142    public boolean requiresIntegerRate() {
143        return true;
144    }
145
146    @Override
147    public double getRate() {
148        return curRate;
149    }
150
151    @SuppressWarnings("deprecation") // Date.getHours, Date.getMinutes
152    @Override
153    public void setTime(Date now) {
154        curDays = now.getDate();
155        curHours = now.getHours();
156        curMinutes = now.getMinutes();
157        setClock();
158    }
159
160    @SuppressWarnings("deprecation") // Date.getTime, Date.getHours
161    @Override
162    public Date getTime() {
163        Date tem = clock.getTime();
164        int cHours = tem.getHours();
165        long cNumMSec = tem.getTime();
166        long nNumMSec = ((cNumMSec / MSECPERHOUR) * MSECPERHOUR) - (cHours * MSECPERHOUR)
167                + (curHours * MSECPERHOUR) + (curMinutes * MSECPERMINUTE);
168        // Work out how far through the current fast minute we are
169        // and add that on to the time.
170        nNumMSec += (long) (((CORRECTION - curFractionalMinutes) / CORRECTION * MSECPERMINUTE));
171        return (new Date(nNumMSec));
172    }
173
174    @Override
175    public void startHardwareClock(Date now) {
176        curRate = savedRate;
177        setTime(now);
178    }
179
180    @Override
181    public void stopHardwareClock() {
182        savedRate = curRate;
183        curRate = 0;
184        setClock();
185    }
186
187    @SuppressWarnings("deprecation") // Date.getDate, Date.getHours
188    @Override
189    public void initializeHardwareClock(double rate, Date now, boolean getTime) {
190        synchronizeWithInternalClock = clock.getSynchronize();
191        correctFastClock = clock.getCorrectHardware();
192        setInternal = !clock.getInternalMaster();
193        if (!setInternal && !synchronizeWithInternalClock && !correctFastClock) {
194            // No request to interact with hardware fast clock - ignore call
195            return;
196        }
197        if (rate == 0.0) {
198            if (curRate != 0) {
199                savedRate = curRate;
200            }
201            curRate = 0;
202        } else {
203            savedRate = (int) rate;
204            if (curRate != 0) {
205                curRate = savedRate;
206            }
207        }
208        curDays = now.getDate();
209        curHours = now.getHours();
210        curMinutes = now.getMinutes();
211        if (!getTime) {
212            setTime(now);
213        }
214        if (getTime || synchronizeWithInternalClock || correctFastClock) {
215            inSyncWithInternalFastClock = false;
216            initiateRead();
217        }
218    }
219
220    /**
221     * Requests read of the LocoNet fast clock
222     */
223    public void initiateRead() {
224        if (!readInProgress) {
225            sm.sendReadSlot(LnConstants.FC_SLOT);
226            readInProgress = true;
227        }
228    }
229
230    /**
231     * Corrects the LocoNet Fast Clock
232     */
233    @SuppressWarnings("deprecation") // Date.getDate, Date.getHours, Date.getMinutes
234    public void newMinute() {
235        // ignore if waiting on LocoNet clock read
236        if (!inSyncWithInternalFastClock) {
237            return;
238        }
239        if (correctFastClock || synchronizeWithInternalClock) {
240            // get time from the internal clock
241            Date now = clock.getTime();
242            // skip the correction if minutes is 0 because Logic Rail Clock displays incorrectly
243            //  if a correction is sent at zero minutes.
244            if (now.getMinutes() != 0) {
245                // Set the Fast Clock Day to the current Day of the month 1-31
246                curDays = now.getDate();
247                // Update current time
248                curHours = now.getHours();
249                curMinutes = now.getMinutes();
250                long millis = now.getTime();
251                // How many ms are we into the fast minute as we want to sync the
252                // Fast Clock Master Frac_Mins to the right 65.535 ms tick
253                long elapsedMS = millis % MSECPERMINUTE;
254                double frac_min = elapsedMS / (double) MSECPERMINUTE;
255                curFractionalMinutes = (int) CORRECTION - (int) (CORRECTION * frac_min);
256                setClock();
257            }
258        } else if (setInternal) {
259            inSyncWithInternalFastClock = false;
260            initiateRead();
261        }
262    }
263
264    /**
265     * Handle changed slot contents, due to clock changes. Can get here three
266     * ways: 1) clock slot as a result of action by a throttle and 2) clock slot
267     * responding to a read from this module 3) a slot not involving the clock
268     * changing.
269     *
270     * @param s the LocoNetSlot object which has been changed
271     */
272    @SuppressWarnings("deprecation") // Date.getTime, Date.getHours
273    @Override
274    public void notifyChangedSlot(LocoNetSlot s) {
275        // only watch the clock slot
276        if (s.getSlot() != LnConstants.FC_SLOT) {
277            return;
278        }
279        // if don't need to know, simply return
280        if (!correctFastClock && !synchronizeWithInternalClock && !setInternal) {
281            return;
282        }
283        if (log.isDebugEnabled()) {
284            log.debug("slot update {}", s);
285        }
286        // update current clock variables from the new slot contents
287        curDays = s.getFcDays();
288        curHours = s.getFcHours();
289        curMinutes = s.getFcMinutes();
290        int temRate = s.getFcRate();
291        // reject the new rate if different and not resetting the internal clock
292        if ((temRate != curRate) && !setInternal) {
293            setRate(curRate);
294        } // keep the new rate if different and resetting the internal clock
295        else if ((temRate != curRate) && setInternal) {
296            try {
297                clock.userSetRate(temRate);
298            } catch (jmri.TimebaseRateException e) {
299                if (!timebaseErrorReported) {
300                    timebaseErrorReported = true;
301                    log.warn("Time base exception on setting rate from LocoNet");
302                }
303            }
304        }
305        curFractionalMinutes = s.getFcFracMins();
306        // we calculate a new msec value for a specific hour/minute
307        // in the current day, then set that.
308        Date tem = clock.getTime();
309        int cHours = tem.getHours();
310        long cNumMSec = tem.getTime();
311        long nNumMSec = ((cNumMSec / MSECPERHOUR) * MSECPERHOUR) - (cHours * MSECPERHOUR)
312                + (curHours * MSECPERHOUR) + (curMinutes * MSECPERMINUTE);
313        // set the internal timebase based on the LocoNet clock
314        if (readInProgress && !inSyncWithInternalFastClock) {
315            // Work out how far through the current fast minute we are
316            // and add that on to the time.
317            nNumMSec += (long) (((CORRECTION - curFractionalMinutes) / CORRECTION * MSECPERMINUTE));
318            clock.setTime(new Date(nNumMSec));
319        } else if (setInternal) {
320            // unsolicited time change from the LocoNet
321            clock.setTime(new Date(nNumMSec));
322        }
323        // Once we have done everything else set the flag to say we are in sync
324        inSyncWithInternalFastClock = true;
325    }
326
327    /**
328     * Push current Clock Control parameters out to LocoNet slot.
329     */
330    private void setClock() {
331        if (setInternal || synchronizeWithInternalClock || correctFastClock) {
332            // we are allowed to send commands to the fast clock
333            LocoNetSlot s = sm.slot(LnConstants.FC_SLOT);
334
335            // load time
336            s.setFcDays(curDays);
337            s.setFcHours(curHours);
338            s.setFcMinutes(curMinutes);
339            s.setFcRate(curRate);
340            s.setFcFracMins(curFractionalMinutes);
341
342            // set other content
343            //     power (GTRK_POWER, 0x01 bit in byte 7)
344            boolean power = true;
345            if (pm != null) {
346                power = (pm.getPower() == PowerManager.ON);
347            } else {
348                jmri.util.LoggingUtil.warnOnce(log, "Can't access power manager for fast clock");
349            }
350            s.setTrackStatus(s.getTrackStatus() &  (~LnConstants.GTRK_POWER) );
351            if (power) s.setTrackStatus(s.getTrackStatus() | LnConstants.GTRK_POWER);
352
353            // and write
354            tc.sendLocoNetMessage(s.writeSlot());
355        }
356    }
357
358    public void dispose() {
359        // Drop LocoNet connection
360        if (sm != null) {
361            sm.removeSlotListener(this);
362        }
363
364        // Remove ourselves from the Timebase minute rollover event
365        if (minuteChangeListener != null) {
366            clock.removeMinuteChangeListener(minuteChangeListener);
367            minuteChangeListener = null;
368        }
369    }
370
371    private final static Logger log = LoggerFactory.getLogger(LnClockControl.class);
372
373}