001package jmri.jmrix.roco.z21; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004import java.net.DatagramPacket; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.List; 008 009import jmri.jmrix.*; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * Abstract base for TrafficControllers in a Message/Reply protocol. 015 * 016 * @author Paul Bender Copyright (C) 2014 017 */ 018public class Z21TrafficController extends jmri.jmrix.AbstractMRTrafficController implements Z21Interface { 019 020 // Z21 LAN Protocol Specification v1.13 section 1.3 allows combined datasets 021 // up to the Ethernet MTU payload: 1472 = 1500-byte MTU - IPv4 - UDP headers. 022 private static final int MAX_UDP_PAYLOAD = 1472; 023 024 private java.net.InetAddress host; 025 private int port; 026 027 public Z21TrafficController() { 028 super(); 029 allowUnexpectedReply = true; 030 } 031 032 /** 033 * Implement this to forward a specific message type to a protocol-specific 034 * listener interface. This puts the casting into the concrete class. 035 */ 036 @Override 037 protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) { 038 ((Z21Listener) client).message((Z21Message) m); 039 } 040 041 /** 042 * Implement this to forward a specific Reply type to a protocol-specific 043 * listener interface. This puts the casting into the concrete class. 044 */ 045 @Override 046 protected void forwardReply(AbstractMRListener client, AbstractMRReply m) { 047 ((Z21Listener) client).reply((Z21Reply) m); 048 } 049 050 /** 051 * Invoked if it's appropriate to do low-priority polling of the command 052 * station, this should return the next message to send, or null if the TC 053 * should just sleep. 054 */ 055 @Override 056 protected Z21Message pollMessage() { 057 return null; 058 } 059 060 @Override 061 protected Z21Listener pollReplyHandler() { 062 return null; 063 } 064 065 /** 066 * enterProgMode() and enterNormalMode() return any message that 067 * needs to be returned to the command station to change modes. 068 * 069 * @see #enterNormalMode() 070 * @return if no message is needed, you may return null. 071 * 072 * If the programmerIdle() function returns true, enterNormalMode() is 073 * called after a timeout while in IDLESTATE during programming to 074 * return the system to normal mode. 075 */ 076 @Override 077 protected Z21Message enterProgMode() { 078 return null; 079 } 080 081 /** 082 * enterProgMode() and enterNormalMode() return any message that 083 * needs to be returned to the command station to change modes. 084 * 085 * @see #enterProgMode() 086 * @return if no message is needed, you may return null. 087 */ 088 @Override 089 protected Z21Message enterNormalMode() { 090 return null; 091 } 092 093 /** 094 * Actually transmits the next message to the port. 095 */ 096 @SuppressFBWarnings(value = {"TLW_TWO_LOCK_WAIT", "", "UW_UNCOND_WAIT"}, 097 justification = "Two locks needed for synchronization here, this is OK; String + only used for debug, so inefficient String processing not really a problem; Unconditional Wait is to give external hardware, which doesn't necessarilly respond, time to process the data.") 098 @Override 099 synchronized protected void forwardToPort(AbstractMRMessage m, AbstractMRListener reply) { 100 if (log.isDebugEnabled()) { 101 log.debug("forwardToPort message: [{}]", m); 102 } 103 // remember who sent this 104 mLastSender = reply; 105 106 // forward the message to the registered recipients, 107 // which includes the communications monitor, except the sender. 108 // Schedule notification via the Swing event queue to ensure order 109 Runnable r = new XmtNotifier(m, mLastSender, this); 110 javax.swing.SwingUtilities.invokeLater(r); 111 112 // stream to port in single write, as that's needed by serial 113 byte[] msg = new byte[lengthOfByteStream(m)]; 114 // add header 115 int offset = addHeaderToOutput(msg, m); 116 117 // add data content 118 int len = m.getNumDataElements(); 119 for (int i = 0; i < len; i++) { 120 msg[i + offset] = (byte) m.getElement(i); 121 } 122 // add trailer 123 addTrailerToOutput(msg, len + offset, m); 124 // and send the bytes 125 try { 126 if (log.isDebugEnabled()) { 127 StringBuilder f = new StringBuilder("formatted message: "); 128 for (byte b : msg) { 129 f.append(Integer.toHexString(0xFF & b)); 130 f.append(" "); 131 } 132 log.debug(new String(f)); 133 } 134 while (m.getRetries() >= 0) { 135 if (portReadyToSend(controller)) { 136 // create a datagram with the data from the 137 // message. 138 byte[] data = ((Z21Message) m).getBuffer(); 139 DatagramPacket sendPacket 140 = new DatagramPacket(data, ((Z21Message) m).getLength(), host, port); 141 // and send it. 142 ((Z21Adapter) controller).getSocket().send(sendPacket); 143 log.debug("written, msg timeout: {} mSec", m.getTimeout()); 144 break; 145 } else if (m.getRetries() >= 0) { 146 if (log.isDebugEnabled()) { 147 StringBuilder b = new StringBuilder("Retry message: "); 148 b.append(m.toString()); 149 b.append(" attempts remaining: "); 150 b.append(m.getRetries()); 151 log.debug(new String(b)); 152 } 153 m.setRetries(m.getRetries() - 1); 154 try { 155 synchronized (xmtRunnable) { 156 xmtRunnable.wait(m.getTimeout()); 157 } 158 } catch (InterruptedException e) { 159 Thread.currentThread().interrupt(); // retain if needed later 160 if(!threadStopRequest) { 161 log.error("retry wait interrupted"); 162 } else { 163 log.error("retry wait interrupted during thread stop"); 164 } 165 } 166 } else { 167 log.warn("sendMessage: port not ready for data sending: {}", java.util.Arrays.toString(msg)); 168 } 169 } 170 } catch (Exception e) { 171 // TODO Currently there's no port recovery if an exception occurs 172 // must restart JMRI to clear xmtException. 173 xmtException = true; 174 portWarn(e); 175 } 176 } 177 178 @Override() 179 public boolean status() { 180 if (controller == null) { 181 return false; 182 } else { 183 return (controller.status()); 184 } 185 } 186 187 /** 188 * Make connection to existing PortController object. 189 */ 190 @Override 191 public void connectPort(AbstractPortController p) { 192 rcvException = false; 193 xmtException = false; 194 if (controller != null) { 195 log.warn("connectPort: connect called while connected"); 196 } else { 197 log.debug("connectPort invoked"); 198 } 199 if (!(p instanceof Z21Adapter)) { 200 throw new IllegalArgumentException("attempt to connect wrong port type"); 201 } 202 controller = p; 203 try { 204 host = java.net.InetAddress.getByName(((Z21Adapter) controller).getHostName()); 205 port = ((Z21Adapter) controller).getPort(); 206 ConnectionStatus.instance().setConnectionState( 207 p.getSystemConnectionMemo(), ConnectionStatus.CONNECTION_UP); 208 } catch (java.net.UnknownHostException uhe) { 209 log.error("Unknown Host: {} ", ((Z21Adapter) controller).getHostName()); 210 ConnectionStatus.instance().setConnectionState( 211 p.getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN); 212 } 213 // and start threads 214 xmtThread = new Thread(xmtRunnable = () -> { 215 try { 216 transmitLoop(); 217 } catch (Throwable e) { 218 if(!threadStopRequest) 219 log.error("Transmit thread terminated prematurely by: {}", e.toString(), e); 220 } 221 }); 222 xmtThread.setName("z21.Z21TrafficController Transmit thread"); 223 xmtThread.start(); 224 rcvThread = new Thread(this::receiveLoop); 225 rcvThread.setName("z21.Z21TrafficController Receive thread"); 226 int xr = rcvThread.getPriority(); 227 xr++; 228 rcvThread.setPriority(xr); //bump up the priority 229 rcvThread.start(); 230 } 231 232 /** 233 * Break connection to existing PortController object. Once broken, attempts 234 * to send via "message" member will fail. 235 */ 236 @Override 237 public void disconnectPort(AbstractPortController p) { 238 if (controller != p) { 239 log.warn("disconnectPort: disconnect called from non-connected AbstractPortController"); 240 } 241 controller = null; 242 } 243 244 @Override 245 protected Z21Reply newReply() { 246 return new Z21Reply(); 247 } 248 249 @Override 250 protected boolean endOfMessage(AbstractMRReply r) { 251 // since this is a UDP protocol, and each reply in the packet is complete, 252 // we don't check for end of message manually. 253 return true; 254 } 255 256 /** 257 * Handle each reply when complete. 258 * <p> 259 * (This is public for testing purposes) Runs in the "Receive" thread. 260 */ 261 @SuppressFBWarnings(value = {"UW_UNCOND_WAIT", "WA_NOT_IN_LOOP", "NO_NOTIFY_NOT_NOTIFYALL"}, 262 justification = "Wait is for external hardware, which doesn't necessarilly respond, to process the data. Notify is used because Having more than one thread waiting on xmtRunnable is an error.") 263 @Override 264 public void handleOneIncomingReply() throws java.io.IOException { 265 // we sit in this until the message is complete, relying on 266 // threading to let other stuff happen 267 268 // create a buffer to hold the incoming data. 269 byte[] buffer = new byte[MAX_UDP_PAYLOAD]; 270 271 // create the packet. 272 DatagramPacket receivePacket = new DatagramPacket(buffer, MAX_UDP_PAYLOAD, host, port); 273 274 // and wait to receive data in the packet. 275 try { 276 ((Z21Adapter) controller).getSocket().receive(receivePacket); 277 } catch (java.net.SocketException | NullPointerException se) { 278 // if we are waiting when the controller is disposed, 279 // a socket exception will be thrown. 280 log.debug("Socket exception during receive. Connection Closed?"); 281 rcvException = true; 282 return; 283 } 284 if (threadStopRequest) return; 285 286 // handle more than one reply in the same UDP packet. 287 List<Z21Reply> replies = new ArrayList<>(); 288 289 int totalLength=receivePacket.getLength(); 290 int consumed=0; 291 292 do { 293 int length = (0xff & buffer[0]) + ((0xff & buffer[1]) << 8); 294 Z21Reply msg = new Z21Reply(buffer, length); 295 296 replies.add(msg); 297 298 buffer = Arrays.copyOfRange(buffer,length,buffer.length); 299 consumed +=length; 300 log.trace("total length: {} consumed {}",totalLength,consumed); 301 } while(totalLength>consumed); 302 303 304 // and then dispatch each reply 305 replies.forEach(this::dispatchReply); 306 } 307 308 private void dispatchReply(Z21Reply msg) { 309 // message is complete, dispatch it !! 310 replyInDispatch = true; 311 if (log.isDebugEnabled()) { 312 log.debug("dispatch reply of length {} contains {} state {}", msg.getNumDataElements(), msg.toString(), mCurrentState); 313 } 314 315 // forward the message to the registered recipients, 316 // which includes the communications monitor 317 // return a notification via the Swing event queue to ensure proper thread 318 Runnable r = new RcvNotifier(msg, mLastSender, this); 319 try { 320 javax.swing.SwingUtilities.invokeAndWait(r); 321 } catch (InterruptedException ie) { 322 if(threadStopRequest) return; 323 log.error("Unexpected exception in invokeAndWait:{}", ie, ie); 324 } catch (Exception e) { 325 log.error("Unexpected exception in invokeAndWait:{}", e, e); 326 } 327 if (log.isDebugEnabled()) { 328 log.debug("dispatch thread invoked"); 329 } 330 331 if (!msg.isUnsolicited()) { 332 // effect on transmit: 333 switch (mCurrentState) { 334 case WAITMSGREPLYSTATE: { 335 // check to see if the response was an error message we want 336 // to automatically handle by re-queueing the last sent 337 // message, otherwise go on to the next message 338 if (msg.isRetransmittableErrorMsg()) { 339 if (log.isDebugEnabled()) { 340 log.debug("Automatic Recovery from Error Message: +msg.toString()"); 341 } 342 synchronized (xmtRunnable) { 343 mCurrentState = AUTORETRYSTATE; 344 replyInDispatch = false; 345 xmtRunnable.notify(); 346 } 347 } else { 348 // update state, and notify to continue 349 synchronized (xmtRunnable) { 350 mCurrentState = NOTIFIEDSTATE; 351 replyInDispatch = false; 352 xmtRunnable.notify(); 353 } 354 } 355 break; 356 } 357 case WAITREPLYINPROGMODESTATE: { 358 // entering programming mode 359 mCurrentMode = PROGRAMINGMODE; 360 replyInDispatch = false; 361 362 // check to see if we need to delay to allow decoders to become 363 // responsive 364 int warmUpDelay = enterProgModeDelayTime(); 365 if (warmUpDelay != 0) { 366 try { 367 synchronized (xmtRunnable) { 368 xmtRunnable.wait(warmUpDelay); 369 } 370 } catch (InterruptedException e) { 371 Thread.currentThread().interrupt(); // retain if needed later 372 if (threadStopRequest) return; 373 } 374 } 375 // update state, and notify to continue 376 synchronized (xmtRunnable) { 377 mCurrentState = OKSENDMSGSTATE; 378 xmtRunnable.notify(); 379 } 380 break; 381 } 382 case WAITREPLYINNORMMODESTATE: { 383 // entering normal mode 384 mCurrentMode = NORMALMODE; 385 replyInDispatch = false; 386 // update state, and notify to continue 387 synchronized (xmtRunnable) { 388 mCurrentState = OKSENDMSGSTATE; 389 xmtRunnable.notify(); 390 } 391 break; 392 } 393 default: { 394 replyInDispatch = false; 395 if (allowUnexpectedReply) { 396 if (log.isDebugEnabled()) { 397 log.debug("Allowed unexpected reply received in state: {} was {}", mCurrentState, msg.toString()); 398 } 399 synchronized (xmtRunnable) { 400 // The transmit thread sometimes gets stuck 401 // when unexpected replies are received. Notify 402 // it to clear the block without a timeout. 403 // (do not change the current state) 404 //if(mCurrentState!=IDLESTATE) 405 xmtRunnable.notify(); 406 } 407 } else { 408 unexpectedReplyStateError(mCurrentState,msg.toString()); 409 } 410 } 411 } 412 // Unsolicited message 413 } else { 414 if (log.isDebugEnabled()) { 415 log.debug("Unsolicited Message Received {}", msg.toString()); 416 } 417 418 replyInDispatch = false; 419 } 420 } 421 422 @SuppressFBWarnings(value = {"UW_UNCOND_WAIT", "WA_NOT_IN_LOOP"}, 423 justification = "Wait is for external hardware, which doesn't necessarilly respond, to process the data.") 424 @Override 425 protected void terminate() { 426 if (controller == null) { 427 log.debug("terminate called while not connected"); 428 return; 429 } else { 430 log.debug("Cleanup Starts"); 431 } 432 433 Z21Message logoffMessage = Z21Message.getLanLogoffRequestMessage(); 434 forwardToPort(logoffMessage, null); 435 // wait for reply 436 try { 437 if (xmtRunnable != null) { 438 synchronized (xmtRunnable) { 439 xmtRunnable.wait(logoffMessage.getTimeout()); 440 } 441 } 442 } catch (InterruptedException e) { 443 Thread.currentThread().interrupt(); // retain if needed later 444 log.error("transmit interrupted"); 445 } finally { 446 // set the controller to null, even if terminate fails. 447 controller = null; 448 } 449 } 450 451 /** 452 * Terminate the receive and transmit threads. 453 * <p> 454 * This is intended to be used only by testing subclasses. 455 */ 456 @Override 457 public void terminateThreads() { 458 threadStopRequest = true; 459 // ensure socket closed to end pending operations 460 if ( controller != null && ((Z21Adapter) controller).getSocket() != null) ((Z21Adapter) controller).getSocket().close(); 461 462 // usual stop process 463 super.terminateThreads(); 464 } 465 466 // The methods to implement the Z21Interface 467 @Override 468 public synchronized void addz21Listener(Z21Listener l) { 469 this.addListener(l); 470 } 471 472 @Override 473 public synchronized void removez21Listener(Z21Listener l) { 474 this.removeListener(l); 475 } 476 477 /** 478 * Forward a preformatted message to the actual interface. 479 */ 480 @Override 481 public void sendz21Message(Z21Message m, Z21Listener reply) { 482 sendMessage(m, reply); 483 } 484 485 private static final Logger log = LoggerFactory.getLogger(Z21TrafficController.class); 486}