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}