001package jmri.jmrix.lenz.liusbserver;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.BufferedReader;
006import java.io.DataInputStream;
007import java.io.DataOutputStream;
008import java.io.IOException;
009import java.io.InputStreamReader;
010import java.io.PipedInputStream;
011import java.io.PipedOutputStream;
012import java.nio.charset.StandardCharsets;
013import jmri.jmrix.ConnectionStatus;
014import jmri.jmrix.lenz.LenzCommandStation;
015import jmri.jmrix.lenz.XNetInitializationManager;
016import jmri.jmrix.lenz.XNetNetworkPortController;
017import jmri.jmrix.lenz.XNetReply;
018import jmri.jmrix.lenz.XNetSystemConnectionMemo;
019import jmri.jmrix.lenz.XNetTrafficController;
020import jmri.util.ImmediatePipedOutputStream;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * Provide access to XpressNet via a the Lenz LIUSB Server. NOTES: The LIUSB
026 * server binds only to localhost (127.0.0.1) on TCP ports 5550 and 5551. Port
027 * 5550 is used for general communication. Port 5551 is used for broadcast
028 * messages only. The LIUSB Server disconnects both ports if there is 60 seconds
029 * of inactivity on the port. The LIUSB Server disconnects port 5550 if another
030 * device puts the system into service mode.
031 *
032 * @author Paul Bender (C) 2009-2010
033 */
034public class LIUSBServerAdapter extends XNetNetworkPortController {
035
036    static final int COMMUNICATION_TCP_PORT = 5550;
037    static final int BROADCAST_TCP_PORT = 5551;
038    static final String DEFAULT_IP_ADDRESS = "localhost";
039
040    private java.util.TimerTask keepAliveTimer; // Timer used to periodically
041    // send a message to both
042    // ports to keep the ports
043    // open
044    private static final int keepAliveTimeoutValue = 30000; // Interval
045    // to send a message
046    // Must be < 60s.
047
048    private BroadCastPortAdapter bcastAdapter = null;
049    private CommunicationPortAdapter commAdapter = null;
050
051    private DataOutputStream pout = null; // for output to other classes
052    private DataInputStream pin = null; // for input from other classes
053    // internal ends of the pipe
054    private DataOutputStream outpipe = null;  // feed pin
055    private Thread commThread;
056    private Thread bcastThread;
057
058    public LIUSBServerAdapter() {
059        super();
060        option1Name = "BroadcastPort"; // NOI18N
061        options.put(option1Name, new Option(Bundle.getMessage("BroadcastPortLabel"),
062                new String[]{String.valueOf(LIUSBServerAdapter.BROADCAST_TCP_PORT), ""}));
063        this.manufacturerName = jmri.jmrix.lenz.LenzConnectionTypeList.LENZ;
064    }
065
066    @Override
067    public synchronized void connect() throws java.io.IOException {
068        opened = false;
069        log.debug("connect called");
070        // open the port in XpressNet mode
071        try {
072            bcastAdapter = new BroadCastPortAdapter(this);
073            commAdapter = new CommunicationPortAdapter(this);
074            bcastAdapter.connect();
075            commAdapter.connect();
076            pout = commAdapter.getOutputStream();
077            PipedOutputStream tempPipeO = new ImmediatePipedOutputStream();
078            outpipe = new DataOutputStream(tempPipeO);
079            pin = new DataInputStream(new PipedInputStream(tempPipeO));
080            opened = true;
081        } catch (java.io.IOException e) {
082            log.error("init (pipe): Exception",e);
083            ConnectionStatus.instance().setConnectionState(
084                        this.getSystemConnectionMemo().getUserName(),
085                        m_HostName, ConnectionStatus.CONNECTION_DOWN);
086            throw e; // re-throw so this can be seen externally.
087        } catch (Exception ex) {
088            log.error("init (connect): Exception", ex);
089            ConnectionStatus.instance().setConnectionState(
090                        this.getSystemConnectionMemo().getUserName(),
091                        m_HostName, ConnectionStatus.CONNECTION_DOWN);
092            throw ex; // re-throw so this can be seen externally.
093        }
094        keepAliveTimer();
095        if (opened) {
096            ConnectionStatus.instance().setConnectionState(
097                    this.getSystemConnectionMemo().getUserName(),
098                    m_HostName, ConnectionStatus.CONNECTION_UP);
099        }
100
101    }
102
103    /**
104     * Can the port accept additional characters? return true if the port is
105     * opened.
106     */
107    @Override
108    public boolean okToSend() {
109        return (super.okToSend() && status());
110    }
111
112    // base class methods for the XNetNetworkPortController interface
113    @Override
114    public DataInputStream getInputStream() {
115        if (pin == null) {
116            log.error("getInputStream called before load(), stream not available");
117        }
118        return pin;
119    }
120
121    @Override
122    public DataOutputStream getOutputStream() {
123        if (pout == null) {
124            log.error("getOutputStream called before load(), stream not available");
125        }
126        return pout;
127    }
128
129    @Override
130    public boolean status() {
131        return (pout != null && pin != null);
132    }
133
134    /**
135     * Set up all of the other objects to operate with a LIUSB Server interface.
136     */
137    @Override
138    public void configure() {
139        log.debug("configure called");
140        // connect to a packetizing traffic controller
141        XNetTrafficController packets = (new LIUSBServerXNetPacketizer(new LenzCommandStation()));
142        packets.connectPort(this);
143
144        this.getSystemConnectionMemo().setXNetTrafficController(packets);
145
146        // Start the threads that handle the network communication.
147        startCommThread();
148        startBCastThread();
149
150        new XNetInitializationManager()
151                .memo(this.getSystemConnectionMemo())
152                .setDefaults()
153                .versionCheck()
154                .setTimeout(30000)
155                .init();
156    }
157
158    /**
159     * Start the Communication port thread.
160     */
161    private void startCommThread() {
162        commThread = new Thread(() -> { // start a new thread
163                // this thread has one task.  It repeatedly reads from the two
164                // incomming network connections and writes the resulting
165                // messages from the network ports and writes any data
166                // received to the output pipe.
167                log.debug("Communication Adapter Thread Started");
168                XNetReply r;
169                BufferedReader bufferedin
170                        = new BufferedReader(
171                                new InputStreamReader(commAdapter.getInputStream(),
172                                        StandardCharsets.UTF_8));
173                for (;;) {
174                    try {
175                        synchronized (commAdapter) {
176                            r = loadChars(bufferedin);
177                        }
178                    } catch (java.io.IOException e) {
179                        // start the process of trying to recover from
180                        // a failed connection.
181                        commAdapter.recover();
182                        break; // then exit the for loop.
183                    }
184                    log.debug("Network Adapter Received Reply: {}",r);
185                    writeReply(r);
186                }
187            });
188        commThread.start();
189    }
190
191    /**
192     * Start the Broadcast Port thread.
193     */
194    private void startBCastThread() {
195        bcastThread = new Thread(() -> { // start a new thread
196                // this thread has one task.  It repeatedly reads from the two
197                // incomming network connections and writes the resulting
198                // messages from the network ports and writes any data received
199                // to the output pipe.
200                log.debug("Broadcast Adapter Thread Started");
201                XNetReply r;
202                BufferedReader bufferedin
203                        = new BufferedReader(
204                                new InputStreamReader(bcastAdapter.getInputStream(),
205                                        StandardCharsets.UTF_8));
206                for (;;) {
207                    try {
208                        synchronized (bcastAdapter) {
209                            r = loadChars(bufferedin);
210                        }
211                    } catch (java.io.IOException e) {
212                        // start the process of trying to recover from
213                        // a failed connection.
214                        bcastAdapter.recover();
215                        break; // then exit the for loop.
216                    }
217                    if (log.isDebugEnabled()) {
218                        log.debug("Network Adapter Received Reply: {}", r.toString());
219                    }
220                    r.setUnsolicited(); // Anything coming through the
221                    // broadcast port is an
222                    // unsolicited message.
223                    writeReply(r);
224                }
225            });
226        bcastThread.start();
227    }
228
229    private synchronized void writeReply(XNetReply r) {
230        log.debug("Write reply to outpipe: {}", r);
231        int i;
232        int len = (r.getElement(0) & 0x0f) + 2;  // opCode+Nbytes+ECC
233        for (i = 0; i < len; i++) {
234            try {
235                outpipe.writeByte((byte) r.getElement(i));
236            } catch (java.io.IOException ex) {
237            }
238        }
239    }
240
241    /**
242     * Get characters from the input source, and file a message.
243     * <p>
244     * Returns only when the message is complete.
245     * <p>
246     * Only used in the Receive thread.
247     *
248     * @param istream character source.
249     * @throws IOException when presented by the input source.
250     * @return filled out message from source
251     */
252    private XNetReply loadChars(java.io.BufferedReader istream) throws java.io.IOException {
253        // The LIUSBServer sends us data as strings of hex values.
254        // These hex values are followed by a <cr><lf>
255        String s;
256        s = istream.readLine();
257        log.debug("Received from port: {}", s);
258        if (s == null) {
259            return null;
260        } else {
261            return new XNetReply(s);
262        }
263    }
264
265    /**
266     * This is called when a connection is initially lost. For this connection,
267     * it calls the default recovery method for both of the internal adapters.
268     */
269    @Override
270    public synchronized void recover() {
271        bcastAdapter.recover();
272        commAdapter.recover();
273    }
274
275    /**
276     * Customizable method to deal with resetting a system connection after a
277     * successful recovery of a connection.
278     */
279    @Override
280    protected void resetupConnection() {
281        this.getSystemConnectionMemo().getXNetTrafficController().connectPort(this);
282    }
283
284    /**
285     * Internal class for broadcast port connection
286     */
287    private static class BroadCastPortAdapter extends jmri.jmrix.AbstractNetworkPortController {
288
289        private final LIUSBServerAdapter parent;
290
291        public BroadCastPortAdapter(LIUSBServerAdapter p) {
292            super(p.getSystemConnectionMemo());
293            parent = p;
294            allowConnectionRecovery = true;
295            setHostName(DEFAULT_IP_ADDRESS);
296            setPort(BROADCAST_TCP_PORT);
297        }
298
299        @Override
300        public void configure() {
301            // no additional configuration required
302        }
303
304        @Override
305        public String getManufacturer() {
306            return this.parent.getManufacturer();
307        }
308
309        @Override
310        protected void resetupConnection() {
311            parent.startBCastThread();
312        }
313
314        @Override
315        public XNetSystemConnectionMemo getSystemConnectionMemo() {
316            return this.parent.getSystemConnectionMemo();
317        }
318
319        @Override
320        @SuppressFBWarnings(value="OVERRIDING_METHODS_MUST_INVOKE_SUPER", 
321                justification="this object does not own SystemConnectionMemo")
322        public void dispose() {
323            // override to prevent super class from disposing of the
324            // SystemConnectionMemo since this object does not own it
325        }
326    }
327
328    /**
329     * Internal class for communication port connection
330     */
331    private static class CommunicationPortAdapter extends jmri.jmrix.AbstractNetworkPortController {
332
333        private final LIUSBServerAdapter parent;
334
335        public CommunicationPortAdapter(LIUSBServerAdapter p) {
336            super(p.getSystemConnectionMemo());
337            parent = p;
338            allowConnectionRecovery = true;
339            setHostName(DEFAULT_IP_ADDRESS);
340            setPort(COMMUNICATION_TCP_PORT);
341        }
342
343        @Override
344        public void configure() {
345            // no additional configuration required
346        }
347
348        @Override
349        public String getManufacturer() {
350            return this.parent.getManufacturer();
351        }
352
353        @Override
354        protected void resetupConnection() {
355            parent.startCommThread();
356        }
357
358        @Override
359        public XNetSystemConnectionMemo getSystemConnectionMemo() {
360            return this.parent.getSystemConnectionMemo();
361        }
362
363        @Override
364        @SuppressFBWarnings(value="OVERRIDING_METHODS_MUST_INVOKE_SUPER", 
365                justification="this object does not own SystemConnectionMemo")
366        public void dispose() {
367            // override to prevent super class from disposing of the
368            // SystemConnectionMemo since this object does not own it
369        }
370
371    }
372
373    /*
374     * Set up the keepAliveTimer, and start it.
375     */
376    private void keepAliveTimer() {
377        if (keepAliveTimer == null) {
378            keepAliveTimer = new java.util.TimerTask(){
379                @Override
380                public void run () {
381                    /* If the timer times out, just send a character to the
382                     *  ports.
383                     */
384                    try {
385                        bcastAdapter.getOutputStream().write('z');
386                        commAdapter.getOutputStream().write('z');
387                    } catch (java.io.IOException ex) {
388                        //We need to do something here, because the
389                        //communication port drops when another device
390                        //puts the command station into service mode.
391                        log.error("Communications port dropped", ex);
392                    }
393                }
394            };
395        }
396        else {
397           keepAliveTimer.cancel();
398        }
399        jmri.util.TimerUtil.schedule(keepAliveTimer,keepAliveTimeoutValue,keepAliveTimeoutValue);
400    }
401
402    private static final Logger log = LoggerFactory.getLogger(LIUSBServerAdapter.class);
403
404}