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}