001package jmri.jmrix.can.cbus; 002 003import java.text.DateFormatSymbols; 004import java.text.SimpleDateFormat; 005import java.time.LocalDateTime; 006import java.time.ZoneId; 007import java.util.Date; 008 009import javax.annotation.Nonnull; 010 011import jmri.Disposable; 012import jmri.TimebaseRateException; 013import jmri.jmrix.can.CanSystemConnectionMemo; 014import jmri.jmrix.can.CanListener; 015import jmri.jmrix.can.CanMessage; 016import jmri.jmrix.can.CanReply; 017 018import org.slf4j.Logger; 019import org.slf4j.LoggerFactory; 020 021/** 022 * Provide access to CBUS Clock Network Functions. 023 * @since 4.19.6 024 * @author Steve Young (C) 2020 025 */ 026public class CbusClockControl extends jmri.implementation.DefaultClockControl implements CanListener, Disposable { 027 028 private boolean isRunning; 029 private int _cbusTemp = 0; 030 private final jmri.Timebase clock; 031 private CanMessage _lastSent; 032 033 private final SimpleDateFormat minuteFormat; 034 private final SimpleDateFormat hourFormat; 035 private final SimpleDateFormat dayofWeek; 036 private final SimpleDateFormat dayInMonth; 037 private final SimpleDateFormat monthFormat; 038 private final SimpleDateFormat yearFormat; 039 040 private final CanSystemConnectionMemo _memo; 041 042 // Timebase listener for Minute change events from the internal clock. 043 // Held as a field so that the same instance can be removed on dispose. 044 private final java.beans.PropertyChangeListener minuteListener = this::newMinute; 045 046 public CbusClockControl(@Nonnull CanSystemConnectionMemo memo) { 047 super(); 048 049 minuteFormat = new java.text.SimpleDateFormat("mm"); 050 hourFormat = new java.text.SimpleDateFormat("H"); 051 dayofWeek = new java.text.SimpleDateFormat("u"); 052 dayInMonth = new java.text.SimpleDateFormat("d"); 053 monthFormat = new java.text.SimpleDateFormat("MM"); 054 yearFormat = new java.text.SimpleDateFormat("yyyy"); // calendar year, "YYYY" is week-year 055 056 _memo = memo; 057 this.addTc(memo); 058 // Get internal timebase 059 clock = jmri.InstanceManager.getDefault(jmri.Timebase.class); 060 // Listen for Minute change events from the internal clock 061 clock.addMinuteChangeListener(minuteListener); 062 } 063 064 private void newMinute(java.beans.PropertyChangeEvent e){ 065 sendToLayout(); 066 } 067 068 /** 069 * Get current Temperature. 070 * Int format, not twos complement. 071 * @return -128 to 127 072 */ 073 public int getTemp() { 074 return _cbusTemp; 075 } 076 077 /** 078 * Set current Temperature. 079 * Calling this method does not send to layout, is just for setting the value. 080 * Int format, not twos complement. 081 * @param newTemp -128 to 127 082 */ 083 public void setTemp(int newTemp) { 084 if (newTemp>=-128 && newTemp<=127) { 085 _cbusTemp = newTemp; 086 } 087 else { 088 log.warn("Temperature {} out of range -128 to 127",newTemp); 089 } 090 } 091 092 /** 093 * System Connection + Clock Name, e.g. MERG CBUS Fast Clock. 094 * {@inheritDoc} 095 */ 096 @Override 097 public String getHardwareClockName() { 098 return(_memo.getUserName() + " CBUS Fast Clock"); 099 } 100 101 /** 102 * {@inheritDoc} 103 */ 104 @Override 105 public void setTime(Date now) { 106 sendToLayout(); 107 } 108 109 /** 110 * 111 * {@inheritDoc} 112 */ 113 @Override 114 public void setRate(double newRate) { 115 // log.info("set rate to {}",newRate); 116 int newRatio = (int) newRate; 117 if ((newRate % 1) != 0){ 118 log.warn("Non Integer Speed rate set, DIV values sent will not be accurate."); 119 } 120 if (newRatio < -255 || newRatio > 255) { // not happening at present as checked by Timebase. 121 log.error(("ClockRatioRangeError")); 122 } else { 123 sendToLayout(); 124 } 125 } 126 127 /** 128 * {@inheritDoc} 129 */ 130 @Override 131 public void initializeHardwareClock(double rate, Date now, boolean getTime) { 132 // on startup, rate already set 133 isRunning = clock.getRun(); 134 setRate(rate); 135 setTime(now); 136 } 137 138 /** 139 * {@inheritDoc} 140 */ 141 @Override 142 public void stopHardwareClock() { 143 isRunning = false; 144 sendToLayout(); 145 } 146 147 /** 148 * {@inheritDoc} 149 */ 150 @Override 151 public void startHardwareClock(Date now) { 152 isRunning = true; 153 setTime(now); // also sends to layout 154 } 155 156 private void sendToLayout(){ 157 if (!clock.getInternalMaster() || !clock.getSynchronize()){ 158 return; 159 } 160 161 int day = Integer.parseInt(dayofWeek.format(clock.getTime()))+1; 162 if (day==8){ 163 day = 1; 164 } 165 int bstot=(Integer.parseInt(monthFormat.format(clock.getTime())) << 4)+day; 166 // weekday month, bits 0-3 are the weekday (1=Sun, 2=Mon etc) 167 // bits 4-7 are the month (1=Jan, 2=Feb etc) 168 169 CanMessage send = getCanMessage(bstot); 170 if (!(send.equals(_lastSent))) { 171 _memo.getTrafficController().sendCanMessage(send, this); 172 _lastSent = send; 173 } 174 } 175 176 private CanMessage getCanMessage(int bstot){ 177 178 CanMessage send = new CanMessage(_memo.getTrafficController().getCanid()); 179 send.setNumDataElements(7); 180 send.setElement(0, CbusConstants.CBUS_FCLK); 181 send.setElement(1, Integer.parseInt(minuteFormat.format(clock.getTime())) ); // mins 182 send.setElement(2, Integer.parseInt(hourFormat.format(clock.getTime())) ); // hrs 183 send.setElement(3, bstot); 184 send.setElement(4, ( isRunning ? (int) getRate() : 0)); // time divider, 0 is stpeed, 1 is real time, 2 twice real, 3 thrice real 185 send.setElement(5, Integer.parseInt(dayInMonth.format(clock.getTime()))); // day of month, 0-31 186 send.setElement(6, _cbusTemp & 0xff); // Temperature as twos complement -128 to +127 187 CbusMessage.setPri(send, CbusConstants.DEFAULT_DYNAMIC_PRIORITY * 4 + CbusConstants.DEFAULT_MINOR_PRIORITY); 188 189 return send; 190 191 } 192 193 /** 194 * Listen for CAN Frames sent by external CBUS FC source. 195 * Typically sent every fast minute. 196 * 197 * {@inheritDoc} 198 */ 199 @Override 200 public void reply(CanReply r) { 201 if ( r.extendedOrRtr() 202 || CbusMessage.getOpcode(r) != CbusConstants.CBUS_FCLK 203 || !clock.getSynchronize() 204 || clock.getInternalMaster()) 205 return; 206 207 setRateFromReply( r.getElement(4) & 0xff); 208 setTimeFromReply(r); 209 setTemp(tempFromTwos(r.getElement(6) & 0xff)); 210 } 211 212 private static int tempFromTwos(int twosTemp){ 213 return (twosTemp > 127 ? twosTemp - 256 : twosTemp); 214 } 215 216 private void setTimeFromReply(CanReply r) { 217 int min = r.getElement(1) & 0xff; 218 int hour = r.getElement(2) & 0xff; 219 int day = r.getElement(5) & 0xff; 220 int month = (r.getElement(3) >>> 4); 221 LocalDateTime specificDate = null; 222 try { 223 specificDate = LocalDateTime.of(Integer.parseInt(yearFormat.format(clock.getTime())) 224 , month, day, hour, min, 0); 225 } 226 catch( java.time.DateTimeException e){ 227 log.debug ("Unable to process FastClock date. Incoming: {}", r, e); 228 } 229 if (specificDate==null) { // if unset, try just the times. 230 try { 231 specificDate = LocalDateTime.of(Integer.parseInt(yearFormat.format(clock.getTime())) 232 , Integer.parseInt(monthFormat.format(clock.getTime())), 233 Integer.parseInt(dayInMonth.format(clock.getTime())), 234 hour, min, 0); 235 } 236 catch( java.time.DateTimeException e){ 237 log.warn ("Unable to process FastClock time hrs:{} mins:{} error:{} CanFrame:{}", 238 hour,min,e.getLocalizedMessage(),r); 239 } 240 } 241 if (specificDate!=null) { 242 clock.setTime(specificDate.atZone( ZoneId.systemDefault()).toInstant()); 243 } 244 } 245 246 /** 247 * Set Clock Rate, Running, Paused from incoming network. 248 * @param rate new fast clock speed multiplier. 249 */ 250 private void setRateFromReply(int rate){ 251 if ( clock.getRun() && rate==0 ){ 252 clock.setRun(false); 253 } 254 if ( !clock.getRun() && rate!=0 ){ 255 clock.setRun(true); 256 } 257 double oldRate = clock.getRate(); 258 if ( (Math.abs(rate - oldRate) > 0.0001) && rate!=0 ) { 259 try { 260 clock.userSetRate(rate); 261 } 262 catch (TimebaseRateException ex) {} // error message logged by clock. 263 } 264 } 265 266 /** 267 * Outgoing CAN Frames ignored. 268 * {@inheritDoc} 269 */ 270 @Override 271 public void message(CanMessage m) { 272 } 273 274 /** 275 * String representation of time / date from a CanMessage or CanReply. 276 * Does not check for FastClock OPC. 277 * @param r FastClock Message to translate. 278 * @return String format of Message, e.g. 279 */ 280 public static String dateFromCanFrame(jmri.jmrix.AbstractMessage r) { 281 282 // not converting to a Java Date in case the data is incorrect 283 // and we don't know 100% what year it is ( leap years ). 284 285 StringBuilder sb = new StringBuilder(); 286 int speed = r.getElement(4) & 0xff; 287 int month = (r.getElement(3) >>> 4); 288 int weekday = r.getElement(3) - ( month << 4); 289 290 // DateFormatSymbols.getInstance().getWeekdays()[] uses 1-7, not 0-6 291 if (weekday == 0){ 292 weekday =7; 293 } 294 295 log.debug("bs tot {}",Integer.toBinaryString(r.getElement(3))); 296 log.debug("bs day {} {}",Integer.toBinaryString(weekday),weekday); 297 log.debug("bs month {} {}",Integer.toBinaryString(month),month); 298 // weekday month, bits 0-3 are the weekday (1=Sun, 2=Mon 3=Tues 4+Weds 5=Thurs 6=Fri 7=Sat 299 // bits 4-7 are the month (1=Jan, 2=Feb etc) 300 301 if (speed>0) { 302 sb.append("Speed: x").append(speed).append(" "); 303 } else { 304 sb.append("Stopped "); 305 } 306 sb.append(String.format("%02d",(r.getElement(2) & 0xff))).append(":") 307 .append(String.format("%02d",r.getElement(1) & 0xff)).append(" "); 308 try { 309 sb.append(DateFormatSymbols.getInstance().getWeekdays()[weekday]).append(" "); 310 } catch ( ArrayIndexOutOfBoundsException ex ){ 311 sb.append("Incorrect weekday (").append(weekday).append(") "); 312 } 313 sb.append(r.getElement(5) & 0xff).append(" "); 314 try { 315 sb.append(DateFormatSymbols.getInstance().getMonths()[month-1]).append(" "); 316 } catch ( ArrayIndexOutOfBoundsException ex ){ 317 sb.append("Incorrect month (").append(month).append(") "); 318 } 319 sb.append("Temp: ").append(tempFromTwos(r.getElement(6) & 0xff)); 320 return sb.toString(); 321 } 322 323 /** 324 * Stops listening for updates from network and main time base. 325 * 326 */ 327 @Override 328 public void dispose() { 329 clock.removeMinuteChangeListener(minuteListener); 330 this.removeTc(_memo); 331 } 332 333 private static final Logger log = LoggerFactory.getLogger(CbusClockControl.class); 334 335}