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