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