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}