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}