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}