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