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}