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}