001package jmri.jmrix.lenz;
002
003import java.util.ArrayList;
004import java.util.List;
005import javax.annotation.Nonnull;
006
007import jmri.ProgrammingMode;
008import jmri.jmrix.AbstractProgrammer;
009
010/**
011 * Convert the jmri.Programmer interface into commands for the Lenz XpressNet
012 * <p>
013 * The read operation state sequence is:
014 * <ul>
015 * <li>Send Register Mode / Paged mode /Direct Mode read request
016 * <li>Wait for Broadcast Service Mode Entry message
017 * <li>Send Request for Service Mode Results request
018 * <li>Wait for results reply, interpret
019 * <li>Send Resume Operations request
020 * <li>Wait for Normal Operations Resumed broadcast
021 * </ul>
022 * <img src="doc-files/XPressNetProgrammer-StateDiagram.png" alt="UML State diagram">
023 * <img src="doc-files/XPressNetProgrammer-SequenceDiagram.png" alt="UML Sequence diagram">
024 *
025 * @author Bob Jacobsen Copyright (c) 2002, 2007
026 * @author Paul Bender Copyright (c) 2003-2010
027 * @author Giorgio Terdina Copyright (c) 2007
028 */
029
030/*
031 * @startuml jmri/jmrix/lenz/doc-files/XPressNetProgrammer-StateDiagram.png
032 * state NormalMode{
033 * [*] --> initialREQUESTSENT: readCV()
034 * [*] --> initialREQUESTSENT: writeCV()
035 * }
036 * [*] --> NormalMode
037 * state ServiceMode{
038 * [*] --> INQUIRESENT 
039 * REQUESTSENT --> INQUIRESENT : Command Successfully Received
040 * REQUESTSENT --> NOTPROGRAMMING : timeout()
041 * INQUIRESENT --> NOTPROGRAMMING : Result Received
042 * INQUIRESENT --> NOTPROGRAMMING : timeout()
043 * NOTPROGRAMMING --> REQUESTSENT : readCV()
044 * NOTPROGRAMMING --> REQUESTSENT : writeCV()
045 * NOTPROGRAMMING --> RequestNormalOps : timeout()
046 * RequestNormalOps --> [*]
047 * }
048 * NormalMode --> ServiceMode : Service Mode Entry Received
049 * ServiceMode --> [*] : Normal Operations Resumed
050 * @enduml
051 *
052 * @startuml jmri/jmrix/lenz/doc-files/XPressNetProgrammer-SequenceDiagram.png
053 * actor user
054 * control programmer
055 * user -> programmer:read/write CV
056 * programmer -> XNetProgrammer:readCV()/writeCV()
057 * XNetProgrammer -> CommandStation: Read/Write CV in appropriate mode.
058 * CommandStation -> XNetProgrammer: Service Mode Entry.
059 * XNetProgrammer -> CommandStation: Request Service Mode Results.
060 * CommandStation -> XNetProgrammer: Service Mode Result or Error Message
061 * XNetProgrammer -> programmer: CV Value or Error Message
062 * programmer -> user: CV value or Error Message
063 * loop 0 or more times
064 * user -> programmer:read/write CV
065 * programmer -> XNetProgrammer:readCV()/writeCV()
066 * XNetProgrammer -> CommandStation: Read/Write CV in appropriate mode.
067 * CommandStation -> XNetProgrammer: Command Successfully Received.
068 * XNetProgrammer -> CommandStation: Request Service Mode Results.
069 * CommandStation -> XNetProgrammer: Service Mode Result or Error Message
070 * XNetProgrammer -> programmer: CV Value or Error Message
071 * programmer -> user: CV value or Error Message
072 * end
073 * XNetProgrammer -> CommandStation: Resume Normal Operations
074 * CommandStation -> XNetProgrammer: Normal Operations Resumed
075 * @enduml
076 */
077public class XNetProgrammer extends AbstractProgrammer implements XNetListener {
078
079    protected static final int XNetProgrammerTimeout = 90000;
080
081    // keep track of whether or not the command station is in service 
082    // mode.  Used for determining if "OK" message is an aproriate 
083    // response to a request to a programming request. 
084    protected boolean _service_mode = false;
085
086    public XNetProgrammer(XNetTrafficController tc) {
087        // error if more than one constructed?
088
089        _controller = tc;
090
091        // connect to listen
092        controller().addXNetListener(XNetInterface.CS_INFO
093                | XNetInterface.COMMINFO
094                | XNetInterface.INTERFACE,
095                this);
096
097        setMode(ProgrammingMode.DIRECTBYTEMODE);
098    }
099
100    /** 
101     * {@inheritDoc}
102     */
103    @Override
104    @Nonnull
105    public List<ProgrammingMode> getSupportedModes() {
106        List<ProgrammingMode> ret = new ArrayList<>();
107        ret.add(ProgrammingMode.DIRECTBYTEMODE);
108        ret.add(ProgrammingMode.DIRECTBITMODE);
109        ret.add(ProgrammingMode.PAGEMODE);
110        ret.add(ProgrammingMode.REGISTERMODE);
111        return ret;
112    }
113
114    /** 
115     * {@inheritDoc}
116     *
117     * Can we read from a specific CV in the specified mode? Answer may not be
118     * correct if the command station type and version sent by the command
119     * station mimics one of the known command stations.
120     */
121    @Override
122    public boolean getCanRead(String addr) {
123        if (log.isDebugEnabled()) {
124            log.debug("check mode {} CV {}", getMode(), addr);
125        }
126        if (!getCanRead()) {
127            return false; // check basic implementation first
128        }
129        // Multimaus cannot read CVs, unless Rocomotion interface is used, assume other Command Stations do.
130        // To be revised if and when a Rocomotion adapter is introduced!!!
131        if (controller().getCommandStation().getCommandStationType() == 0x10) {
132            return false;
133        }
134
135        if (getMode().equals(ProgrammingMode.DIRECTBITMODE) || getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
136            switch (controller().getCommandStation().getCommandStationType()) {
137                case XNetConstants.CS_TYPE_LZ100:
138                    if (controller().getCommandStation()
139                            .getCommandStationSoftwareVersion() <= 3.5) {
140                        return Integer.parseInt(addr) <= 256;
141                    } else {
142                        return Integer.parseInt(addr) <= 1024;
143                    }
144                default:
145                    return Integer.parseInt(addr) <= 256;
146            }
147        } else {
148            return Integer.parseInt(addr) <= 256;
149        }
150    }
151
152    /** 
153     * {@inheritDoc}
154     *
155     * Can we write to a specific CV in the specified mode? Answer may not be
156     * correct if the command station type and version sent by the command
157     * station mimics one of the known command stations.
158     */
159    @Override
160    public boolean getCanWrite(String addr) {
161        if (log.isDebugEnabled()) {
162            log.debug("check CV {} ", addr);
163            log.debug("Command Station Version {}", controller().getCommandStation().getVersionString());
164
165        }
166        if (!getCanWrite()) {
167            return false; // check basic implementation first
168        }
169        if (getMode().equals(ProgrammingMode.DIRECTBITMODE) || getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
170            switch (controller().getCommandStation().getCommandStationType()) {
171                case XNetConstants.CS_TYPE_LZ100:
172                    if (controller().getCommandStation()
173                            .getCommandStationSoftwareVersion() <= 3.5) {
174                        return Integer.parseInt(addr) <= 256;
175                    } else {
176                        return Integer.parseInt(addr) <= 1024;
177                    }
178                default:
179                    return Integer.parseInt(addr) <= 256;
180            }
181        } else {
182            return Integer.parseInt(addr) <= 256;
183        }
184    }
185
186    // members for handling the programmer interface
187    protected int progState = 0;
188    protected static final int NOTPROGRAMMING = 0; // is notProgramming
189    protected static final int REQUESTSENT = 1; // waiting reply to command to go into programming mode
190    protected static final int INQUIRESENT = 2; // read/write command sent, waiting reply
191    protected boolean _progRead = false;
192    protected int _val; // remember the value being read/written for confirmative reply
193    protected int _cv; // remember the cv being read/written
194
195    /** 
196     * {@inheritDoc}
197     */
198    @Override
199    public synchronized void writeCV(String CVname, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
200        final int CV = Integer.parseInt(CVname);
201        log.debug("writeCV {} listens {} ",CV,p);
202        useProgrammer(p);
203        _progRead = false;
204        // set new state & save values
205        progState = REQUESTSENT;
206        _val = val;
207        _cv = 0xffff & CV;
208
209        try {
210            // start the error timer
211            restartTimer(XNetProgrammerTimeout);
212
213            // format and send message to go to program mode
214            if (getMode().equals(ProgrammingMode.PAGEMODE)) {
215                XNetMessage msg = XNetMessage.getWritePagedCVMsg(CV, val);
216                controller().sendXNetMessage(msg, this);
217            } else if (getMode().equals(ProgrammingMode.DIRECTBITMODE) || getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
218                XNetMessage msg = XNetMessage.getWriteDirectCVMsg(CV, val);
219                controller().sendXNetMessage(msg, this);
220            } else { // register mode by elimination 
221                XNetMessage msg = XNetMessage.getWriteRegisterMsg(registerFromCV(CV), val);
222                controller().sendXNetMessage(msg, this);
223            }
224        } catch (jmri.ProgrammerException e) {
225            progState = NOTPROGRAMMING;
226            throw e;
227        }
228    }
229
230    /** 
231     * {@inheritDoc}
232     */
233    @Override
234    public synchronized void confirmCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException {
235        readCV(CV, p);
236    }
237
238    /** 
239     * {@inheritDoc}
240     */
241    @Override
242    public synchronized void readCV(String CVname, jmri.ProgListener p) throws jmri.ProgrammerException {
243        final int CV = Integer.parseInt(CVname);
244        log.debug("readCV {} listenes {}",CV,p);
245        // If can't read (e.g. multiMaus CS), this shouldnt be invoked, but
246        // still we need to do something rational by returning a NotImplemented error
247        if (!getCanRead()) {
248            notifyProgListenerEnd(p, CV, jmri.ProgListener.NotImplemented);
249            return;
250        }
251        useProgrammer(p);
252        _cv = 0xffff & CV;
253        _progRead = true;
254        // set new state
255        progState = REQUESTSENT;
256        try {
257            // start the error timer
258            restartTimer(XNetProgrammerTimeout);
259
260            // format and send message to go to program mode
261            if (getMode().equals(ProgrammingMode.PAGEMODE)) {
262                XNetMessage msg = XNetMessage.getReadPagedCVMsg(CV);
263                controller().sendXNetMessage(msg, this);
264            } else if (getMode().equals(ProgrammingMode.DIRECTBITMODE) || getMode().equals(ProgrammingMode.DIRECTBYTEMODE)) {
265                XNetMessage msg = XNetMessage.getReadDirectCVMsg(CV);
266                controller().sendXNetMessage(msg, this);
267            } else { // register mode by elimination    
268                XNetMessage msg = XNetMessage.getReadRegisterMsg(registerFromCV(CV));
269                controller().sendXNetMessage(msg, this);
270            }
271        } catch (jmri.ProgrammerException e) {
272            progState = NOTPROGRAMMING;
273            throw e;
274        }
275
276    }
277
278    private jmri.ProgListener _usingProgrammer = null;
279
280    // internal method to remember who's using the programmer
281    protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException {
282        // test for only one!
283        if (_usingProgrammer != null && _usingProgrammer != p) {
284            log.info("programmer already in use by {}",_usingProgrammer);
285            throw new jmri.ProgrammerException("programmer in use");
286        } else {
287            _usingProgrammer = p;
288        }
289    }
290
291    /** 
292     * {@inheritDoc}
293     */
294    @Override
295    public synchronized void message(XNetReply m) {
296        if (m.getElement(0) == XNetConstants.CS_INFO
297                && m.getElement(1) == XNetConstants.BC_SERVICE_MODE_ENTRY) {
298            if (!_service_mode) {
299                // the command station is in service mode.  An "OK" 
300                // message can trigger a request for service mode 
301                // results if progrstate is REQUESTSENT.
302                _service_mode = true;
303            } else {  // _ service_mode == true
304                // Since we get this message as both a broadcast and
305                // a directed message, ignore the message if we're
306                //already in the indicated mode
307                return;
308            }
309        }
310        if (m.getElement(0) == XNetConstants.CS_INFO
311                && m.getElement(1) == XNetConstants.BC_NORMAL_OPERATIONS) {
312            if (_service_mode) {
313                // the command station is not in service mode.  An 
314                // "OK" message can not trigger a request for service 
315                // mode results if progrstate is REQUESTSENT.
316                _service_mode = false;
317            } else { // _service_mode == false 
318                // Since we get this message as both a broadcast and
319                // a directed message, ignore the message if we're
320                //already in the indicated mode
321                return;
322            }
323        }
324        if (progState == NOTPROGRAMMING) {
325            // we get the complete set of replies now, so ignore these
326
327        } else if (progState == REQUESTSENT) {
328            if (log.isDebugEnabled()) {
329                log.debug("reply in REQUESTSENT state");
330            }
331            // see if reply is the acknowledge of program mode; if not, wait for next
332            if ((_service_mode && ( m.isOkMessage() || m.isTimeSlotRestored() ))
333                    || (m.getElement(0) == XNetConstants.CS_INFO
334                    && (m.getElement(1) == XNetConstants.BC_SERVICE_MODE_ENTRY
335                    || m.getElement(1) == XNetConstants.PROG_CS_READY))) {
336                if (!getCanRead()) {
337                    // on systems like the Roco MultiMaus 
338                    // (which does not support reading)
339                    // let a timeout occur so the system
340                    // has time to write data to the 
341                    // decoder
342                    restartTimer(SHORT_TIMEOUT);
343                    return;
344                }
345
346                // here ready to request the results
347                progState = INQUIRESENT;
348                //start the error timer
349                restartTimer(XNetProgrammerTimeout);
350
351                controller().sendXNetMessage(XNetMessage.getServiceModeResultsMsg(),
352                        this);
353            } else if (m.getElement(0) == XNetConstants.CS_INFO
354                    && m.getElement(1) == XNetConstants.CS_NOT_SUPPORTED) {
355                // programming operation not supported by this command station
356                progState = NOTPROGRAMMING;
357                notifyProgListenerEnd(_val, jmri.ProgListener.NotImplemented);
358            } else if (m.getElement(0) == XNetConstants.CS_INFO
359                    && m.getElement(1) == XNetConstants.BC_NORMAL_OPERATIONS) {
360                // We Exited Programming Mode early
361                log.error("Service mode exited before sequence complete.");
362                progState = NOTPROGRAMMING;
363                stopTimer();
364                notifyProgListenerEnd(_val, jmri.ProgListener.SequenceError);
365            } else if (m.getElement(0) == XNetConstants.CS_INFO
366                    && m.getElement(1) == XNetConstants.PROG_SHORT_CIRCUIT) {
367                // We experienced a short Circuit on the Programming Track
368                log.error("Short Circuit While Programming Decoder");
369                progState = NOTPROGRAMMING;
370                stopTimer();
371                notifyProgListenerEnd(_val, jmri.ProgListener.ProgrammingShort);
372            } else if (m.isTimeSlotErrorMessage()){
373                // we just ignore timeslot errors in the programmer.
374            } else if (m.isCommErrorMessage()) {
375                // We experienced a communications error
376                if(_controller.hasTimeSlot()) {
377                   // We have a timeslot, so report it as an error
378                   log.error("Communications error in REQUESTSENT state while programming.  Error: {}",m);
379                   progState = NOTPROGRAMMING;
380                   stopTimer();
381                   notifyProgListenerEnd(_val, jmri.ProgListener.CommError);
382                }
383            }
384        } else if (progState == INQUIRESENT) {
385            if (log.isDebugEnabled()) {
386                log.debug("reply in INQUIRESENT state");
387            }
388            // check for right message, else return
389            if (m.isPagedModeResponse()) {
390                // valid operation response, but does it belong to us?
391                try {
392                    // we always save the cv number, but if
393                    // we are using register mode, there is
394                    // at least one case (CV29) where the value
395                    // returned does not match the value we saved. 
396                    if (m.getServiceModeCVNumber() != _cv
397                            && m.getServiceModeCVNumber() != registerFromCV(_cv)) {
398                        log.debug(" result for CV {} expecting {}",m.getServiceModeCVNumber(),_cv);
399                        return;
400                    }
401                } catch (jmri.ProgrammerException e) {
402                    progState = NOTPROGRAMMING;
403                    notifyProgListenerEnd(_val, jmri.ProgListener.UnknownError);
404                }
405                // see why waiting
406                if (_progRead) {
407                    // read was in progress - get return value
408                    _val = m.getServiceModeCVValue();
409                }
410                progState = NOTPROGRAMMING;
411                stopTimer();
412                // if this was a read, we cached the value earlier.  
413                // If its a write, we're to return the original write value
414                notifyProgListenerEnd(_val, jmri.ProgListener.OK);
415            } else if (m.isDirectModeResponse()) {
416                // valid operation response, but does it belong to us?
417                if (m.getServiceModeCVNumber() != _cv) {
418                    log.debug(" CV read {} expecting {}",m.getServiceModeCVNumber(),_cv);
419                    return;
420                }
421
422                // see why waiting
423                if (_progRead) {
424                    // read was in progress - get return value
425                    _val = m.getServiceModeCVValue();
426                }
427                progState = NOTPROGRAMMING;
428                stopTimer();
429                // if this was a read, we cached the value earlier.  If its a
430                // write, we're to return the original write value
431                notifyProgListenerEnd(_val, jmri.ProgListener.OK);
432            } else if (m.getElement(0) == XNetConstants.CS_SERVICE_MODE_RESPONSE
433                    && (m.getElement(1) & 0x14) == (0x14)) {
434                // valid operation response, but does it belong to us?
435                int sent_cv = m.getServiceModeCVNumber();
436                if (sent_cv != _cv && (sent_cv == 0 && _cv != 0x0400)) {
437                    return;
438                }
439                // see why waiting
440                if (_progRead) {
441                    // read was in progress - get return value
442                    _val = m.getServiceModeCVValue();
443                }
444                progState = NOTPROGRAMMING;
445                stopTimer();
446                // if this was a read, we cached the value earlier.  If its a
447                // write, we're to return the original write value
448                notifyProgListenerEnd(_val, jmri.ProgListener.OK);
449            } else if (m.getElement(0) == XNetConstants.CS_INFO
450                    && m.getElement(1) == XNetConstants.PROG_BYTE_NOT_FOUND) {
451                // "data byte not found", e.g. no reply
452                progState = NOTPROGRAMMING;
453                stopTimer();
454                notifyProgListenerEnd(_val, jmri.ProgListener.NoLocoDetected);
455            } else if (m.getElement(0) == XNetConstants.CS_INFO
456                    && m.getElement(1) == XNetConstants.BC_NORMAL_OPERATIONS) {
457                // We Exited Programming Mode early
458                log.error("Service Mode exited before sequence complete.");
459                progState = NOTPROGRAMMING;
460                stopTimer();
461                notifyProgListenerEnd(_val, jmri.ProgListener.SequenceError);
462            } else if (m.getElement(0) == XNetConstants.CS_INFO
463                    && m.getElement(1) == XNetConstants.PROG_SHORT_CIRCUIT) {
464                // We experienced a short Circuit on the Programming Track
465                log.error("Short Circuit While Programming Decoder");
466                progState = NOTPROGRAMMING;
467                stopTimer();
468                notifyProgListenerEnd(_val, jmri.ProgListener.ProgrammingShort);
469            } else if (m.getElement(0) == XNetConstants.CS_INFO
470                    && m.getElement(1) == XNetConstants.PROG_CS_BUSY) {
471                // Command station indicated it was busy in 
472                // programming mode, request results again 
473                // (do not reset timer or change mode)
474                // NOTE: Currently only sent by OpenDCC.
475                controller().sendXNetMessage(XNetMessage.getServiceModeResultsMsg(),
476                        this);
477            } else if (m.isTimeSlotErrorMessage()){
478                // we just ignore timeslot errors in the programmer.
479            } else if (m.isCommErrorMessage()) {
480                // We experienced a communications error
481                if(_controller.hasTimeSlot()) {
482                   // We have a timeslot, so report it as an error
483                   log.error("Communications error in INQUIRESENT state while programming.  Error: {}", m);
484                   progState = NOTPROGRAMMING;
485                   stopTimer();
486                   notifyProgListenerEnd(_val, jmri.ProgListener.CommError);
487               }
488            } else {
489                // nothing important, ignore
490                log.debug("Ignoring message {}",m);
491            }
492        } else {
493            if (log.isDebugEnabled()) {
494                log.debug("reply in un-decoded state");
495            }
496        }
497    }
498
499    /** 
500     * {@inheritDoc}
501     *
502     */
503    @Override
504    public synchronized void message(XNetMessage l) {
505      // The prgrammer does not use outgoing XpressNet messges.
506    }
507
508    /** 
509     * {@inheritDoc}
510     * 
511     * Log and ignore
512     */
513    @Override
514    public void notifyTimeout(XNetMessage msg) {
515        log.debug("Notified of timeout on message {}",msg);
516    }
517
518    /**
519     * Since the Lenz programming sequence requires several 
520     * operations, we want to be able to check and see if we are
521     * currently programming before allowing the Traffic Controller 
522     * to send a request to exit service mode.
523     * @return true if programmer busy, else false.
524     */
525    public synchronized boolean programmerBusy() {
526        return (progState != NOTPROGRAMMING);
527    }
528
529    /** 
530     * {@inheritDoc}
531     */
532    @Override
533    protected synchronized void timeout() {
534        if (progState != NOTPROGRAMMING) {
535            // we're programming, time to stop
536            if (log.isDebugEnabled()) {
537                log.debug("timeout!");
538            }
539            // perhaps no loco present? Fail back to end of programming
540            progState = NOTPROGRAMMING;
541            if (getCanRead()) {
542                notifyProgListenerEnd(_val, jmri.ProgListener.FailedTimeout);
543            } else {
544                notifyProgListenerEnd(_val, jmri.ProgListener.OK);
545            }
546        }
547    }
548
549    /**
550     * Internal method to notify of the final result
551     * @param value Value returned
552     * @param status Status of operation
553     */
554    protected void notifyProgListenerEnd(int value, int status) {
555        log.debug("notifyProgListenerEnd value {} status {}.",value,status);
556        // programmingOpReply, called by noitfyProgListenerEnd
557        // in the super class, might send an immediate reply, so
558        // clear the current listener _first_
559        jmri.ProgListener temp = _usingProgrammer;
560        _usingProgrammer = null;
561        notifyProgListenerEnd(temp,value, status);
562    }
563
564    XNetTrafficController _controller;
565
566    protected XNetTrafficController controller() {
567        return _controller;
568    }
569
570    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(XNetProgrammer.class);
571
572}