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, 2023 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_SIZE = 0x10; 073 public static final int CS_CLOCK_SCALE = 0x00; 074 public static final int CS_CLOCK_TICK = 0x01; 075 public static final int CS_CLOCK_SECONDS = 0x02; 076 public static final int CS_CLOCK_MINUTES = 0x03; 077 public static final int CS_CLOCK_HOURS = 0x04; 078 public static final int CS_CLOCK_AMPM = 0x05; 079 public static final int CS_CLOCK_1224 = 0x06; 080 public static final int CS_CLOCK_STATUS = 0x0D; 081 public static final int CMD_CLOCK_SET_TIME_SIZE = 0x03; 082 public static final int CMD_CLOCK_SET_PARAM_SIZE = 0x02; 083 public static final int CMD_CLOCK_SET_RUN_SIZE = 0x01; 084 public static final int CMD_CLOCK_SET_REPLY_SIZE = 0x01; 085 public static final int CMD_MEM_SET_REPLY_SIZE = 0x01; 086 public static final int MAX_ERROR_ARRAY = 4; 087 public static final double TARGET_SYNC_DELAY = 55; 088 public static final int SYNCMODE_OFF = 0; //0 - clocks independent 089 public static final int SYNCMODE_NCE_MASTER = 1; //1 - NCE sets Internal 090 public static final int SYNCMODE_INTERNAL_MASTER = 2; //2 - Internal sets NCE 091 public static final int WAIT_CMD_EXECUTION = 1000; 092 093 DecimalFormat fiveDigits = new DecimalFormat("0.00000"); 094 DecimalFormat fourDigits = new DecimalFormat("0.0000"); 095 DecimalFormat threeDigits = new DecimalFormat("0.000"); 096 DecimalFormat twoDigits = new DecimalFormat("0.00"); 097 098 private int waiting = 0; 099 private final int clockMode = SYNCMODE_OFF; 100 private boolean waitingForCmdRead = false; 101 private boolean waitingForCmdStop = false; 102 private boolean waitingForCmdStart = false; 103 private boolean waitingForCmdRatio = false; 104 private boolean waitingForCmdTime = false; 105 private boolean waitingForCmd1224 = false; 106 private NceReply lastClockReadPacket = null; 107 //private Date lastClockReadAtTime; 108 private int nceLastHour; 109 private int nceLastMinute; 110 private int nceLastSecond; 111 private int nceLastRatio; 112 private boolean nceLastAmPm; 113 private boolean nceLast1224; 114 //private boolean nceLastRunning; 115 //private double internalLastRatio; 116 //private boolean internalLastRunning; 117 //private double syncInterval = TARGET_SYNC_DELAY; 118 //private int internalSyncInitStateCounter = 0; 119 //private int internalSyncRunStateCounter = 0; 120 private boolean issueDeferredGetTime = false; 121 //private boolean issueDeferredGetRate = false; 122 //private boolean initNeverCalledBefore = true; 123 124 private final int nceSyncInitStateCounter = 0; // NCE master sync initialzation state machine 125 private final int nceSyncRunStateCounter = 0; // NCE master sync runtime state machine 126 //private int alarmDisplayStateCounter = 0; // manages the display update from the alarm 127 128 Timebase internalClock; 129 javax.swing.Timer alarmSyncUpdate = null; 130 java.beans.PropertyChangeListener minuteChangeListener; 131 132 // ignore replies 133 @Override 134 public void message(NceMessage m) { 135 log.error("message received: {}", m); 136 } 137 138 @Override 139 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 140 justification="I18N of log message") 141 public void reply(NceReply r) { 142 log.trace("NceReply(len {}) waiting: {} watingForRead: {} waitingForCmdTime: {} waitingForCmd1224: {} waitingForCmdRatio: {} waitingForCmdStop: {} waitingForCmdStart: {}", r.getNumDataElements(), waiting, waitingForCmdRead, waitingForCmdTime, waitingForCmd1224, waitingForCmdRatio, waitingForCmdStop, waitingForCmdStart); 143 144 if (waiting <= 0) { 145 log.error("{}", Bundle.getMessage("LogReplyEnexpected")); 146 return; 147 } 148 waiting--; 149 if (waitingForCmdRead && r.getNumDataElements() == CS_CLOCK_MEM_SIZE) { 150 readClockPacket(r); 151 waitingForCmdRead = false; 152 return; 153 } 154 if (waitingForCmdTime) { 155 if (r.getNumDataElements() != CMD_CLOCK_SET_REPLY_SIZE) { 156 log.error("{}{}", Bundle.getMessage("LogNceClockReplySizeError"), r.getNumDataElements()); 157 return; 158 } else { 159 waitingForCmdTime = false; 160 if (r.getElement(0) != NceMessage.NCE_OKAY) { 161 log.error("NCE set clock replied: {}", r.getElement(0)); 162 } 163 return; 164 } 165 } 166 if (r.getNumDataElements() != CMD_CLOCK_SET_REPLY_SIZE) { 167 log.error("{}{}", Bundle.getMessage("LogNceClockReplySizeError"), r.getNumDataElements()); 168 return; 169 } else { 170 if (waitingForCmd1224) { 171 waitingForCmd1224 = false; 172 if (r.getElement(0) != NceMessage.NCE_OKAY) { 173 log.error("{}{}", Bundle.getMessage("LogNceClock1224CmdError"), r.getElement(0)); 174 } 175 return; 176 } 177 if (waitingForCmdRatio) { 178 waitingForCmdRatio = false; 179 if (r.getElement(0) != NceMessage.NCE_OKAY) { 180 log.error("{}{}", Bundle.getMessage("LogNceClockRatioCmdError"), r.getElement(0)); 181 } 182 return; 183 } 184 if (waitingForCmdStop) { 185 waitingForCmdStop = false; 186 if (r.getElement(0) != NceMessage.NCE_OKAY) { 187 log.error("{}{}", Bundle.getMessage("LogNceClockStopCmdError"), r.getElement(0)); 188 } 189 return; 190 } 191 if (waitingForCmdStart) { 192 waitingForCmdStart = false; 193 if (r.getElement(0) != NceMessage.NCE_OKAY) { 194 log.error("waitingForCmdStart: {}{}", Bundle.getMessage("LogNceClockStartCmdError"), r.getElement(0)); 195 } 196 return; 197 } 198 } 199 // unhandled reply, nothing to do about it 200 if (log.isDebugEnabled()) { 201 StringBuffer buf = new StringBuffer(); 202 if (waiting > 0) { 203 buf.append("waiting: ").append(waiting); 204 } 205 if (waitingForCmdRead) { 206 buf.append("waitingForCmdRead: ").append(waitingForCmdRead); 207 } 208 if (waitingForCmdTime) { 209 buf.append("waitingForCmdTime: ").append(waitingForCmdTime); 210 } 211 if (waitingForCmd1224) { 212 buf.append("waitingForCmd1224: ").append(waitingForCmd1224); 213 } 214 if (waitingForCmdRatio) { 215 buf.append("waitingForCmdRatio: ").append(waitingForCmdRatio); 216 } 217 if (waitingForCmdStop) { 218 buf.append("waitingForCmdStop: ").append(waitingForCmdStop); 219 } 220 if (waitingForCmdStart) { 221 buf.append("waitingForCmdStart: ").append(waitingForCmdStart); 222 } 223 log.debug("NceReply(len {}) {}", r.getNumDataElements(), buf); 224 buf = new StringBuffer(); 225 for (int i = 0; i < r.getNumDataElements(); i++) { 226 buf.append(" ").append(r.getElement(i)); 227 } 228 log.debug("{}:{}", Bundle.getMessage("LogReplyUnexpected"), buf ); 229 } 230 } 231 232 /** 233 * name of Nce clock 234 */ 235 @Override 236 public String getHardwareClockName() { 237 if (DEBUG_SHOW_PUBLIC_CALLS ) { 238 log.debug("getHardwareClockName"); 239 } 240 return ("Nce Fast Clock"); 241 } 242 243 /** 244 * Nce clock runs stable enough 245 */ 246 @Override 247 public boolean canCorrectHardwareClock() { 248 if (DEBUG_SHOW_PUBLIC_CALLS ) { 249 log.debug("getHardwareClockName"); 250 } 251 return false; 252 } 253 254 /** 255 * Nce clock supports 12/24 operation 256 */ 257 @Override 258 public boolean canSet12Or24HourClock() { 259 if (DEBUG_SHOW_PUBLIC_CALLS ) { 260 log.debug("canSet12Or24HourClock"); 261 } 262 return true; 263 } 264 265 /** 266 * Set Nce clock speed, must be 1 to 15. 267 */ 268 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 269 justification="I18N of log message") 270 @Override 271 public void setRate(double newRate) { 272 if (DEBUG_SHOW_PUBLIC_CALLS ) { 273 log.debug("setRate: {}", newRate); 274 } 275 int newRatio = (int) newRate; 276 if (newRatio < 1 || newRatio > 15) { 277 log.error("{}", Bundle.getMessage("LogNceClockRatioRangeError")); 278 } else { 279 issueClockRatio(newRatio); 280 } 281 } 282 283 /** 284 * NCE only supports integer rates. 285 */ 286 @Override 287 public boolean requiresIntegerRate() { 288 if (DEBUG_SHOW_PUBLIC_CALLS ) { 289 log.debug("requiresIntegerRate"); 290 } 291 return true; 292 } 293 294 /** 295 * Get last known ratio from Nce clock. 296 */ 297 @Override 298 public double getRate() { 299 issueReadOnlyRequest(); // get the current rate 300 //issueDeferredGetRate = true; 301 if (DEBUG_SHOW_PUBLIC_CALLS ) { 302 log.debug("getRate: {}", nceLastRatio); 303 } 304 return (nceLastRatio); 305 } 306 307 /** 308 * Set the time, the date part is ignored. 309 */ 310 @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds 311 @Override 312 public void setTime(Date now) { 313 if (DEBUG_SHOW_PUBLIC_CALLS ) { 314 log.debug("setTime: {}", now); 315 } 316 issueClockSet(now.getHours(), now.getMinutes(), now.getSeconds()); 317 } 318 319 /** 320 * Get the current Nce time, does not have a date component. 321 */ 322 @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds 323 @Override 324 public Date getTime() { 325 issueReadOnlyRequest(); // go get the current time value 326 issueDeferredGetTime = true; 327 Date now = internalClock.getTime(); 328 if (lastClockReadPacket != null) { 329 if (nceLast1224) { // is 24 hour mode 330 now.setHours(nceLastHour); 331 } else { 332 if (nceLastAmPm) { // is AM 333 now.setHours(nceLastHour); 334 } else { 335 now.setHours(nceLastHour + 12); 336 } 337 } 338 now.setMinutes(nceLastMinute); 339 now.setSeconds(nceLastSecond); 340 } 341 if (DEBUG_SHOW_PUBLIC_CALLS ) { 342 log.debug("getTime returning: {}", now); 343 } 344 return (now); 345 } 346 347 /** 348 * Set Nce clock and start clock. 349 */ 350 @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds 351 @Override 352 public void startHardwareClock(Date now) { 353 if (DEBUG_SHOW_PUBLIC_CALLS ) { 354 log.debug("startHardwareClock: {}", now); 355 } 356 issueClockSet(now.getHours(), now.getMinutes(), now.getSeconds()); 357 issueClockStart(); 358 } 359 360 /** 361 * Stop the Nce Clock. 362 */ 363 @Override 364 public void stopHardwareClock() { 365 if (DEBUG_SHOW_PUBLIC_CALLS ) { 366 log.debug("stopHardwareClock"); 367 } 368 issueClockStop(); 369 } 370 371 /** 372 * not sure when or if this gets called, but will issue a read to get latest 373 * time 374 */ 375 public void initiateRead() { 376 if (DEBUG_SHOW_PUBLIC_CALLS ) { 377 log.debug("initiateRead"); 378 } 379 issueReadOnlyRequest(); 380 } 381 382 /** 383 * Stop any sync, removes listeners. 384 */ 385 public void dispose() { 386 387 // Remove ourselves from the timebase minute rollover event 388 if (minuteChangeListener != null) { 389 internalClock.removeMinuteChangeListener(minuteChangeListener); 390 minuteChangeListener = null; 391 } 392 } 393 394 /** 395 * Handles minute notifications for NCE Clock Monitor/Synchronizer 396 */ 397 public void newInternalMinute() { 398 if (DEBUG_SHOW_SYNC_CALLS) { 399 log.debug("newInternalMinute clockMode: {} nceInit: {} nceRun: {}", 400 clockMode, nceSyncInitStateCounter, nceSyncRunStateCounter ); 401 } 402 } 403 404 @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds 405 private void readClockPacket(NceReply r) { 406 //NceReply priorClockReadPacket = lastClockReadPacket; 407 //int priorNceRatio = nceLastRatio; 408 //boolean priorNceRunning = nceLastRunning; 409 lastClockReadPacket = r; 410 //lastClockReadAtTime = internalClock.getTime(); 411 //log.debug("readClockPacket - at time: " + lastClockReadAtTime); 412 nceLastHour = r.getElement(CS_CLOCK_HOURS) & 0xFF; 413 nceLastMinute = r.getElement(CS_CLOCK_MINUTES) & 0xFF; 414 nceLastSecond = r.getElement(CS_CLOCK_SECONDS) & 0xFF; 415 if (r.getElement(CS_CLOCK_1224) == 1) { 416 nceLast1224 = true; 417 } else { 418 nceLast1224 = false; 419 } 420 if (r.getElement(CS_CLOCK_AMPM) == 'A') { 421 nceLastAmPm = true; 422 } else { 423 nceLastAmPm = false; 424 } 425 if (issueDeferredGetTime) { 426 issueDeferredGetTime = false; 427 Date now = internalClock.getTime(); 428 if (nceLast1224) { // is 24 hour mode 429 now.setHours(nceLastHour); 430 } else { 431 if (nceLastAmPm) { // is AM 432 now.setHours(nceLastHour); 433 } else { 434 now.setHours(nceLastHour + 12); 435 } 436 } 437 now.setMinutes(nceLastMinute); 438 now.setSeconds(nceLastSecond); 439 internalClock.userSetTime(now); 440 } 441 int sc = r.getElement(CS_CLOCK_SCALE) & 0xFF; 442 if (sc > 0) { 443 nceLastRatio = 250 / sc; 444 } 445 } 446 447 private void issueClockRatio(int r) { 448 log.debug("sending ratio {} to nce cmd station", r); 449 byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accSetClockRatio(r); 450 NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE); 451 waiting++; 452 waitingForCmdRatio = true; 453 tc.sendNceMessage(cmdNce, this); 454 } 455 456 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown") 457 private void issueClock1224(boolean mode) { 458 byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accSetClock1224(mode); 459 NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE); 460 waiting++; 461 waitingForCmd1224 = true; 462 tc.sendNceMessage(cmdNce, this); 463 } 464 465 private void issueClockStop() { 466 byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accStopClock(); 467 NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE); 468 waiting++; 469 waitingForCmdStop = true; 470 tc.sendNceMessage(cmdNce, this); 471 } 472 473 private void issueClockStart() { 474 byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accStartClock(); 475 NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_CLOCK_SET_REPLY_SIZE); 476 waiting++; 477 waitingForCmdStart = true; 478 tc.sendNceMessage(cmdNce, this); 479 } 480 481 private void issueReadOnlyRequest() { 482 if (!waitingForCmdRead) { 483 byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accMemoryRead(tc.csm.getClockAddr()); 484 NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CS_CLOCK_MEM_SIZE); 485 waiting++; 486 waitingForCmdRead = true; 487 tc.sendNceMessage(cmdNce, this); 488 // log.debug("issueReadOnlyRequest at " + internalClock.getTime()); 489 } 490 } 491 492 private void issueClockSet(int hh, int mm, int ss) { 493 issueClockSetMem(hh, mm, ss); 494 } 495 496 private void issueClockSetMem(int hh, int mm, int ss) { 497 byte[] b = new byte[3]; 498 b[0] = (byte) ss; 499 b[1] = (byte) mm; 500 b[2] = (byte) hh; 501 byte[] cmd = jmri.jmrix.nce.NceBinaryCommand.accMemoryWriteN(tc.csm.getClockAddr() + CS_CLOCK_SECONDS, b); 502 NceMessage cmdNce = jmri.jmrix.nce.NceMessage.createBinaryMessage(tc, cmd, CMD_MEM_SET_REPLY_SIZE); 503 waiting++; 504 waitingForCmdTime = true; 505 tc.sendNceMessage(cmdNce, this); 506 } 507 508 @SuppressWarnings({"deprecation"}) // Date.getHours, getMinutes, getSeconds 509 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown") 510 private Date getNceDate() { 511 Date now = internalClock.getTime(); 512 if (lastClockReadPacket != null) { 513 now.setHours(lastClockReadPacket.getElement(CS_CLOCK_HOURS)); 514 now.setMinutes(lastClockReadPacket.getElement(CS_CLOCK_MINUTES)); 515 now.setSeconds(lastClockReadPacket.getElement(CS_CLOCK_SECONDS)); 516 } 517 return (now); 518 } 519 520 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown") 521 private double getNceTime() { 522 double nceTime = 0; 523 if (lastClockReadPacket != null) { 524 nceTime = (lastClockReadPacket.getElement(CS_CLOCK_HOURS) * 3600) 525 + (lastClockReadPacket.getElement(CS_CLOCK_MINUTES) * 60) 526 + lastClockReadPacket.getElement(CS_CLOCK_SECONDS) 527 + (lastClockReadPacket.getElement(CS_CLOCK_TICK) * 0.25); 528 } 529 return (nceTime); 530 } 531 532 @SuppressWarnings({"deprecation"}) // Date.getHours, getMinutes, getSeconds 533 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification="was previously marked with @SuppressWarnings, reason unknown") 534 private double getIntTime() { 535 Date now = internalClock.getTime(); 536 int ms = (int) (now.getTime() % 1000); 537 int ss = now.getSeconds(); 538 int mm = now.getMinutes(); 539 int hh = now.getHours(); 540 log.trace("getIntTime: {}:{}:{}.{}", hh, mm, ss, ms); 541 return ((hh * 60 * 60) + (mm * 60) + ss + (ms / 1000)); 542 } 543 544 private final static Logger log = LoggerFactory.getLogger(NceClockControl.class); 545 546}