001package jmri.jmrix.roco.z21;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.net.DatagramPacket;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.List;
008
009import jmri.jmrix.*;
010import org.slf4j.Logger;
011import org.slf4j.LoggerFactory;
012
013/**
014 * Abstract base for TrafficControllers in a Message/Reply protocol.
015 *
016 * @author Paul Bender Copyright (C) 2014
017 */
018public class Z21TrafficController extends jmri.jmrix.AbstractMRTrafficController implements Z21Interface {
019
020    // Z21 LAN Protocol Specification v1.13 section 1.3 allows combined datasets
021    // up to the Ethernet MTU payload: 1472 = 1500-byte MTU - IPv4 - UDP headers.
022    private static final int MAX_UDP_PAYLOAD = 1472;
023
024    private java.net.InetAddress host;
025    private int port;
026
027    public Z21TrafficController() {
028        super();
029        allowUnexpectedReply = true;
030    }
031
032    /**
033     * Implement this to forward a specific message type to a protocol-specific
034     * listener interface. This puts the casting into the concrete class.
035     */
036    @Override
037    protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) {
038        ((Z21Listener) client).message((Z21Message) m);
039    }
040
041    /**
042     * Implement this to forward a specific Reply type to a protocol-specific
043     * listener interface. This puts the casting into the concrete class.
044     */
045    @Override
046    protected void forwardReply(AbstractMRListener client, AbstractMRReply m) {
047        ((Z21Listener) client).reply((Z21Reply) m);
048    }
049
050    /**
051     * Invoked if it's appropriate to do low-priority polling of the command
052     * station, this should return the next message to send, or null if the TC
053     * should just sleep.
054     */
055    @Override
056    protected Z21Message pollMessage() {
057        return null;
058    }
059
060    @Override
061    protected Z21Listener pollReplyHandler() {
062        return null;
063    }
064
065    /**
066     * enterProgMode() and enterNormalMode() return any message that
067     * needs to be returned to the command station to change modes.
068     *
069     * @see #enterNormalMode()
070     * @return if no message is needed, you may return null.
071     *
072     * If the programmerIdle() function returns true, enterNormalMode() is
073     * called after a timeout while in IDLESTATE during programming to
074     * return the system to normal mode.
075     */
076    @Override
077    protected Z21Message enterProgMode() {
078        return null;
079    }
080
081    /**
082     * enterProgMode() and enterNormalMode() return any message that
083     * needs to be returned to the command station to change modes.
084     *
085     * @see #enterProgMode()
086     * @return if no message is needed, you may return null.
087     */
088    @Override
089    protected Z21Message enterNormalMode() {
090        return null;
091    }
092
093    /**
094     * Actually transmits the next message to the port.
095     */
096    @SuppressFBWarnings(value = {"TLW_TWO_LOCK_WAIT", "", "UW_UNCOND_WAIT"},
097            justification = "Two locks needed for synchronization here, this is OK; String + only used for debug, so inefficient String processing not really a problem; Unconditional Wait is to give external hardware, which doesn't necessarilly respond, time to process the data.")
098    @Override
099    synchronized protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) {
100        if (log.isDebugEnabled()) {
101            log.debug("forwardToPort message: [{}]", m);
102        }
103        // remember who sent this
104        mLastSender = reply;
105
106        // forward the message to the registered recipients,
107        // which includes the communications monitor, except the sender.
108        // Schedule notification via the Swing event queue to ensure order
109        Runnable r = new XmtNotifier(m, mLastSender, this);
110        javax.swing.SwingUtilities.invokeLater(r);
111
112        // stream to port in single write, as that's needed by serial
113        byte[] msg = new byte[lengthOfByteStream(m)];
114        // add header
115        int offset = addHeaderToOutput(msg, m);
116
117        // add data content
118        int len = m.getNumDataElements();
119        for (int i = 0; i < len; i++) {
120            msg[i + offset] = (byte) m.getElement(i);
121        }
122        // add trailer
123        addTrailerToOutput(msg, len + offset, m);
124        // and send the bytes
125        try {
126            if (log.isDebugEnabled()) {
127                StringBuilder f = new StringBuilder("formatted message: ");
128                for (byte b : msg) {
129                    f.append(Integer.toHexString(0xFF & b));
130                    f.append(" ");
131                }
132                log.debug(new String(f));
133            }
134            while (m.getRetries() >= 0) {
135                if (portReadyToSend(controller)) {
136                    // create a datagram with the data from the
137                    // message.
138                    byte[] data = ((Z21Message) m).getBuffer();
139                    DatagramPacket sendPacket
140                            = new DatagramPacket(data, ((Z21Message) m).getLength(), host, port);
141                    // and send it.
142                    ((Z21Adapter) controller).getSocket().send(sendPacket);
143                    log.debug("written, msg timeout: {} mSec", m.getTimeout());
144                    break;
145                } else if (m.getRetries() >= 0) {
146                    if (log.isDebugEnabled()) {
147                        StringBuilder b = new StringBuilder("Retry message: ");
148                        b.append(m.toString());
149                        b.append(" attempts remaining: ");
150                        b.append(m.getRetries());
151                        log.debug(new String(b));
152                    }
153                    m.setRetries(m.getRetries() - 1);
154                    try {
155                        synchronized (xmtRunnable) {
156                            xmtRunnable.wait(m.getTimeout());
157                        }
158                    } catch (InterruptedException e) {
159                        Thread.currentThread().interrupt(); // retain if needed later
160                        if(!threadStopRequest) {
161                           log.error("retry wait interrupted");
162                        } else {
163                           log.error("retry wait interrupted during thread stop");
164                        }
165                    }
166                } else {
167                    log.warn("sendMessage: port not ready for data sending: {}", java.util.Arrays.toString(msg));
168                }
169            }
170        } catch (Exception e) {
171            // TODO Currently there's no port recovery if an exception occurs
172            // must restart JMRI to clear xmtException.
173            xmtException = true;
174            portWarn(e);
175        }
176    }
177
178    @Override()
179    public boolean status() {
180        if (controller == null) {
181            return false;
182        } else {
183            return (controller.status());
184        }
185    }
186
187    /**
188     * Make connection to existing PortController object.
189     */
190    @Override
191    public void connectPort(AbstractPortController p) {
192        rcvException = false;
193        xmtException = false;
194        if (controller != null) {
195            log.warn("connectPort: connect called while connected");
196        } else {
197            log.debug("connectPort invoked");
198        }
199        if (!(p instanceof Z21Adapter)) {
200            throw new IllegalArgumentException("attempt to connect wrong port type");
201        }
202        controller = p;
203        try {
204            host = java.net.InetAddress.getByName(((Z21Adapter) controller).getHostName());
205            port = ((Z21Adapter) controller).getPort();
206            ConnectionStatus.instance().setConnectionState(
207                    p.getSystemConnectionMemo(), ConnectionStatus.CONNECTION_UP);
208        } catch (java.net.UnknownHostException uhe) {
209            log.error("Unknown Host: {} ", ((Z21Adapter) controller).getHostName());
210            ConnectionStatus.instance().setConnectionState(
211                    p.getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
212        }
213        // and start threads
214        xmtThread = new Thread(xmtRunnable = () -> {
215            try {
216                transmitLoop();
217            } catch (Throwable e) {
218                if(!threadStopRequest)
219                    log.error("Transmit thread terminated prematurely by: {}", e.toString(), e);
220            }
221        });
222        xmtThread.setName("z21.Z21TrafficController Transmit thread");
223        xmtThread.start();
224        rcvThread = new Thread(this::receiveLoop);
225        rcvThread.setName("z21.Z21TrafficController Receive thread");
226        int xr = rcvThread.getPriority();
227        xr++;
228        rcvThread.setPriority(xr);      //bump up the priority
229        rcvThread.start();
230    }
231
232    /**
233     * Break connection to existing PortController object. Once broken, attempts
234     * to send via "message" member will fail.
235     */
236    @Override
237    public void disconnectPort(AbstractPortController p) {
238        if (controller != p) {
239            log.warn("disconnectPort: disconnect called from non-connected AbstractPortController");
240        }
241        controller = null;
242    }
243
244    @Override
245    protected Z21Reply newReply() {
246        return new Z21Reply();
247    }
248
249    @Override
250    protected boolean endOfMessage(AbstractMRReply r) {
251        // since this is a UDP protocol, and each reply in the packet is complete,
252        // we don't check for end of message manually.
253        return true;
254    }
255
256    /**
257     * Handle each reply when complete.
258     * <p>
259     * (This is public for testing purposes) Runs in the "Receive" thread.
260     */
261    @SuppressFBWarnings(value = {"UW_UNCOND_WAIT", "WA_NOT_IN_LOOP", "NO_NOTIFY_NOT_NOTIFYALL"},
262            justification = "Wait is for external hardware, which doesn't necessarilly respond, to process the data.  Notify is used because Having more than one thread waiting on xmtRunnable is an error.")
263    @Override
264    public void handleOneIncomingReply() throws java.io.IOException {
265        // we sit in this until the message is complete, relying on
266        // threading to let other stuff happen
267
268        // create a buffer to hold the incoming data.
269        byte[] buffer = new byte[MAX_UDP_PAYLOAD];
270
271        // create the packet.
272        DatagramPacket receivePacket = new DatagramPacket(buffer, MAX_UDP_PAYLOAD, host, port);
273
274        // and wait to receive data in the packet.
275        try {
276            ((Z21Adapter) controller).getSocket().receive(receivePacket);
277        } catch (java.net.SocketException | NullPointerException se) {
278            // if we are waiting when the controller is disposed,
279            // a socket exception will be thrown.
280            log.debug("Socket exception during receive.  Connection Closed?");
281            rcvException = true;
282            return;
283        }
284        if (threadStopRequest) return;
285
286        // handle more than one reply in the same UDP packet.
287        List<Z21Reply> replies = new ArrayList<>();
288
289        int totalLength=receivePacket.getLength();
290        int consumed=0;
291
292        do {
293            int length = (0xff & buffer[0]) + ((0xff & buffer[1]) << 8);
294            Z21Reply msg = new Z21Reply(buffer, length);
295
296            replies.add(msg);
297
298            buffer = Arrays.copyOfRange(buffer,length,buffer.length);
299            consumed +=length;
300            log.trace("total length: {} consumed {}",totalLength,consumed);
301        } while(totalLength>consumed);
302
303
304        // and then dispatch each reply
305        replies.forEach(this::dispatchReply);
306    }
307
308    private void dispatchReply(Z21Reply msg) {
309        // message is complete, dispatch it !!
310        replyInDispatch = true;
311        if (log.isDebugEnabled()) {
312            log.debug("dispatch reply of length {} contains {} state {}", msg.getNumDataElements(), msg.toString(), mCurrentState);
313        }
314
315        // forward the message to the registered recipients,
316        // which includes the communications monitor
317        // return a notification via the Swing event queue to ensure proper thread
318        Runnable r = new RcvNotifier(msg, mLastSender, this);
319        try {
320            javax.swing.SwingUtilities.invokeAndWait(r);
321        } catch (InterruptedException ie) {
322            if(threadStopRequest) return;
323            log.error("Unexpected exception in invokeAndWait:{}", ie, ie);
324        } catch (Exception e) {
325            log.error("Unexpected exception in invokeAndWait:{}", e, e);
326        }
327        if (log.isDebugEnabled()) {
328            log.debug("dispatch thread invoked");
329        }
330
331        if (!msg.isUnsolicited()) {
332            // effect on transmit:
333            switch (mCurrentState) {
334                case WAITMSGREPLYSTATE: {
335                    // check to see if the response was an error message we want
336                    // to automatically handle by re-queueing the last sent
337                    // message, otherwise go on to the next message
338                    if (msg.isRetransmittableErrorMsg()) {
339                        if (log.isDebugEnabled()) {
340                            log.debug("Automatic Recovery from Error Message: +msg.toString()");
341                        }
342                        synchronized (xmtRunnable) {
343                            mCurrentState = AUTORETRYSTATE;
344                            replyInDispatch = false;
345                            xmtRunnable.notify();
346                        }
347                    } else {
348                        // update state, and notify to continue
349                        synchronized (xmtRunnable) {
350                            mCurrentState = NOTIFIEDSTATE;
351                            replyInDispatch = false;
352                            xmtRunnable.notify();
353                        }
354                    }
355                    break;
356                }
357                case WAITREPLYINPROGMODESTATE: {
358                    // entering programming mode
359                    mCurrentMode = PROGRAMINGMODE;
360                    replyInDispatch = false;
361
362                    // check to see if we need to delay to allow decoders to become
363                    // responsive
364                    int warmUpDelay = enterProgModeDelayTime();
365                    if (warmUpDelay != 0) {
366                        try {
367                            synchronized (xmtRunnable) {
368                                xmtRunnable.wait(warmUpDelay);
369                            }
370                        } catch (InterruptedException e) {
371                            Thread.currentThread().interrupt(); // retain if needed later
372                            if (threadStopRequest) return;
373                        }
374                    }
375                    // update state, and notify to continue
376                    synchronized (xmtRunnable) {
377                        mCurrentState = OKSENDMSGSTATE;
378                        xmtRunnable.notify();
379                    }
380                    break;
381                }
382                case WAITREPLYINNORMMODESTATE: {
383                    // entering normal mode
384                    mCurrentMode = NORMALMODE;
385                    replyInDispatch = false;
386                    // update state, and notify to continue
387                    synchronized (xmtRunnable) {
388                        mCurrentState = OKSENDMSGSTATE;
389                        xmtRunnable.notify();
390                    }
391                    break;
392                }
393                default: {
394                    replyInDispatch = false;
395                    if (allowUnexpectedReply) {
396                        if (log.isDebugEnabled()) {
397                            log.debug("Allowed unexpected reply received in state: {} was {}", mCurrentState, msg.toString());
398                        }
399                        synchronized (xmtRunnable) {
400                            // The transmit thread sometimes gets stuck
401                            // when unexpected replies are received.  Notify
402                            // it to clear the block without a timeout.
403                            // (do not change the current state)
404                            //if(mCurrentState!=IDLESTATE)
405                            xmtRunnable.notify();
406                        }
407                    } else {
408                        unexpectedReplyStateError(mCurrentState,msg.toString());
409                    }
410                }
411            }
412            // Unsolicited message
413        } else {
414            if (log.isDebugEnabled()) {
415                log.debug("Unsolicited Message Received {}", msg.toString());
416            }
417
418            replyInDispatch = false;
419        }
420    }
421
422    @SuppressFBWarnings(value = {"UW_UNCOND_WAIT", "WA_NOT_IN_LOOP"},
423            justification = "Wait is for external hardware, which doesn't necessarilly respond, to process the data.")
424    @Override
425    protected void terminate() {
426        if (controller == null) {
427            log.debug("terminate called while not connected");
428            return;
429        } else {
430            log.debug("Cleanup Starts");
431        }
432
433        Z21Message logoffMessage = Z21Message.getLanLogoffRequestMessage();
434        forwardToPort(logoffMessage, null);
435        // wait for reply
436        try {
437            if (xmtRunnable != null) {
438                synchronized (xmtRunnable) {
439                    xmtRunnable.wait(logoffMessage.getTimeout());
440                }
441            }
442        } catch (InterruptedException e) {
443            Thread.currentThread().interrupt(); // retain if needed later
444            log.error("transmit interrupted");
445        } finally {
446            // set the controller to null, even if terminate fails.
447            controller = null;
448        }
449    }
450
451    /**
452     * Terminate the receive and transmit threads.
453     * <p>
454     * This is intended to be used only by testing subclasses.
455     */
456    @Override
457    public void terminateThreads() {
458        threadStopRequest = true;
459        // ensure socket closed to end pending operations
460        if ( controller != null && ((Z21Adapter) controller).getSocket() != null) ((Z21Adapter) controller).getSocket().close();
461
462        // usual stop process
463        super.terminateThreads();
464    }
465
466    // The methods to implement the Z21Interface
467    @Override
468    public synchronized void addz21Listener(Z21Listener l) {
469        this.addListener(l);
470    }
471
472    @Override
473    public synchronized void removez21Listener(Z21Listener l) {
474        this.removeListener(l);
475    }
476
477    /**
478     * Forward a preformatted message to the actual interface.
479     */
480    @Override
481    public void sendz21Message(Z21Message m, Z21Listener reply) {
482        sendMessage(m, reply);
483    }
484
485    private static final Logger log = LoggerFactory.getLogger(Z21TrafficController.class);
486}