001package jmri.jmrix.lenz.xntcp;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.DataInputStream;
006import java.io.DataOutputStream;
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.OutputStream;
010import java.net.DatagramPacket;
011import java.net.DatagramSocket;
012import java.net.InetAddress;
013import java.net.Socket;
014import java.net.SocketException;
015import java.net.UnknownHostException;
016import java.nio.charset.StandardCharsets;
017import java.util.Vector;
018
019import jmri.jmrix.ConnectionStatus;
020import jmri.jmrix.lenz.LenzCommandStation;
021import jmri.jmrix.lenz.XNetInitializationManager;
022import jmri.jmrix.lenz.XNetNetworkPortController;
023import jmri.jmrix.lenz.XNetTrafficController;
024
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * Provide access to XpressNet via a XnTcp interface attached on the Ethernet
030 * port.
031 *
032 * @author Giorgio Terdina Copyright (C) 2008-2011, based on LI100 adapter by
033 * Bob Jacobsen, Copyright (C) 2002
034 * @author Portions by Paul Bender, Copyright (C) 2003
035 * GT - May 2008 - Added possibility of manually
036 * defining the IP address and the TCP port number GT - May 2008 - Added
037 * updating of connection status in the main menu panel (using ConnectionStatus
038 * by Daniel Boudreau) PB - December 2010 - refactored to be based off of
039 * AbstractNetworkController. GT - May 2011 - Fixed problems arising from recent
040 * refactoring
041 */
042public class XnTcpAdapter extends XNetNetworkPortController {
043
044    static final int DEFAULT_UDP_PORT = 61234;
045    static final int DEFAULT_TCP_PORT = 61235;
046    static final String DEFAULT_IP_ADDRESS = "10.1.0.1";
047    static final int UDP_LENGTH = 18;   // Length of UDP packet
048    static final int BROADCAST_TIMEOUT = 1000;
049    static final int READ_TIMEOUT = 8000;
050    // Increasing MAX_PENDING_PACKETS makes output to CS faster, but may delay reception of unexpected notifications from CS
051    static final int MAX_PENDING_PACKETS = 15; // Allow a buffer of up to 128 bytes to be sent before waiting for acknowledgment
052    private static final String MANUAL = "Manual";
053
054    private Vector<String> hostNameVector = null;  // Contains the list of interfaces found on the LAN
055    private Vector<HostAddress> hostAddressVector = null; // Contains their IP and port numbers
056    private InputStream inTcpStream = null;
057    private OutputTcpStream outTcpStream = null;
058    private int pendingPackets = 0;   // Number of packets sent and not yet acknowledged
059    private String outName = MANUAL;  // Interface name, used for possible error messages (can be either the netBios name or the IP address)
060
061    public XnTcpAdapter() {
062        super();
063        option1Name = "XnTcpInterface"; // NOI18N
064        options.put(option1Name, new Option(Bundle.getMessage("XnTcpInterfaceLabel"), getInterfaces()));
065        m_HostName = DEFAULT_IP_ADDRESS;
066        m_port = DEFAULT_TCP_PORT;
067    }
068
069    // Internal class, used to keep track of IP and port number
070    // of each interface found on the LAN
071    private static class HostAddress {
072
073        private final String ipNumber;
074        private final int portNumber;
075
076        private HostAddress(String h, int p) {
077            ipNumber = h;
078            portNumber = p;
079        }
080    }
081
082    String[] getInterfaces() {
083        Vector<String> v = getInterfaceNames();
084        String[] a = new String[v.size() + 1];
085        for (int i = 0; i < v.size(); i++) {
086            a[i + 1] = v.elementAt(i);
087        }
088        a[0] = Bundle.getMessage(MANUAL);
089        return a;
090    }
091
092    public Vector<String> getInterfaceNames() {
093        // Return the list of XnTcp interfaces connected to the LAN
094        findInterfaces();
095        return hostNameVector;
096    }
097
098    @Override
099    public void connect() throws java.io.IOException {
100        // Connect to the choosen XpressNet/TCP interface
101        int ind;
102        // Retrieve XnTcp interface name from Option1
103        if (getOptionState(option1Name) != null) {
104            outName = getOptionState(option1Name);
105        }
106        // Did user manually provide IP number and port?
107        if (outName.equals(Bundle.getMessage(MANUAL)) || outName.equals(MANUAL)) {
108            // Yes - retrieve IP number and port
109            if (m_HostName == null) {
110                m_HostName = DEFAULT_IP_ADDRESS;
111            }
112            if (m_port == 0) {
113                m_port = DEFAULT_TCP_PORT;
114            }
115            outName = m_HostName;
116        } else {
117            // User specified a XnTcp interface name. Check if it's available on the LAN.
118            if (hostNameVector == null) {
119                findInterfaces();
120            }
121            if ((ind = hostNameVector.indexOf(outName)) < 0) {
122                throw (new IOException("XpressNet/TCP interface " + outName + " not found"));
123            }
124            // Interface card found. Get the relevantIP number and port
125            m_HostName = hostAddressVector.get(ind).ipNumber;
126            m_port = hostAddressVector.get(ind).portNumber;
127        }
128        try {
129            // Connect!
130            try {
131                socketConn = new Socket(m_HostName, m_port);
132                socketConn.setSoTimeout(READ_TIMEOUT);
133            } catch (UnknownHostException e) {
134                ConnectionStatus.instance().setConnectionState(
135                        this.getSystemConnectionMemo().getUserName(),
136                        outName, ConnectionStatus.CONNECTION_DOWN);
137                throw (e);
138            }
139            // get and save input stream
140            inTcpStream = socketConn.getInputStream();
141
142            // purge contents, if any
143            purgeStream(inTcpStream);
144
145            // Connection established.
146            opened = true;
147            ConnectionStatus.instance().setConnectionState(
148                        this.getSystemConnectionMemo().getUserName(),
149                        outName, ConnectionStatus.CONNECTION_UP);
150
151        } // Report possible errors encountered while opening the connection
152        catch (SocketException se) {
153            log.error("Socket exception while opening TCP connection with {} trace follows", outName, se);
154            ConnectionStatus.instance().setConnectionState(
155                        this.getSystemConnectionMemo().getUserName(),
156                        outName, ConnectionStatus.CONNECTION_DOWN);
157            throw (se);
158        }
159        catch (IOException e) {
160            log.error("Unexpected exception while opening TCP connection with {} trace follows", outName, e);
161            ConnectionStatus.instance().setConnectionState(
162                        this.getSystemConnectionMemo().getUserName(),
163                        outName, ConnectionStatus.CONNECTION_DOWN);
164            throw (e);
165        }
166    }
167
168    /**
169     * Retrieve all XnTcp interfaces available on the network
170 by broadcasting a UDP request on port 61234, listening
171 to all possible replies, storing in hostNameVector
172 the NETBIOS names of interfaces found and their IP
173 and port numbers in hostAddressVector.
174     */
175    private void findInterfaces() {
176
177        DatagramSocket udpSocket = null;
178
179        hostNameVector = new Vector<>(10, 1);
180        hostAddressVector = new Vector<>(10, 1);
181
182        try {
183            byte[] udpBuffer = new byte[UDP_LENGTH];
184            // Create a UDP socket
185            udpSocket = new DatagramSocket();
186            // Prepare the output message (it should contain ASCII '%')
187            udpBuffer[0] = 0x25;
188            DatagramPacket udpPacket = new DatagramPacket(udpBuffer, 1, InetAddress.getByName("255.255.255.255"), DEFAULT_UDP_PORT);
189            // Broadcast the request
190            udpSocket.send(udpPacket);
191            // Set a timeout limit for replies
192            udpSocket.setSoTimeout(BROADCAST_TIMEOUT);
193            // Loop listening until timeout occurs
194            while (true) {
195                // Wait for a reply
196                udpPacket.setLength(UDP_LENGTH);
197                udpSocket.receive(udpPacket);
198                // Reply received, make sure that we got all data
199                if (udpPacket.getLength() >= UDP_LENGTH) {
200                    // Retrieve the NETBIOS name of the interface
201                    hostNameVector.addElement((new String(udpBuffer, 0, 16, StandardCharsets.US_ASCII)).trim());
202                    // Retrieve the IP and port numbers of the interface
203                    hostAddressVector.addElement(new HostAddress(cleanIP((udpPacket.getAddress()).getHostAddress()),
204                            ((udpBuffer[16]) & 0xff) * 256 + ((udpBuffer[17]) & 0xff)));
205                }
206            }
207        } // When timeout or any error occurs, simply exit the loop // When timeout or any error occurs, simply exit the loop
208        catch (IOException e) {
209            log.debug("Exception occured",e);
210        } finally {
211            // Before exiting, release resources
212            if (udpSocket != null) {
213                udpSocket.close();
214                udpSocket = null;
215            }
216        }
217    }
218
219    /**
220     * TCP/IP stack and the XnTcp interface provide enough buffering to avoid
221     * overrun. However, queueing commands faster than they can be processed
222     * should in general be avoided.
223     * <p>
224     * To this purpose, a counter is incremented
225     * each time a packet is queued and decremented when a reply from the
226     * interface is received. When the counter reaches the pre-defined maximum
227     * (e.g. 15) queuing of commands is blocked. Owing to broadcasts from the
228     * command station, the number of commands received can actually be higher
229     * than that of commands sent, but this fact simply implies that we may have
230     * a higher number of pending commands for a while, without any negative
231     * consequence (the maximum is however arbitrary).
232     *
233     * @param s number to send
234     */
235    protected synchronized void xnTcpSetPendingPackets(int s) {
236        pendingPackets += s;
237        if (pendingPackets < 0) {
238            pendingPackets = 0;
239        }
240    }
241
242    /**
243     * If an error occurs, either in the input or output thread, set the
244     * connection status to disconnected. This status will be reset once a
245     * TCP/IP connection is re-established via the reconnection routines defined
246     * in the parent classes.
247     */
248    protected synchronized void xnTcpError() {
249        // If the error message was already posted, simply ignore this call
250        if (opened) {
251            ConnectionStatus.instance().setConnectionState(
252                        this.getSystemConnectionMemo().getUserName(),
253                        outName, ConnectionStatus.CONNECTION_DOWN);
254            // Clear open status, in order to avoid issuing the error
255            // message more than than once.
256            opened = false;
257            log.debug("XnTcpError: TCP/IP communication dropped");
258        }
259    }
260
261    /**
262     * Can the port accept additional characters? There is no CTS signal
263     * available. We only limit the number of commands queued in TCP/IP stack
264     */
265    @Override
266    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
267            justification = "Further investigation is needed to handle this correctly")
268    public boolean okToSend() {
269        // If a communication error occurred, return always "true" in order to avoid program hang-up while quitting
270        if (!opened) {
271            return true;
272        }
273        synchronized (this) {
274            // Return "true" if the maximum number of commands queued has not been reached
275            log.debug("XnTcpAdapter.okToSend = {} (pending packets = {})", (pendingPackets < MAX_PENDING_PACKETS), pendingPackets);
276            return pendingPackets < MAX_PENDING_PACKETS;
277        }
278    }
279
280    /**
281     * Set up all of the other objects to operate with a XnTcp interface.
282     */
283    @Override
284    public void configure() {
285        // connect to a packetizing traffic controller
286        XNetTrafficController packets = new XnTcpXNetPacketizer(new LenzCommandStation());
287        packets.connectPort(this);
288        this.getSystemConnectionMemo().setXNetTrafficController(packets);
289        new XNetInitializationManager()
290                .memo(this.getSystemConnectionMemo())
291                .setDefaults()
292                .versionCheck()
293                .setTimeout(30000)
294                .init();
295    }
296
297// Base class methods for the XNetNetworkPortController interface
298
299    @Override
300    public DataInputStream getInputStream() {
301        if (!opened) {
302            log.error("getInputStream called before load(), stream not available");
303            return null;
304        }
305        return new DataInputStream(inTcpStream);
306    }
307
308    @Override
309    public DataOutputStream getOutputStream() {
310        if (!opened) {
311            log.error("getOutputStream called before load(), stream not available");
312        }
313        try {
314            outTcpStream = (new OutputTcpStream(socketConn.getOutputStream()));
315            return new DataOutputStream(outTcpStream);
316        } catch (java.io.IOException e) {
317            log.error("getOutputStream exception: {}",e.getMessage());
318        }
319        return null;
320    }
321
322    @Override
323    public boolean status() {
324        return opened;
325    }
326
327    /**
328     * Extract the IP number from a URL, by removing the
329     * domain name, if present.
330     */
331    private static String cleanIP(String ip) {
332        String outIP = ip;
333        int i = outIP.indexOf('/');
334        if ((i >= 0) && (i < (outIP.length() - 2))) {
335            outIP = outIP.substring(i + 1);
336        }
337        return outIP;
338    }
339
340    /**
341     * Output class, used to count output packets and make sure that
342     * they are immediatelly sent.
343     */
344    public class OutputTcpStream extends OutputStream {
345
346        private OutputStream tcpOut = null;
347        private int count;
348
349        public OutputTcpStream() {
350        }
351
352        public OutputTcpStream(OutputStream out) {
353            tcpOut = out; // Save the handle to the actual output stream
354            count = -1; // First byte should contain packet's length
355        }
356
357        @Override
358        public void write(int b) throws java.io.IOException {
359            // Make sure that we don't interleave bytes, if called
360            // at the same time by different threads
361            synchronized (tcpOut) {
362                try {
363                    tcpOut.write(b);
364                    if(log.isDebugEnabled()) {
365                        log.debug("XnTcpAdapter: sent {}", Integer.toHexString(b & 0xff));
366                    }
367                    // If this is the start of a new packet, save its length
368                    if (count < 0) {
369                        count = b & 0x0f;
370                    } // If the whole packet was queued, send it and count it
371                    else if (count-- == 0) {
372                        tcpOut.flush();
373                        log.debug("XnTcpAdapter: flush ");
374                        xnTcpSetPendingPackets(1);
375                    }
376                } catch (java.io.IOException e) {
377                    xnTcpError();
378                    throw e;
379                }
380            }
381        }
382
383        @Override
384        public void write(byte[] b, int off, int len) throws java.io.IOException {
385            // Make sure that we don't mix bytes of different packets,
386            // if called at the same time by different threads
387            synchronized (tcpOut) {
388                while (len-- > 0) {
389                    write((b[off++]) & 0xff);
390                }
391            }
392        }
393
394        public void write(byte[] b, int len) throws java.io.IOException {
395            write(b, 0, len);
396        }
397    }
398
399    private static final Logger log = LoggerFactory.getLogger(XnTcpAdapter.class);
400
401}