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}