001package jmri.jmrix.tams; 002 003import java.util.concurrent.ConcurrentLinkedQueue; 004import jmri.CommandStation; 005import jmri.jmrix.AbstractMRListener; 006import jmri.jmrix.AbstractMRMessage; 007import jmri.jmrix.AbstractMRReply; 008import jmri.jmrix.AbstractMRTrafficController; 009import jmri.util.StringUtil; 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013/** 014 * Converts Stream-based I/O to/from Tams messages. The "TamsInterface" side 015 * sends/receives message objects. 016 * <p> 017 * The connection to a TamsPortController is via a pair of Streams, which then 018 * carry sequences of characters for transmission. Note that this processing is 019 * handled in an independent thread. 020 * <p> 021 * This handles the state transitions, based on the necessary state in each 022 * message. 023 * <p> 024 * Based on work by Bob Jacobsen and Kevin Dickerson 025 * With support from Bob Jacobsen for which my thanks 026 * 027 * @author Jan Boen 028 */ 029 030// May/June 2018 - adjust so it works properly in synchronous mode. 031 032public class TamsTrafficController extends AbstractMRTrafficController implements TamsInterface, CommandStation { 033 034 /** 035 * Create a new TamsTrafficController instance. 036 */ 037 public TamsTrafficController() { 038 super(); 039 log.debug("creating a new TamsTrafficController object"); 040 log.debug("Just a silly change to force an staged change"); 041 // set as command station too 042 jmri.InstanceManager.store(TamsTrafficController.this, jmri.CommandStation.class); 043 super.setAllowUnexpectedReply(false); 044 } 045 046 public void setAdapterMemo(TamsSystemConnectionMemo memo) { 047 adaptermemo = memo; 048 log.trace("setAdapterMemo method"); 049 } 050 051 TamsSystemConnectionMemo adaptermemo; 052 053 @Override 054 public String getUserName() { 055 if (adaptermemo == null) { 056 return "Tams"; 057 } 058 return adaptermemo.getUserName(); 059 } 060 061 @Override 062 public String getSystemPrefix() { 063 if (adaptermemo == null) { 064 return "T"; 065 } 066 return adaptermemo.getSystemPrefix(); 067 } 068 069 // The methods to implement the TamsInterface 070 @Override 071 public synchronized void addTamsListener(TamsListener l) { 072 this.addListener(l); 073 } 074 075 @Override 076 public synchronized void removeTamsListener(TamsListener l) { 077 this.removeListener(l); 078 } 079 080 @Override 081 protected int enterProgModeDelayTime() { 082 // we should wait at least a second after enabling the programming 083 // track 084 return 1000; 085 } 086 087 /** 088 * CommandStation implementation. 089 * 090 * @param packet ignored, but needed for API compatibility 091 * @param count ignored, but needed for API compatibility 092 */ 093 @Override 094 public boolean sendPacket(byte[] packet, int count) { 095 log.trace("*** sendPacket ***"); 096 return true; 097 } 098 099 /** 100 * Forward a TamsMessage to all registered TamsInterface listeners. 101 * 102 * @param client the listener, may throw an uncaught exception if not a 103 * TamsListner 104 * @param m the message, may throw an uncaught exception if not a 105 * TamsMessage 106 */ 107 @Override 108 // Not for polled messages 109 protected void forwardMessage(AbstractMRListener client, AbstractMRMessage m) { 110 log.trace("*** forwardMessage ***"); 111 ((TamsListener) client).message((TamsMessage) m); 112 } 113 114 /** 115 * Forward a TamsReply to all TamsInterface listeners. 116 * 117 * @param client the listener for the TamsInterface 118 * @param tr the message to forward 119 */ 120 @Override 121 // Not for polled messages 122 protected void forwardReply(AbstractMRListener client, AbstractMRReply tr) { 123 log.trace("*** forward Tams Reply ***"); 124 ((TamsListener) client).reply((TamsReply) tr); 125 } 126 127 /** 128 * Poll Message Handler. 129 */ 130 private static class PollMessage { 131 132 TamsListener tl; 133 TamsMessage tm; 134 135 PollMessage(TamsMessage tm, TamsListener tl) { 136 log.trace("*** Tams Poll Message ***"); 137 this.tm = tm; 138 this.tl = tl; 139 } 140 141 TamsListener getListener() { 142 return tl; 143 } 144 145 TamsMessage getMessage() { 146 return tm; 147 } 148 } 149 150 ConcurrentLinkedQueue<PollMessage> pollQueue = new ConcurrentLinkedQueue<>(); 151 152 boolean disablePoll = false; 153 154 public boolean getPollQueueDisabled() { 155 return disablePoll; 156 } 157 158 public void setPollQueueDisabled(boolean poll) { 159 disablePoll = poll; 160 } 161 162 /** 163 * As we have to poll the Tams MC system to get updates, we put request into 164 * a queue and allow the abstract traffic controller to handle requests when 165 * it is free. 166 * 167 * @param tm the message to queue 168 * @param tl the listener to monitor the message and its reply 169 */ 170 public void addPollMessage(TamsMessage tm, TamsListener tl) { 171 log.trace("*** add Tams Poll Message ***"); 172 tm.setTimeout(1000); 173 boolean found = false; 174 for (PollMessage pm : pollQueue) { 175 log.trace("comparing poll messages: {} {}", pm.getMessage(), tm); 176 if (pm.getListener() == tl && pm.getMessage().toString().equals(tm.toString())) { 177 log.debug("Message is already in the poll queue so will not add"); 178 found = true; 179 } 180 } 181 if (!found) { 182 log.trace("Added to poll queue = {}", tm); 183 PollMessage pm = new PollMessage(tm, tl); 184 pollQueue.offer(pm); 185 } 186 } 187 188 /** 189 * Remove a message that is used for polling from the queue. 190 * 191 * @param tm the message to remove 192 * @param tl the listener waiting for the reply to the message 193 */ 194 public void removePollMessage(TamsMessage tm, TamsListener tl) { 195 log.trace("*** remove Tams Poll Message ***"); 196 for (PollMessage pm : pollQueue) { 197 if (pm.getListener() == tl && pm.getMessage().toString().equals(tm.toString())) { 198 pollQueue.remove(pm); 199 } 200 } 201 } 202 203 /** 204 * Check Tams MC for status updates. 205 * 206 * @return the next available message 207 */ 208 @Override 209 // The pollMessage class is a fill in for the abstract newReply class and as such specific to the Tams system 210 // Can be completely changed if needed 211 protected TamsMessage pollMessage() { 212 log.trace("*** Tams Poll Message ***"); 213 if (disablePoll) { 214 log.trace("Nothing in the Poll Queue"); 215 return null; 216 } 217 if (!pollQueue.isEmpty()) { 218 PollMessage pm = pollQueue.peek(); 219 if (pm != null) { 220 log.trace("PollMessage = {}", pm.getMessage()); 221 return pm.getMessage(); 222 } 223 } 224 return null; 225 } 226 227 @Override 228 // The pollReplyHandler class is a fill in for the abstract newReply class and as such specific to the Tams system 229 // Can be completely changed if needed 230 protected AbstractMRListener pollReplyHandler() { 231 log.trace("*** Tams Poll Reply Handler ***"); 232 if (disablePoll) { 233 return null; 234 } 235 if (!pollQueue.isEmpty()) { 236 PollMessage pm = pollQueue.poll(); 237 if (pm != null) { 238 pollQueue.offer(pm); 239 return pm.getListener(); 240 } 241 } 242 return null; 243 } 244 245 /** 246 * Forward a pre-formatted message to the actual interface. 247 * 248 * @param tm the message to forward 249 * @param tl the listener for the reply to the messageF 250 */ 251 @Override 252 // The sendTamsMessage class is specific to the Tams system 253 // Can be completely changed if needed 254 public void sendTamsMessage(TamsMessage tm, TamsListener tl) { 255 log.trace("*** Send Tams Message ***"); 256 if (log.isTraceEnabled()) { 257 if (tm.isBinary()) { 258 log.trace("Binary TamsMessage = {} {} and replyType = {}", StringUtil.appendTwoHexFromInt(tm.getElement(0) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tm.getElement(1) & 0xFF, ""), tm.getReplyType()); 259 } else { 260 log.trace("ASCII TamsMessage = {} and replyType = {}", tm, tm.getReplyType()); 261 } 262 } 263 sendMessage(tm, tl); 264 } 265 266 @Override 267 protected void forwardToPort(AbstractMRMessage tm, AbstractMRListener reply) { 268 log.trace("*** Forward Tams Message to Port ***"); 269 //Enhance this method to capture details related to the outgoing message so it can be used when receiving a reply 270 // Check if binary 271 // Check what type of reply is expected 272 replyBinary = tm.isBinary(); 273 replyType = ((TamsMessage)tm).getReplyType(); 274 replyOneByte = ((TamsMessage)tm).getReplyOneByte(); 275 replyLastByte = ((TamsMessage)tm).getReplyLastByte(); 276 super.forwardToPort(tm, reply); 277 } 278 279 protected char replyType; 280 protected boolean replyBinary; 281 protected boolean replyOneByte; 282 protected int replyLastByte; 283 284 @Override 285 protected TamsMessage enterProgMode() { 286 return null; 287 } 288 289 @Override 290 protected TamsMessage enterNormalMode() { 291 return null; 292 } 293 294 /** 295 * Add trailer to the outgoing byte stream. 296 * 297 * @param msg the output byte stream 298 * @param offset the first byte not yet used 299 * @param m the message in the byte stream 300 */ 301 protected void addTrailerToOutput(byte[] msg, int offset, TamsMessage m) { 302 log.trace("*** Tams Add Trailer to Output ***"); 303 if (!m.isBinary()) {// Activated this in case the output is not binary 304 msg[offset] = 0x0d; 305 } 306 } 307 308 /** 309 * Determine how many bytes the entire message will take, including space 310 * for header and trailer 311 * 312 * @param m The message to be sent 313 * @return Number of bytes 314 */ 315 protected int lengthOfByteStream(TamsMessage m) { 316 log.trace("*** Tams Length of Byte Stream ***"); 317 int len = m.getNumDataElements(); 318 int cr = 0; 319 if (!m.isBinary()) { 320 cr = 1; // space for return 321 } 322 log.trace("length ByteStream = {}, message = |{}|", len + cr, m); 323 return len + cr; 324 } 325 326 // The reply part 327 protected int myCounter = 0; //Helper variable used to count the number of iterations 328 protected int groupSize = 0; //Helper variable used to determine how many bytes are present in each reply nibble 329 protected boolean endReached = false; //Helper variable used to indicate we reached the end of the message 330 protected int numberOfNibbles = 0; //Helper variable used to calculate how many message nibbles there are in the reply 331 protected int messageLength = 0; //Helper variable used hold the length of the message 332 protected int index = 0; //Helper variable used keep track of where we are in the message 333 334 @Override 335 // The TamsReply class is a fill in for the abstract newReply class and as such specific to the Tams system 336 // Can be completely changed if needed 337 protected TamsReply newReply() { 338 log.trace("*** Tams Reply ***"); 339 TamsReply reply = new TamsReply(); 340 return reply; 341 } 342 343 // Has the message been completely received? 344 // The length depends on the message type 345 // Here we also use information related to the source message binary and type 346 @Override 347 protected boolean endOfMessage(AbstractMRReply reply) { 348 TamsReply tr = (TamsReply) reply; 349 log.trace("*** Tams End of Message ***"); 350 // Input is a continuous stream of characters and we must chop them up into separate messages 351 index = tr.getNumDataElements() - 1; 352 if (log.isTraceEnabled()) { 353 log.trace("Reading byte number = {}, value = {}", tr.getNumDataElements(), StringUtil.appendTwoHexFromInt(tr.getElement(index) & 0xFF, "")); 354 } 355 if (replyBinary) {// Binary reply 356 if (replyOneByte) {// Single byte reply 357 if (tr.getNumDataElements() < 1) {// Read one byte reply 358 endReached = false; 359 } else { 360 if (log.isTraceEnabled()) { 361 log.trace("One byte binary reply = {}", StringUtil.appendTwoHexFromInt(tr.getElement(index) & 0xFF, "")); 362 } 363 //Must add in code to handle Power messages and any other oneByteReply messages coming from Sensors or Turnouts 364 myCounter = 0; 365 endReached = true; 366 } 367 } else {// Multi byte reply 368 // Read multiple byte reply, until expected last byte 369 // Sensor reply 370 if (replyType == 'S') { 371 // Sensor replies are grouped per 3 (AA BB CC) when a new group has 0x00 as AA then this is the end of the message 372 // BUT 0x00 is also a valid byte in the 2 data bytes (BB CC) of a sensor read 373 log.trace("*** Receiving Sensor Reply ***"); 374 groupSize = 3; 375 log.trace("Looking for byte# = {} and index = {} and expect as last byte = {}", groupSize * myCounter + 1, index, replyLastByte); 376 if (tr.getNumDataElements() == (groupSize * myCounter + 1) && tr.getElement(index) == replyLastByte) { 377 myCounter = 0; 378 endReached = true; 379 log.trace("S - End reached!"); 380 } else { 381 if (tr.getNumDataElements() == (groupSize * myCounter + 1)) { 382 myCounter++; 383 } 384 endReached = false; 385 } 386 } 387 // Turnout reply 388 if (replyType == 'T') { 389 // The first byte of a reply can be 0x00 or hold the value of the number messages that will follow 390 // Turnout replies are grouped per 2 (AA BB) 391 // 0x00 is also a valid byte in the 2 data bytes (AA BB) of a turnout read 392 log.trace("*** Receiving Turnout Reply ***"); 393 numberOfNibbles = tr.getElement(0); 394 if (numberOfNibbles > 50) { 395 numberOfNibbles = 50; 396 } 397 messageLength = numberOfNibbles * 2; 398 log.trace("Number of turnout events# = {}", numberOfNibbles); 399 if (myCounter < messageLength) { 400 log.trace("myCounter = {}, reply length= {}", myCounter, tr.getNumDataElements()); 401 myCounter++; 402 endReached = false; 403 } else { 404 myCounter = 0; 405 endReached = true; 406 log.trace("myCounter = {}", myCounter); 407 log.trace("T - End reached!"); 408 } 409 } 410 // Loco reply 411 if (replyType == 'L') { 412 // The first byte of a reply can be 0x80 or if different messages will follow, 0x80 will be the last byte 413 // Loco replies are grouped per 5 (AA BB CC DD EE) 414 // Anything is a valid byte in the 5 data bytes (AA BB CC DD EE) of a Loco read 415 log.trace("*** Receiving Loco Reply ***"); 416 if (log.isTraceEnabled()) { 417 log.trace("Current byte = {}", StringUtil.appendTwoHexFromInt(tr.getElement(index) & 0xFF, "")); 418 } 419 groupSize = 5; 420 if (((tr.getElement(index) & 0xFF) == TamsConstants.EOM80)) { 421 myCounter = 0; 422 endReached = true; 423 if (index > 1) {//OK we have a real message 424 if (log.isTraceEnabled()) { 425 log.trace("reply = {} {} {} {} {}", StringUtil.appendTwoHexFromInt(tr.getElement(0) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(1) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(2) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(3) & 0xFF, ""), StringUtil.appendTwoHexFromInt(tr.getElement(4) & 0xFF, "")); 426 } 427 } 428 log.trace("L - End reached!"); 429 } else { 430 if (tr.getNumDataElements() == (groupSize * myCounter + 1)) { 431 myCounter++; 432 } 433 endReached = false; 434 } 435 } 436 } 437 } else {// ASCII reply 438 if (tr.getNumDataElements() > 0 && tr.getElement(index) != 0x5d) {// Read ASCII reply, last is [ 439 log.trace("Building ASCII reply = {}", tr); 440 //myCounter++; 441 endReached = false; 442 } else { 443 log.trace("ASCII reply = {} isBinary = {}", tr, replyBinary); 444 myCounter = 0; 445 endReached = true; 446 } 447 } 448 log.trace("End of Message = {}", endReached); 449 return endReached; 450 } 451 452 // mode accessors 453 private boolean _isBinary; 454 455 // display format 456 protected int[] _dataChars = null; 457 458 // display format 459 // contents (private) 460 protected int _nDataChars = 0; 461 462 // display format 463 @Override 464 public String toString() { 465 String s = ""; 466 for (int i = 0; i < _nDataChars; i++) { 467 if (_isBinary) { 468 if (i != 0) { 469 s += " "; 470 } 471 s = StringUtil.appendTwoHexFromInt(_dataChars[i] & 0xFF, s); 472 } else { 473 s += (char) _dataChars[i]; 474 } 475 } 476 return s; 477 } 478 private final static Logger log = LoggerFactory.getLogger(TamsTrafficController.class); 479 480}