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}