001package jmri.jmrix.rps.serial; 002 003import java.util.Arrays; 004import java.io.IOException; 005 006import jmri.InvokeOnGuiThread; 007import jmri.jmrix.rps.Distributor; 008import jmri.jmrix.rps.Engine; 009import jmri.jmrix.rps.Reading; 010import jmri.jmrix.rps.RpsSystemConnectionMemo; 011import org.apache.commons.csv.CSVFormat; 012import org.apache.commons.csv.CSVParser; 013import org.apache.commons.csv.CSVRecord; 014 015/** 016 * Implements SerialPortAdapter for the RPS system. 017 * <p> 018 * Unlike many other SerialPortAdapters, this also converts the input stream 019 * into Readings that can be passed to the Distributor. 020 * <p> 021 * This version expects that the "A" command will send back "DATA,," followed by 022 * a list of receivers numbers, and data lines will be "0,0,0,0": A value for 023 * each address up to the max receiver, even if some are missing (0 in that 024 * case) 025 * 026 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2008 027 */ 028public class SerialAdapter extends jmri.jmrix.AbstractSerialPortController { 029 030 public SerialAdapter() { 031 super(new RpsSystemConnectionMemo()); 032 option1Name = "Protocol"; // NOI18N 033 options.put(option1Name, new Option(Bundle.getMessage("ProtocolVersionLabel"), validOptions1)); 034 this.manufacturerName = jmri.jmrix.rps.RpsConnectionTypeList.NAC; 035 } 036 037 /** 038 * {@inheritDoc} 039 */ 040 @Override 041 public RpsSystemConnectionMemo getSystemConnectionMemo() { 042 return (RpsSystemConnectionMemo) super.getSystemConnectionMemo(); 043 } 044 045 /** 046 * Set up all of the other objects to operate. 047 */ 048 @Override 049 public void configure() { 050 // Connect the control objects: 051 log.debug("configure() connecting RPS objects"); 052 // connect an Engine to the Distributor 053 Engine e = Engine.instance(); 054 Distributor.instance().addReadingListener(e); 055 // start the reader 056 readerThread = new Thread(new Reader()); 057 readerThread.start(); 058 059 this.getSystemConnectionMemo().configureManagers(); 060 } 061 062 @Override 063 public synchronized String openPort(String portName, String appName) { 064 065 // get and open the primary port 066 currentSerialPort = activatePort(portName, log); 067 if (currentSerialPort == null) { 068 log.error("failed to connect RPS to {}", portName); 069 return Bundle.getMessage("SerialPortNotFound", portName); 070 } 071 log.info("Connecting RPS to {} {}", portName, currentSerialPort); 072 073 // try to set it for communication via SerialDriver 074 // find the baud rate value, configure comm options 075 int baud = currentBaudNumber(mBaudRate); 076 setBaudRate(currentSerialPort, baud); 077 configureLeads(currentSerialPort, true, true); 078 setFlowControl(currentSerialPort, FlowControl.NONE); 079 080 // report status 081 reportPortStatus(log, portName); 082 083 opened = true; 084 085 // capture streams 086 serialStream = getInputStream(); 087 ostream = getOutputStream(); 088 089 return null; // indicates OK return 090 } 091 092 java.io.DataInputStream serialStream; 093 java.io.OutputStream ostream; 094 095 /** 096 * Send output bytes, e.g. characters controlling operation, with small 097 * delays between the characters. This is used to reduce overrrun problems. 098 * @param bytes Array of characters to be sent one at a time 099 */ 100 synchronized void sendBytes(byte[] bytes) { 101 try { 102 for (int i = 0; i < bytes.length - 1; i++) { 103 ostream.write(bytes[i]); 104 wait(3); 105 } 106 final byte endbyte = bytes[bytes.length - 1]; 107 ostream.write(endbyte); 108 } catch (java.io.IOException e) { 109 log.error("Exception on output: ", e); 110 } catch (java.lang.InterruptedException e) { 111 Thread.currentThread().interrupt(); // retain if needed later 112 log.error("Interrupted output: ", e); 113 } 114 } 115 116 // base class methods for the PortController interface 117 @Override 118 public boolean status() { 119 return opened; 120 } 121 122 /** 123 * {@inheritDoc} 124 */ 125 @Override 126 public String[] validBaudRates() { 127 return Arrays.copyOf(validSpeeds, validSpeeds.length); 128 } 129 130 /** 131 * {@inheritDoc} 132 */ 133 @Override 134 public int[] validBaudNumbers() { 135 return Arrays.copyOf(validSpeedValues, validSpeedValues.length); 136 } 137 138 protected String[] validSpeeds = new String[]{Bundle.getMessage("Baud115200")}; 139 protected int[] validSpeedValues = new int[]{115200}; 140 141 @Override 142 public int defaultBaudIndex() { 143 return 0; 144 } 145 146 String[] validOptions1 = new String[]{Bundle.getMessage("Version1Choice"), Bundle.getMessage("Version2Choice")}; 147 148 /** 149 * Set the second port option. 150 */ 151 @Override 152 public void configureOption1(String value) { 153 setOptionState(option1Name, value); 154 if (value.equals(validOptions1[0])) { 155 version = 1; 156 } else if (value.equals(validOptions1[1])) { 157 version = 2; 158 } 159 } 160 161 // private control members 162 int[] offsetArray = null; 163 164 // code for handling the input characters 165 Thread readerThread; 166 167 // flag for protocol version 168 int version = 1; 169 170 /** 171 * Internal class to handle the separate character-receive thread. 172 */ 173 class Reader implements Runnable { 174 175 /** 176 * Handle incoming characters. This is a permanent loop, looking for 177 * input messages in character form on the stream connected to the 178 * PortController via <code>connectPort</code>. Terminates with the 179 * input stream breaking out of the try block. 180 */ 181 @Override 182 public void run() { 183 // have to limit verbosity! 184 185 while (true) { // loop permanently, stream close will exit via exception 186 try { 187 handleIncomingData(); 188 } catch (java.io.IOException e) { 189 log.warn("run: Exception: ", e); 190 } 191 } 192 } 193 194 static final int maxMsg = 200; 195 StringBuffer msg; 196 String msgString; 197 198 void handleIncomingData() throws java.io.IOException { 199 // we sit in this until the message is complete, relying on 200 // threading to let other stuff happen 201 202 // Create output message 203 msg = new StringBuffer(maxMsg); 204 // message exists, now fill it 205 int i; 206 for (i = 0; i < maxMsg; i++) { 207 char char1; 208 209 synchronized (SerialAdapter.this) { 210 char1 = (char) serialStream.readByte(); 211 } 212 213 if (char1 == 13) { // 13 is the CR at the end; done this 214 // way to be coding-independent 215 break; 216 } 217 // Strip off the CR and LF 218 if (char1 != 10) { 219 msg.append(char1); 220 } 221 } 222 223 // create the String to display (as String has .equals) 224 msgString = msg.toString(); 225 log.debug("Msg <{}>", msgString); 226 227 // return a notification via the queue to ensure end 228 Runnable r = new Runnable() { 229 230 // retain a copy of the message at startup 231 String msgForLater = msgString; 232 233 @Override 234 public void run() { 235 nextLine(msgForLater); 236 } 237 }; 238 javax.swing.SwingUtilities.invokeLater(r); 239 } 240 241 } // end class Reader 242 243 /** 244 * Handle a new line from the device. 245 * <p> 246 * This needs to execute on the Swing GUI thread. It forwards via the 247 * Distributor object. 248 * 249 * @param s The new message to distribute 250 */ 251 @InvokeOnGuiThread 252 protected void nextLine(String s) { 253 // check for startup lines we ignore 254 if (s.length() < 5) { 255 return; 256 } 257 if (s.startsWith("DATA,,")) { 258 // skip reply to A 259 setReceivers(s); 260 return; 261 } 262 263 Reading r; 264 try { 265 r = makeReading(s); 266 } catch (IOException e) { 267 log.error("Exception formatting input line \"{}\": ", s, e); 268 // r = new Reading(-1, new double[]{-1, -1, -1, -1} ); 269 // skip handling this line 270 return; 271 } 272 273 if (r == null) { 274 return; // nothing useful 275 } 276 // forward 277 try { 278 Distributor.instance().submitReading(r); 279 } catch (Exception e) { 280 log.error("Exception forwarding reading: ", e); 281 } 282 } 283 284 /** 285 * Handle the message which lists the receiver numbers. Just makes an array 286 * of those, which is not actually used. 287 * @param s Input line 288 */ 289 void setReceivers(String s) { 290 try { 291 // parse string 292 CSVParser c = CSVParser.parse(s, CSVFormat.DEFAULT); 293 CSVRecord r = c.getRecords().get(0); 294 c.close(); 295 296 // first two are "Data, ," so are ignored. 297 // rest are receiver numbers 298 // Find how many 299 int n = r.size() - 2; 300 log.debug("Found {} receivers", n); 301 302 // find max receiver number 303 int max = Integer.parseInt(r.get(r.size() - 1)); 304 log.debug("Highest receiver address is {}", max); 305 306 offsetArray = new int[n]; 307 for (int i = 0; i < n; i++) { 308 offsetArray[i] = Integer.parseInt(r.get(i + 2)); 309 } 310 311 } catch (IOException e) { 312 log.debug("Did not handle init message <{}> due to {}", s, e); 313 } 314 } 315 316 static private final int SKIPCOLS = 0; // used to skip DATA,TIME; was there a trailing "'"? 317 private boolean first = true; 318 319 /** 320 * Convert input line to Reading object. 321 * @param s The line of input 322 * @return A Reading object with content parsed from the input line 323 * @throws IOException from underlying I/O 324 */ 325 Reading makeReading(String s) throws IOException { 326 if (first) { 327 log.info("RPS starts, using protocol version {}", version); 328 first = false; 329 } 330 331 if (version == 1) { 332 // parse string 333 CSVParser c = CSVParser.parse(s, CSVFormat.DEFAULT); 334 CSVRecord record = c.getRecords().get(0); 335 c.close(); 336 337 // values are stored in 1-N of the output array; 0 not used 338 int count = record.size() - SKIPCOLS; 339 double[] vals = new double[count + 1]; 340 for (int i = 1; i < count + 1; i++) { 341 vals[i] = Double.valueOf(record.get(i + SKIPCOLS - 1)); 342 } 343 344 return new Reading(Engine.instance().getPolledID(), vals, s); 345 } else if (version == 2) { 346 // parse string 347 CSVParser c = CSVParser.parse(s, CSVFormat.DEFAULT); 348 CSVRecord record = c.getRecords().get(0); 349 c.close(); 350 351 int count = (record.size() - 2) / 2; // skip 'ADR, DAT,' 352 double[] vals = new double[Engine.instance().getMaxReceiverNumber() + 1]; // Receiver 2 goes in element 2 353 for (int i = 0; i < vals.length; i++) { 354 vals[i] = 0.0; 355 } 356 try { 357 for (int i = 0; i < count; i++) { // i is zero-based count of input pairs 358 int index = Integer.parseInt(record.get(2 + i * 2)); // index is receiver number 359 // numbers are from one for valid receivers 360 // the null message starts with index zero 361 if (index < 0) { 362 continue; 363 } 364 if (index >= vals.length) { // data for undefined Receiver 365 log.warn("Data from unexpected receiver {}, creating receiver", index); 366 Engine.instance().setMaxReceiverNumber(index + 1); 367 // 368 // Originally, we made vals[] longer if we got 369 // a response from an unexpected receiver. 370 // This caused terrible trouble at Kesen's layout, 371 // so was commented-out here. 372 // 373 //double[] temp = new double[index+1]; 374 //for (int j = 0; j<vals.length; j++) temp[j] = vals[j]; 375 //vals = temp; 376 } 377 if (index < vals.length) { 378 vals[index] = Double.valueOf(record.get(2 + i * 2 + 1)); 379 } 380 } 381 } catch (NumberFormatException e) { 382 log.warn("Exception handling input.", e); 383 return null; 384 } 385 return new Reading(Engine.instance().getPolledID(), vals, s); 386 } else { 387 log.error("can't handle version {}", version); 388 return null; 389 } 390 } 391 392 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SerialAdapter.class); 393 394}