001package jmri.jmrix.lenz.liusbserver;
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
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;
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 {
036 static final int COMMUNICATION_TCP_PORT = 5550;
037 static final int BROADCAST_TCP_PORT = 5551;
038 static final String DEFAULT_IP_ADDRESS = "localhost";
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.
048 private BroadCastPortAdapter bcastAdapter = null;
049 private CommunicationPortAdapter commAdapter = null;
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;
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 }
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 }
101 }
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 }
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 }
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 }
129 @Override
130 public boolean status() {
131 return (pout != null && pin != null);
132 }
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);
144 this.getSystemConnectionMemo().setXNetTrafficController(packets);
146 // Start the threads that handle the network communication.
147 startCommThread();
148 startBCastThread();
150 new XNetInitializationManager()
151 .memo(this.getSystemConnectionMemo())
152 .setDefaults()
153 .versionCheck()
154 .setTimeout(30000)
155 .init();
156 }
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 }
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 }
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 }
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 }
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 }
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 }
284 /**
285 * Internal class for broadcast port connection
286 */
287 private static class BroadCastPortAdapter extends jmri.jmrix.AbstractNetworkPortController {
289 private final LIUSBServerAdapter parent;
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 }
299 @Override
300 public void configure() {
301 // no additional configuration required
302 }
304 @Override
305 public String getManufacturer() {
306 return this.parent.getManufacturer();
307 }
309 @Override
310 protected void resetupConnection() {
311 parent.startBCastThread();
312 }
314 @Override
315 public XNetSystemConnectionMemo getSystemConnectionMemo() {
316 return this.parent.getSystemConnectionMemo();
317 }
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 }
328 /**
329 * Internal class for communication port connection
330 */
331 private static class CommunicationPortAdapter extends jmri.jmrix.AbstractNetworkPortController {
333 private final LIUSBServerAdapter parent;
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 }
343 @Override
344 public void configure() {
345 // no additional configuration required
346 }
348 @Override
349 public String getManufacturer() {
350 return this.parent.getManufacturer();
351 }
353 @Override
354 protected void resetupConnection() {
355 parent.startCommThread();
356 }
358 @Override
359 public XNetSystemConnectionMemo getSystemConnectionMemo() {
360 return this.parent.getSystemConnectionMemo();
361 }
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 }
371 }
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 }
402 private static final Logger log = LoggerFactory.getLogger(LIUSBServerAdapter.class);
404}