001package jmri.jmrix.qsi;
002
003import java.io.DataInputStream;
004import java.io.OutputStream;
005import java.util.Vector;
006import jmri.jmrix.qsi.serialdriver.SerialDriverAdapter;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009import purejavacomm.SerialPort;
010
011/**
012 * Converts Stream-based I/O to/from QSI messages. The "QsiInterface" side
013 * sends/receives message objects. The connection to a QsiPortController is via
014 * a pair of *Streams, which then carry sequences of characters for
015 * transmission. Note that this processing is handled in an independent thread.
016 * <p>
017 * Messages to and from the programmer are in a packet format. In both
018 * directions, every message starts with 'S' and ends with 'E'. These are
019 * handled automatically, and are not included in the QsiMessage and QsiReply
020 * content.
021 *
022 * @author Bob Jacobsen Copyright (C) 2007, 2008
023 */
024public class QsiTrafficController implements QsiInterface, Runnable {
025
026    /**
027     * Create a new QsiTrafficController instance.
028     */
029    public QsiTrafficController() {
030    }
031
032// The methods to implement the QsiInterface
033    protected Vector<QsiListener> cmdListeners = new Vector<>();
034
035    @Override
036    public boolean status() {
037        return (ostream != null && istream != null);
038    }
039
040    @Override
041    public synchronized void addQsiListener(QsiListener l) {
042        // add only if not already registered
043        if (l == null) {
044            throw new java.lang.NullPointerException();
045        }
046        if (!cmdListeners.contains(l)) {
047            cmdListeners.addElement(l);
048        }
049    }
050
051    @Override
052    public synchronized void removeQsiListener(QsiListener l) {
053        if (cmdListeners.contains(l)) {
054            cmdListeners.removeElement(l);
055        }
056    }
057
058    /**
059     * Forward a QsiMessage to all registered QsiInterface listeners.
060     * @param m message to forward.
061     * @param notMe Listener to hear the returned status
062     */
063    @SuppressWarnings("unchecked")
064    protected void notifyMessage(QsiMessage m, QsiListener notMe) {
065        // make a copy of the listener vector to synchronized not needed for transmit
066        Vector<QsiListener> v;
067        synchronized (this) {
068            v = (Vector<QsiListener>) cmdListeners.clone();
069        }
070        // forward to all listeners
071        int cnt = v.size();
072        for (int i = 0; i < cnt; i++) {
073            QsiListener client = v.elementAt(i);
074            if (notMe != client) {
075                log.debug("notify client: {}", client);
076                try {
077                    client.message(m);
078                } catch (Exception e) {
079                    log.warn("notify: During dispatch to {}", client, e);
080                }
081            }
082        }
083    }
084
085    QsiListener lastSender = null;
086
087    // Current QSI state
088    public static final int NORMAL = 0;
089    public static final int SIIBOOTMODE = 1;
090    public static final int V4BOOTMODE = 2;
091
092    private int qsiState = NORMAL;
093
094    public int getQsiState() {
095        return qsiState;
096    }
097
098    public void setQsiState(int s) {
099        qsiState = s;
100        if(controller instanceof SerialDriverAdapter) {
101           if (s == V4BOOTMODE) {
102               // enable flow control - required for QSI v4 bootloader
103               ((SerialDriverAdapter)controller).setHandshake(SerialPort.FLOWCONTROL_RTSCTS_IN
104                       | SerialPort.FLOWCONTROL_RTSCTS_OUT);
105
106           } else {
107               // disable flow control
108               ((SerialDriverAdapter)controller).setHandshake(0);
109           }
110           log.debug("Setting qsiState {}", s);
111       }
112    }
113
114    public boolean isNormalMode() {
115        return qsiState == NORMAL;
116    }
117
118    public boolean isSIIBootMode() {
119        return qsiState == SIIBOOTMODE;
120    }
121
122    public boolean isV4BootMode() {
123        return qsiState == V4BOOTMODE;
124    }
125
126    @SuppressWarnings("unchecked")
127    protected void notifyReply(QsiReply r) {
128        // make a copy of the listener vector to synchronized (not needed for transmit?)
129        Vector<QsiListener> v;
130        synchronized (this) {
131            v = (Vector<QsiListener>) cmdListeners.clone();
132        }
133        // forward to all listeners
134        int cnt = v.size();
135        for (int i = 0; i < cnt; i++) {
136            QsiListener client = v.elementAt(i);
137            log.debug("notify client: {}", client);
138            try {
139                // skip forwarding to the last sender for now, we'll get them later
140                if (lastSender != client) {
141                    client.reply(r);
142                }
143            } catch (Exception e) {
144                log.warn("notify: During dispatch to {}", client, e);
145            }
146        }
147
148        // forward to the last listener who send a message
149        // this is done _second_ so monitoring can have already stored the reply
150        // before a response is sent
151        if (lastSender != null) {
152            lastSender.reply(r);
153        }
154    }
155
156    /**
157     * Forward a preformatted message to the actual interface.
158     */
159    @Override
160    public void sendQsiMessage(QsiMessage m, QsiListener reply) {
161        log.debug("sendQsiMessage message: [{}]", m);
162        // remember who sent this
163        lastSender = reply;
164
165        // notify all _other_ listeners
166        notifyMessage(m, reply);
167
168        // stream to port in single write, as that's needed by serial
169        int len = m.getNumDataElements();
170
171        // space for carriage return if required
172        int cr = 0;
173        int start = 0;
174        if (isSIIBootMode()) {
175            cr = 1;
176            start = 0;
177        } else {
178            cr = 3;  // 'S', CRC, 'E'
179            start = 1;
180        }
181
182        byte msg[] = new byte[len + cr];
183
184        byte crc = 0;
185
186        for (int i = 0; i < len; i++) {
187            msg[i + start] = (byte) m.getElement(i);
188            crc ^= msg[i + start];
189        }
190
191        if (isSIIBootMode()) {
192            msg[len] = 0x0d;
193        } else {
194            msg[0] = 'S';
195            msg[len + cr - 2] = crc;
196            msg[len + cr - 1] = 'E';
197        }
198
199        try {
200            if (ostream != null) {
201                if (log.isDebugEnabled()) {
202                    log.debug("write message: {}", jmri.util.StringUtil.hexStringFromBytes(msg));
203                }
204                ostream.write(msg);
205            } else {
206                // no stream connected
207                log.warn("sendMessage: no connection established");
208            }
209        } catch (Exception e) {
210            log.warn("sendMessage: Exception: {}", e.toString());
211        }
212    }
213
214    // methods to connect/disconnect to a source of data in a LnPortController
215    private QsiPortController controller = null;
216
217    /**
218     * Make connection to existing PortController object.
219     * @param p the QSI port controller.
220     */
221    public void connectPort(QsiPortController p) {
222        istream = p.getInputStream();
223        ostream = p.getOutputStream();
224        if (controller != null) {
225            log.warn("connectPort: connect called while connected");
226        }
227        controller = p;
228    }
229
230    /**
231     * Break connection to existing QsiPortController object.
232     * Once broken, attempts to send via "message" member will fail.
233     * @param p the QSI port controller.
234     */
235    public void disconnectPort(QsiPortController p) {
236        istream = null;
237        ostream = null;
238        if (controller != p) {
239            log.warn("disconnectPort: disconnect called from non-connected LnPortController");
240        }
241        controller = null;
242    }
243
244    // data members to hold the streams
245    DataInputStream istream = null;
246    OutputStream ostream = null;
247
248    /**
249     * Handle incoming characters. This is a permanent loop, looking for input
250     * messages in character form on the stream connected to the PortController
251     * via <code>connectPort</code>. Terminates with the input stream breaking
252     * out of the try block.
253     */
254    @Override
255    public void run() {
256        while (true) {   // loop permanently, stream close will exit via exception
257            try {
258                handleOneIncomingReply();
259            } catch (java.io.IOException e) {
260                log.warn("run: Exception: {}", e.toString());
261            }
262        }
263    }
264
265    void handleOneIncomingReply() throws java.io.IOException {
266          // we sit in this until the message is complete, relying on
267        // threading to let other stuff happen
268
269        // Create output message
270        QsiReply msg = new QsiReply();
271        // message exists, now fill it
272        int i;
273        for (i = 0; i < QsiReply.MAXSIZE; i++) {
274            byte char1 = istream.readByte();
275            if (log.isDebugEnabled()) {
276                log.debug("   Rcv char: {}", jmri.util.StringUtil.twoHexFromInt(char1));
277            }
278            msg.setElement(i, char1);
279            if (endReply(msg)) {
280                break;
281            }
282        }
283
284        // message is complete, dispatch it !!
285        log.debug("dispatch reply of length {}",i);
286        {
287            final QsiReply thisMsg = msg;
288            final QsiTrafficController thisTc = this;
289            // return a notification via the queue to ensure end
290            Runnable r = new Runnable() {
291                QsiReply msgForLater = thisMsg;
292                QsiTrafficController myTc = thisTc;
293
294                @Override
295                public void run() {
296                    log.debug("Delayed notify starts");
297                    myTc.notifyReply(msgForLater);
298                }
299            };
300            javax.swing.SwingUtilities.invokeLater(r);
301        }
302    }
303
304    /*
305     * Normal QSI replies will end with the prompt for the next command
306     */
307    boolean endReply(QsiReply msg) {
308        if (endNormalReply(msg)) {
309            return true;
310        }
311        return false;
312    }
313
314    boolean endNormalReply(QsiReply msg) {
315        // Detect that the reply buffer ends with "E"
316        // This should really be based on length....
317        int num = msg.getNumDataElements();
318        if (num >= 3) {
319            // ptr is offset of last element in QsiReply
320            int ptr = num - 1;
321            if (msg.getElement(ptr) != 'E') {
322                return false;
323            }
324            return true;
325        } else {
326            return false;
327        }
328    }
329
330    private final static Logger log = LoggerFactory.getLogger(QsiTrafficController.class);
331
332}