001package jmri.jmrix.dcc4pc;
002
003import java.io.DataInputStream;
004import java.lang.reflect.InvocationTargetException;
005import java.util.Calendar;
006import jmri.jmrix.AbstractMRListener;
007import jmri.jmrix.AbstractMRMessage;
008import jmri.jmrix.AbstractMRReply;
009import jmri.jmrix.AbstractMRTrafficController;
010import jmri.jmrix.dcc4pc.serialdriver.SerialDriverAdapter;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013import purejavacomm.SerialPort;
014
015/**
016 * Converts Stream-based I/O to/from DCC4PC messages. The "Dcc4PcInterface" side
017 * sends/receives message objects.
018 * <p>
019 * The connection to a Dcc4PcPortController is via a pair of *Streams, which
020 * then carry sequences of characters for transmission. Note that this
021 * processing is handled in an independent thread.
022 * <p>
023 * This handles the state transitions, based on the necessary state in each
024 * message.
025 *
026 * @author Bob Jacobsen Copyright (C) 2001
027 */
028public class Dcc4PcTrafficController extends AbstractMRTrafficController implements Dcc4PcInterface {
029
030    /**
031     * Create a new DccPcTrafficController instance.
032     */
033    public Dcc4PcTrafficController() {
034        super();
035        if (log.isDebugEnabled()) {
036            log.debug("creating a new Dcc4PcTrafficController object");
037        }
038        this.setAllowUnexpectedReply(false);
039    }
040
041    public void setAdapterMemo(Dcc4PcSystemConnectionMemo memo) {
042        adaptermemo = memo;
043    }
044
045    Dcc4PcSystemConnectionMemo adaptermemo;
046
047    @Override
048    public synchronized void addDcc4PcListener(Dcc4PcListener l) {
049        this.addListener(l);
050    }
051
052    @Override
053    public synchronized void removeDcc4PcListener(Dcc4PcListener l) {
054        this.removeListener(l);
055    }
056
057    public static final int RETRIEVINGDATA = 100;
058
059    /**
060     * Forward a Dcc4PcMessage to all registered Dcc4PcInterface listeners.
061     */
062    @Override
063    protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) {
064        ((Dcc4PcListener) client).message((Dcc4PcMessage) m);
065    }
066
067    /**
068     * Forward a Dcc4PcReply to all registered Dcc4PcInterface listeners.
069     */
070    @Override
071    protected void forwardReply(AbstractMRListener client, AbstractMRReply r) {
072        ((Dcc4PcListener) client).reply((Dcc4PcReply) r);
073    }
074
075    @Override
076    protected AbstractMRMessage pollMessage() {
077        return null;
078    }
079
080    @Override
081    protected AbstractMRListener pollReplyHandler() {
082        return null;
083    }
084
085    /**
086     * Forward a preformatted message to the actual interface.
087     */
088    @Override
089    public void sendDcc4PcMessage(Dcc4PcMessage m, Dcc4PcListener reply) {
090        sendMessage(m, reply);
091    }
092
093    protected boolean unsolicitedSensorMessageSeen = false;
094
095    //Dcc4Pc doesn't support this function.
096    @Override
097    protected AbstractMRMessage enterProgMode() {
098        return Dcc4PcMessage.getProgMode();
099    }
100
101    //Dcc4Pc doesn't support this function!
102    @Override
103    protected AbstractMRMessage enterNormalMode() {
104        return Dcc4PcMessage.getExitProgMode();
105    }
106
107    @Override
108    protected void addTrailerToOutput(byte[] msg, int offset, AbstractMRMessage m) {
109    }
110
111    Dcc4PcMessage mLastMessage;  //Last message requested with a reply listener ie from external methods
112    Dcc4PcMessage mLastSentMessage; //Last message actually sent from within the code, ie getResponse.
113
114    @Override
115    synchronized protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) {
116        if (log.isDebugEnabled()) {
117            log.debug("forwardToPort message: [{}]", m);
118        }
119        if (port == null) {
120            return;
121        }
122        // remember who sent this
123        mLastSender = reply;
124        mLastMessage = (Dcc4PcMessage) m;
125
126        // forward the message to the registered recipients,
127        // which includes the communications monitor, except the sender.
128        // Schedule notification via the Swing event queue to ensure order
129        if (!mLastMessage.isGetResponse()) {
130            //Do not forward on the get response packets, saves filling up the monitors with chaff
131            Runnable r = new XmtNotifier(m, mLastSender, this);
132            javax.swing.SwingUtilities.invokeLater(r);
133        }
134        forwardToPort(m);
135
136    }
137
138    //this forward to port is also used internally for repeating commands.
139    private void forwardToPort(AbstractMRMessage m) {
140        mLastSentMessage = (Dcc4PcMessage) m;
141        // stream to port in single write, as that's needed by serial
142        byte msg[] = new byte[lengthOfByteStream(m)];
143
144        // add data content
145        int len = m.getNumDataElements();
146        for (int i = 0; i < len; i++) {
147            msg[i] = (byte) m.getElement(i);
148        }
149
150        try {
151            if (ostream != null) {
152                if (log.isDebugEnabled()) {
153                    StringBuilder f = new StringBuilder();
154                    for (int i = 0; i < msg.length; i++) {
155                        f.append(Integer.toHexString(0xFF & msg[i]));
156                        f.append(" ");
157                    }
158                    log.debug("formatted message: {}", f);
159                }
160                while (m.getRetries() >= 0) {
161                    if (portReadyToSend(controller)) {
162                        port.setDTR(true);
163                        ostream.write(msg);
164                        try {
165                            Thread.sleep(20);
166                        } catch (InterruptedException ex) {
167                            Thread.currentThread().interrupt();
168                        } catch (Exception ex) {
169                            log.warn("sendMessage: Exception: {}", ex.toString());
170                        }
171                        ostream.flush();
172                        port.setDTR(false);
173                        break;
174                    } else if (m.getRetries() >= 0) {
175                        log.debug("Retry message: {} attempts remaining: {}", m, m.getRetries());
176                        m.setRetries(m.getRetries() - 1);
177                        try {
178                            synchronized (xmtRunnable) {
179                                xmtRunnable.wait(m.getTimeout());
180                            }
181                        } catch (InterruptedException e) {
182                            Thread.currentThread().interrupt(); // retain if needed later
183                            log.error("retry wait interrupted");
184                        }
185                    } else {
186                        log.warn("sendMessage: port not ready for data sending: {}", java.util.Arrays.toString(msg));
187                    }
188                }
189            } else {
190                // ostream is null
191                // no stream connected
192                connectionWarn();
193            }
194        } catch (java.io.IOException | RuntimeException e) {
195            // TODO Currently there's no port recovery if an exception occurs
196            // must restart JMRI to clear xmtException.
197            xmtException = true;
198            portWarn(e);
199        }
200    }
201    SerialPort port;
202
203    @Override
204    public void connectPort(jmri.jmrix.AbstractPortController p) {
205
206        super.connectPort(p);
207        port = ((SerialDriverAdapter) controller).getSerialPort();
208
209    }
210
211    @Override
212    protected AbstractMRReply newReply() {
213        Dcc4PcReply reply = new Dcc4PcReply();
214        return reply;
215    }
216
217    // for now, receive always OK
218    @Override
219    protected boolean canReceive() {
220        return true;
221    }
222
223    @Override
224    protected boolean endOfMessage(AbstractMRReply msg) {
225        if (port.isDSR()) {
226            return false;
227        }
228        try {
229            if (controller.getInputStream().available() > 0) {
230                if (port.isRI()) {
231                    log.debug("??? Ringing true ???");
232                }
233                return false;
234            }
235
236            //log.debug("No more input available " + port.isDSR());
237            if (port.isRI()) {
238                log.debug("??? Ringing true ???");
239            }
240            return true;
241        } catch (java.io.IOException ex) {
242            log.error("IO Exception{}", ex.toString());
243        }
244        return !port.isDSR();
245    }
246
247    @Override
248    protected void handleTimeout(AbstractMRMessage msg, AbstractMRListener l) {
249        if(l != null){
250            ((Dcc4PcListener) l).handleTimeout((Dcc4PcMessage) msg);
251        }
252        super.handleTimeout(msg, l);
253    }
254
255    Dcc4PcReply lastIncomplete;
256    boolean waitingForMore = false;
257    boolean loading = false;
258
259    final int GETMOREDATA = 0x01;
260
261    /**
262     * Handle each reply when complete.
263     * <p>
264     * (This is public for testing purposes) Runs in the "Receive" thread.
265     *
266     */
267    @Override
268    public void handleOneIncomingReply() throws java.io.IOException {
269        // we sit in this until the message is complete, relying on
270        // threading to let other stuff happen
271
272        // Create message off the right concrete class
273        AbstractMRReply msg = newReply();
274
275        // message exists, now fill it
276        loadChars(msg, istream);
277        if (mLastSentMessage != null) {
278            ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage);
279            //log.debug(mLastMessage.getElement(0));
280            if (mLastSentMessage.isForChildBoard()) {
281                if (log.isDebugEnabled()) {
282                    log.debug("This is a message for a child board {}", ((Dcc4PcReply) msg).toHexString());
283                    log.debug("Originate {}", mLastMessage.toString());
284                }
285                if ((mLastSentMessage.getNumDataElements() - 1) == msg.getElement(1)) {
286                    log.debug("message lengths match");
287                    waitingForMore = true;
288                    try {
289                        Thread.sleep(10);
290                    } catch (InterruptedException ex) {
291                        log.debug("InterruptedException", ex);
292                    }
293                    //log.debug("We do not forward the response to the listener as it has not been formed");
294                    lastIncomplete = null;
295                    forwardToPort(Dcc4PcMessage.getResponse());
296
297                    return;
298                } else {
299                    if (log.isDebugEnabled()) {
300                        log.debug("Not all of the command was sent, we need to figure out a way to resend the bits");
301                        log.debug("Original Message length {}", mLastSentMessage.getNumDataElements());
302                        log.debug("What CID has procced in size {}", (byte) msg.getElement(1));
303                        log.debug("Reply is in error {}", ((Dcc4PcReply) msg).toHexString());
304                    }
305                }
306            } else if (mLastSentMessage.getElement(0) == 0x0C) {
307                if (log.isDebugEnabled()) {
308                    log.debug("last message was a get response {}", ((Dcc4PcReply) msg).toHexString());
309                }
310                if (msg.getElement(0) == Dcc4PcReply.SUCCESS) {
311                    ((Dcc4PcReply) msg).strip();
312                    if (lastIncomplete != null) {
313                        //log.debug("Need to add the new reply to this message");
314                        //log.debug("existing : " + lastIncomplete.toHexString());
315
316                        //Append this message to the last incomplete message
317                        if (msg.getNumDataElements() != 0) {
318                            int iOrig = lastIncomplete.getNumDataElements();
319                            int iNew = 0;
320                            while (iNew < msg.getNumDataElements()) {
321                                lastIncomplete.setElement(iOrig, msg.getElement(iNew));
322                                iOrig++;
323                                iNew++;
324                            }
325                        }
326                        //set the last incomplete message as the one to return
327                        log.debug("Reply set as lastIncomplete");
328                        msg = lastIncomplete;
329                    }
330                    ((Dcc4PcReply) msg).setError(false);
331                    ((Dcc4PcReply)msg).setOriginalRequest(mLastMessage);
332                    lastIncomplete = null;
333                    waitingForMore = false;
334                    mLastMessage = null;
335                    mLastSentMessage = null;
336                } else if (msg.getElement(0) == Dcc4PcReply.INCOMPLETE) {
337                    waitingForMore = true;
338                    ((Dcc4PcReply) msg).strip();
339                    if (lastIncomplete != null) {
340                        //Append this message to the last incomplete message
341                        if (msg.getNumDataElements() != 0) {
342                            int iOrig = lastIncomplete.getNumDataElements();
343                            int iNew = 0;
344                            while (iNew < msg.getNumDataElements()) {
345                                lastIncomplete.setElement(iOrig, msg.getElement(iNew));
346                                iOrig++;
347                                iNew++;
348                            }
349                        }
350
351                    } else if (msg.getNumDataElements() > 1) {
352                        lastIncomplete = (Dcc4PcReply) msg;
353                    }
354                    //We do not forward the response to the listener as it has not been formed
355                    forwardToPort(Dcc4PcMessage.getResponse());
356
357                    return;
358
359                } else {
360                    log.debug("Reply is an error mesage");
361                    ((Dcc4PcReply) msg).setError(true);
362                    mLastMessage.setRetries(mLastMessage.getRetries() - 1);
363                    if (mLastMessage.getRetries() >= 0) {
364                        synchronized (xmtRunnable) {
365                            mCurrentState = AUTORETRYSTATE;
366                            replyInDispatch = false;
367                            xmtRunnable.notify();
368                        }
369                        return;
370                    }
371                }
372            }
373        } else {
374            log.debug("Last message sent was null {}", ((Dcc4PcReply) msg).toHexString());
375        }
376
377        // message is complete, dispatch it !!
378        replyInDispatch = true;
379        if (log.isDebugEnabled()) {
380            log.debug("dispatch reply of length {} contains {} state {}", msg.getNumDataElements(), msg.toString(), mCurrentState);
381        }
382        // forward the message to the registered recipients,
383        // which includes the communications monitor
384        // return a notification via the Swing event queue to ensure proper thread
385        Runnable r = newRcvNotifier(msg, mLastSender, this);
386        try {
387            javax.swing.SwingUtilities.invokeAndWait(r);
388        } catch (InterruptedException | InvocationTargetException e) {
389            log.error("Unexpected exception in invokeAndWait:", e);
390        }
391
392        if (log.isDebugEnabled()) {
393            log.debug("dispatch thread invoked");
394        }
395        if (!msg.isUnsolicited()) {
396            // effect on transmit:
397            switch (mCurrentState) {
398                case WAITMSGREPLYSTATE: {
399                    // check to see if the response was an error message we want
400                    // to automatically handle by re-queueing the last sent
401                    // message, otherwise go on to the next message
402                    if (msg.isRetransmittableErrorMsg()) {
403                        if (log.isDebugEnabled()) {
404                            log.debug("Automatic Recovery from Error Message: {}", msg.toString());
405                        }
406                        synchronized (xmtRunnable) {
407                            mCurrentState = AUTORETRYSTATE;
408                            replyInDispatch = false;
409                            xmtRunnable.notify();
410                        }
411                    } else {
412                        // update state, and notify to continue
413                        synchronized (xmtRunnable) {
414                            mCurrentState = NOTIFIEDSTATE;
415                            replyInDispatch = false;
416                            xmtRunnable.notify();
417                        }
418                    }
419                    break;
420                }
421                case WAITREPLYINPROGMODESTATE: {
422                    // entering programming mode
423                    mCurrentMode = PROGRAMINGMODE;
424                    replyInDispatch = false;
425
426                    // check to see if we need to delay to allow decoders to become
427                    // responsive
428                    int warmUpDelay = enterProgModeDelayTime();
429                    if (warmUpDelay != 0) {
430                        try {
431                            synchronized (xmtRunnable) {
432                                xmtRunnable.wait(warmUpDelay);
433                            }
434                        } catch (InterruptedException e) {
435                            Thread.currentThread().interrupt(); // retain if needed later
436                        }
437                    }
438                    // update state, and notify to continue
439                    synchronized (xmtRunnable) {
440                        mCurrentState = OKSENDMSGSTATE;
441                        xmtRunnable.notify();
442                    }
443                    break;
444                }
445                case WAITREPLYINNORMMODESTATE: {
446                    // entering normal mode
447                    mCurrentMode = NORMALMODE;
448                    replyInDispatch = false;
449                    // update state, and notify to continue
450                    synchronized (xmtRunnable) {
451                        mCurrentState = OKSENDMSGSTATE;
452                        xmtRunnable.notify();
453                    }
454                    break;
455                }
456                default: {
457                    replyInDispatch = false;
458                    unexpectedReplyStateError(mCurrentState,msg.toString());
459                }
460            }
461            // Unsolicited message
462        } else {
463            if (log.isDebugEnabled()) {
464                log.debug("Unsolicited Message Received {}", msg.toString());
465            }
466            replyInDispatch = false;
467        }
468    }
469
470    boolean normalFlushReceiveChars = false;
471
472    //Need a way to detect that the dsr has gone low.
473    @Override
474    protected void loadChars(AbstractMRReply msg, DataInputStream istream)
475            throws java.io.IOException {
476        int i;
477        readingData = false;
478        MAINGET:
479        {
480            for (i = 0; i < msg.maxSize(); i++) {
481                boolean waiting = true;
482                while (waiting) {
483                    if (controller.getInputStream().available() > 0) {
484                        readingData = true;
485                        byte char1 = readByteProtected(istream);
486                        waiting = false;
487
488                        //potentially add in a flush here that is generated by the transmit after a command has been sent, but this is not an error type flush.l
489                        // if there was a timeout, flush any char received and start over
490                        if (flushReceiveChars) {
491                            lastIncomplete = null;
492                            waitingForMore = false;
493                            mLastMessage = null;
494                            mLastSentMessage = null;
495                            readingData = false;
496                            log.warn("timeout flushes receive buffer: {}", ((Dcc4PcReply) msg).toHexString());
497                            msg.flush();
498                            i = 0;  // restart
499                            flushReceiveChars = false;
500                            waiting = true;
501                        } else {
502                            if (canReceive()) {
503                                if (log.isDebugEnabled()) {
504                                    log.debug("Set data {}, {}", i, char1 & 0xff);
505                                }
506                                msg.setElement(i, char1);
507                                waiting = false;
508                                if (port.isRI()) {
509                                    log.debug("Ring high error");
510                                    ((Dcc4PcReply) msg).setError(true);
511                                    break MAINGET;
512                                }
513                                if (endOfMessage(msg)) {
514                                    break MAINGET;
515                                }
516                            } else {
517                                i--; // flush char
518                                log.error("unsolicited character received: {}", Integer.toHexString(char1));
519                            }
520                        }
521                    } else if (!port.isDSR()) {
522                        if (i == 0) {
523                            waiting = true;
524                        } else {
525                            log.debug("We have data so will break");
526                            waiting = false;
527                            break MAINGET;
528                        }
529                    } else {
530                        //As we have no data to process we will set the readingData flag false;
531                        readingData = false;
532                    }
533                }
534            }
535        }
536    }
537
538    boolean readingData = false;
539
540    @Override
541    protected void transmitWait(int waitTime, int state, String InterruptMessage) {
542        // wait() can have spurious wakeup!
543        // so we protect by making sure the entire timeout time is used
544        long currentTime = Calendar.getInstance().getTimeInMillis();
545        long endTime = currentTime + waitTime;
546        while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) {
547            long wait = endTime - currentTime;
548            try {
549                synchronized (xmtRunnable) {
550                    // Do not wait if the current state has changed since we
551                    // last set it.
552
553                    if (mCurrentState != state) {
554                        return;
555                    }
556                    xmtRunnable.wait(wait); // rcvr normally ends this w state change
557                    //If we are in the process of reading the data then do not time out.
558                    if (readingData) {
559                        endTime = endTime + 10;
560                    }
561                    //if we have received a packet and a seperate message has been sent to retrieve
562                    //the reply we will add more time to our wait process.
563                    if (waitingForMore) {
564                        waitingForMore = false;
565                        //if we are in the process of retrieving data, then we shall increase the endTime by 200ms.
566                        endTime = endTime + 200;
567                    }
568
569                }
570            } catch (InterruptedException e) {
571                Thread.currentThread().interrupt(); // retain if needed later
572                log.error("{} from {}", InterruptMessage, e.getMessage());
573            }
574        }
575        log.debug("TIMEOUT in transmitWait, mCurrentState:{} {} port dsr {} wait time {}", mCurrentState, state, port.isDSR(), waitTime);
576    }
577
578    private final static Logger log = LoggerFactory.getLogger(Dcc4PcTrafficController.class);
579}