001package jmri.jmrix.loconet.uhlenbrock; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.util.Calendar; 005import java.util.concurrent.ConcurrentLinkedQueue; 006import jmri.jmrix.loconet.LnPacketizer; 007import jmri.jmrix.loconet.LocoNetMessage; 008import jmri.jmrix.loconet.LocoNetMessageException; 009import jmri.jmrix.loconet.LocoNetSystemConnectionMemo; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * Converts Stream-based I/O to/from LocoNet messages. The "LocoNetInterface" 015 * side sends/receives LocoNetMessage objects. The connection to a 016 * LnPortController is via a pair of *Streams, which then carry sequences of 017 * characters for transmission. 018 * <p> 019 * Messages come to this via the main GUI thread, and are forwarded back to 020 * listeners in that same thread. Reception and transmission are handled in 021 * dedicated threads by RcvHandler and XmtHandler objects. Those are internal 022 * classes defined here. The thread priorities are: 023 * <ul> 024 * <li> RcvHandler - at highest available priority 025 * <li> XmtHandler - down one, which is assumed to be above the GUI 026 * <li> (everything else) 027 * </ul> 028 * 029 * Some of the message formats used in this class are Copyright Digitrax, Inc. 030 * and used with permission as part of the JMRI project. That permission does 031 * not extend to uses in other software products. If you wish to use this code, 032 * algorithm or these message formats outside of JMRI, please contact Digitrax 033 * Inc for separate permission. 034 * 035 * @author Bob Jacobsen Copyright (C) 2001, 2010 036 */ 037public class UhlenbrockPacketizer extends LnPacketizer { 038 039 @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", 040 justification = "Only used during system initialization") 041 public UhlenbrockPacketizer(LocoNetSystemConnectionMemo m) { 042 super(m); 043 log.debug("UhlenbrockPacketizer instantiated"); 044 } 045 046 public static final int NOTIFIEDSTATE = 15; // xmt notified, will next wake 047 public static final int WAITMSGREPLYSTATE = 25; // xmt has sent, await reply to message 048 049 static int defaultWaitTimer = 10000; 050 051 /** 052 * Forward a preformatted LocoNetMessage to the actual interface. 053 * 054 * Checksum is computed and overwritten here, then the message is converted 055 * to a byte array and queued for transmission. 056 * 057 * @param m Message to send; will be updated with CRC 058 * @param requestIgnoreEcho If true: Notify listeners on enqueing message, ignore echo from line. 059 * Only in effect if preference "LoconetUpdateSlotOnMessageCreation" is set. 060 */ 061 @Override 062 public void sendLocoNetMessage(LocoNetMessage m, boolean requestIgnoreEcho) { 063 log.debug("add to queue message {}", m.toString()); 064 // update statistics 065 transmittedMsgCount++; 066 067 // set the error correcting code byte(s) before transmittal 068 m.setParity(); 069 070 // stream to port in single write, as that's needed by serial 071 int len = m.getNumDataElements(); 072 byte msg[] = new byte[len]; 073 for (int i = 0; i < len; i++) { 074 msg[i] = (byte) m.getElement(i); 075 } 076 077 log.trace("queue LocoNet packet: {}", m.toString()); 078 // queue the request 079 try { 080 xmtLocoNetList.add(m); // done first to make sure it's there before xmtList has an element 081 xmtList.add(msg); 082 // save to queue if we want to remember it to check in receive handler 083 if (mLoconetUpdateSlotOnMessageCreation && requestIgnoreEcho) { 084 log.trace("add LocoNet packet {} to sentList. Now {} packets in sentList.", m, sentList.size()); 085 sentList.add(m); 086 log.trace("queue message for notification: {}", m); 087 jmri.util.ThreadingUtil.runOnLayoutEventually(new RcvMemo(m, this)); 088 } 089 } catch (RuntimeException e) { 090 log.warn("passing to xmit: unexpected exception: ", e); 091 } 092 } 093 094 /** 095 * Synchronized list used as a transmit queue. 096 * <p> 097 * This is public to allow access from the internal class(es) when compiling 098 * with Java 1.1 099 */ 100 public ConcurrentLinkedQueue<LocoNetMessage> xmtLocoNetList = new ConcurrentLinkedQueue<>(); 101 102 /** 103 * Captive class to handle incoming characters. This is a permanent loop, 104 * looking for input messages in character form on the stream connected to 105 * the LnPortController via <code>connectPort</code>. 106 */ 107 class RcvHandler implements Runnable { 108 109 /** 110 * Remember the LnPacketizer object. 111 */ 112 LnPacketizer trafficController; 113 114 public RcvHandler(LnPacketizer lt) { 115 trafficController = lt; 116 } 117 118 @Override 119 public void run() { 120 121 int opCode; 122 while (true) { // loop permanently, program close will exit 123 try { 124 // start by looking for command - skip if bit not set 125 int inbyte = readByteProtected(istream) & 0xFF; 126 while (((opCode = (inbyte)) & 0x80) == 0) { 127 log.debug("Skipping: {}", Integer.toHexString(opCode)); 128 inbyte = readByteProtected(istream) & 0xFF; 129 } 130 // here opCode is OK. Create output message 131 log.debug("Start message with opcode: {}", Integer.toHexString(opCode)); 132 LocoNetMessage msg = null; 133 while (msg == null) { 134 try { 135 // Capture 2nd byte, always present 136 int byte2 = readByteProtected(istream) & 0xFF; 137 //log.debug("Byte2: "+Integer.toHexString(byte2)); 138 if ((byte2 & 0x80) != 0) { 139 log.warn("LocoNet message with opCode: {} ended early. Byte2 is also an opcode: {}", Integer.toHexString(opCode), Integer.toHexString(byte2)); 140 opCode = byte2; 141 throw new LocoNetMessageException(); 142 } 143 144 // Decide length 145 switch ((opCode & 0x60) >> 5) { 146 case 0: 147 /* 2 byte message */ 148 149 msg = new LocoNetMessage(2); 150 break; 151 152 case 1: 153 /* 4 byte message */ 154 155 msg = new LocoNetMessage(4); 156 break; 157 158 case 2: 159 /* 6 byte message */ 160 161 msg = new LocoNetMessage(6); 162 break; 163 164 case 3: 165 /* N byte message */ 166 167 if (byte2 < 2) { 168 log.error("LocoNet message length invalid: {} opcode: {}", byte2, Integer.toHexString(opCode)); 169 } 170 msg = new LocoNetMessage(byte2); 171 break; 172 default: // can't happen with this code, but just in case... 173 throw new LocoNetMessageException("decode failure " + byte2); 174 } 175 // message exists, now fill it 176 msg.setOpCode(opCode); 177 msg.setElement(1, byte2); 178 int len = msg.getNumDataElements(); 179 //log.debug("len: "+len); 180 for (int i = 2; i < len; i++) { 181 // check for message-blocking error 182 int b = readByteProtected(istream) & 0xFF; 183 //log.debug("char "+i+" is: "+Integer.toHexString(b)); 184 if ((b & 0x80) != 0) { 185 log.warn("LocoNet message with opCode: {} ended early. Expected length: {} seen length: {} unexpected byte: {}", Integer.toHexString(opCode), len, i, Integer.toHexString(b)); 186 opCode = b; 187 throw new LocoNetMessageException(); 188 } 189 msg.setElement(i, b); 190 } 191 } catch (LocoNetMessageException e) { 192 // retry by going around again 193 // opCode is set for the newly-started packet 194 msg = null; 195 continue; 196 } 197 } 198 // check parity 199 if (!msg.checkParity()) { 200 log.warn("Ignore LocoNet packet with bad checksum: {}", msg.toString()); 201 throw new LocoNetMessageException(); 202 } 203 204 synchronized (xmtHandler) { 205 if (mCurrentState == WAITMSGREPLYSTATE && msg.equals(lastMessage)) { 206 log.debug("We have our returned message and can send back out our next instruction"); 207 mCurrentState = NOTIFIEDSTATE; 208 xmtHandler.notify(); 209 } 210 } 211 212 // message is complete, dispatch it !! 213 log.trace("message complete: {}", msg); 214 215 // check if this message was supposed to be ignored 216 // sentList will be empty if preference "LoconetUpdateSlotOnMessageCreation" is not activated 217 if(trafficController.getSentList().contains(msg)) { 218 trafficController.getSentList().remove(msg); 219 log.trace("found packet {} in sentList, ignoring. {} packets in sentList remaining.", msg, trafficController.getSentList().size()); 220 } 221 else { 222 log.trace("queue message for notification: {}", msg); 223 224 final LocoNetMessage thisMsg = msg; 225 final LnPacketizer thisTc = trafficController; 226 // return a notification via the queue to ensure end 227 Runnable r = new Runnable() { 228 LocoNetMessage msgForLater = thisMsg; 229 LnPacketizer myTc = thisTc; 230 231 @Override 232 public void run() { 233 myTc.notify(msgForLater); 234 } 235 }; 236 javax.swing.SwingUtilities.invokeLater(r); 237 } 238 239 // done with this one 240 } catch (LocoNetMessageException e) { 241 // just let it ride for now 242 log.warn("run: unexpected LocoNetMessageException: ", e); 243 } catch (java.io.EOFException | java.io.InterruptedIOException e) { 244 // posted from idle port when enableReceiveTimeout used 245 // Normal condition, go around the loop again 246 continue; 247 } catch (java.io.IOException e) { 248 // fired when write-end of HexFile reaches end 249 log.debug("IOException, should only happen with HexFile", e); 250 log.debug("End of file"); 251 disconnectPort(controller); 252 return; 253 } catch (RuntimeException e) { 254 // normally, we don't catch RuntimeException, but in this 255 // permanently running loop it seems wise. 256 log.warn("run: unexpected Exception", e); // NOI18N 257 continue; 258 } 259 } // end of permanent loop 260 } 261 } 262 263 LocoNetMessage lastMessage; 264 265 /** 266 * Captive class to handle transmission 267 */ 268 class XmtHandler implements Runnable { 269 270 @Override 271 public void run() { 272 273 while (true) { // loop permanently 274 // any input? 275 try { 276 // get content; blocks until present 277 log.debug("check for input"); 278 byte msg[] = null; 279 lastMessage = null; 280 msg = xmtList.take(); 281 lastMessage = xmtLocoNetList.remove(); // done second to make sure xmlList had an element 282 283 //log.debug("-------------------Uhlenbrock IB-COM LocoNet message to SEND: {}", msg.toString()); 284 285 // input - now send 286 try { 287 if (ostream != null) { 288 if (!controller.okToSend()) { 289 log.debug("LocoNet port not ready to receive"); 290 } 291 log.debug("start write to stream"); 292 while (!controller.okToSend()) { 293 Thread.yield(); 294 } 295 synchronized (xmtHandler) { 296 mCurrentState = WAITMSGREPLYSTATE; 297 } 298 ostream.write(msg); 299 ostream.flush(); 300 log.debug("end write to stream"); 301 messageTransmitted(msg); 302 transmitWait(defaultWaitTimer, WAITMSGREPLYSTATE); 303 } else { 304 // no stream connected 305 log.warn("sendLocoNetMessage: no connection established"); 306 } 307 } catch (java.io.IOException e) { 308 log.warn("sendLocoNetMessage: IOException: {}", e.toString()); 309 } 310 } catch (InterruptedException ie) { 311 return; // ending the thread 312 } 313 } 314 } 315 } 316 317 protected void transmitWait(int waitTime, int state/*, String InterruptMessage*/) { 318 // wait() can have spurious wakeup! 319 // so we protect by making sure the entire timeout time is used 320 long currentTime = Calendar.getInstance().getTimeInMillis(); 321 long endTime = currentTime + waitTime; 322 while (endTime > (currentTime = Calendar.getInstance().getTimeInMillis())) { 323 long wait = endTime - currentTime; 324 try { 325 synchronized (xmtHandler) { 326 // Do not wait if the current state has changed since we 327 // last set it. 328 if (mCurrentState != state) { 329 return; 330 } 331 xmtHandler.wait(wait); // rcvr normally ends this w state change 332 } 333 } catch (InterruptedException e) { 334 Thread.currentThread().interrupt(); // retain if needed later 335 log.info("Transmit loop interrupted"); 336 return; // If we don't return here, xmtHandler.wait(wait) will be called again, which will cause a new InterruptedException, which results in a loop 337 } 338 } 339 log.debug("Timeout in transmitWait, mCurrentState: {}", mCurrentState); 340 } 341 342 volatile protected int mCurrentState; 343 344 /** 345 * Invoked at startup to start the threads needed here. 346 */ 347 @Override 348 public void startThreads() { 349 int priority = Thread.currentThread().getPriority(); 350 log.debug("startThreads current priority = {} max available = " + Thread.MAX_PRIORITY + " default = " + Thread.NORM_PRIORITY + " min available = " + Thread.MIN_PRIORITY, priority); 351 352 // make sure that the xmt priority is no lower than the current priority 353 int xmtpriority = (Thread.MAX_PRIORITY - 1 > priority ? Thread.MAX_PRIORITY - 1 : Thread.MAX_PRIORITY); 354 // start the XmtHandler in a thread of its own 355 if (xmtHandler == null) { 356 xmtHandler = new XmtHandler(); 357 } 358 xmtThread = new Thread(xmtHandler, "LocoNet Uhlenbrock transmit handler"); 359 log.debug("Xmt thread starts at priority {}", xmtpriority); 360 xmtThread.setDaemon(true); 361 xmtThread.setPriority(Thread.MAX_PRIORITY - 1); 362 xmtThread.start(); 363 364 // start the RcvHandler in a thread of its own 365 if (rcvHandler == null) { 366 rcvHandler = new RcvHandler(this); 367 } 368 rcvThread = new Thread(rcvHandler, "LocoNet Uhlenbrock receive handler"); 369 rcvThread.setDaemon(true); 370 rcvThread.setPriority(Thread.MAX_PRIORITY); 371 rcvThread.start(); 372 373 } 374 375 private static final Logger log = LoggerFactory.getLogger(UhlenbrockPacketizer.class); 376 377}