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