001package jmri.jmrix.jserialcomm; 002 003import jmri.jmrix.*; 004 005import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 006 007import java.io.*; 008import java.util.Set; 009import java.util.Vector; 010import java.util.regex.Pattern; 011import java.util.stream.Collectors; 012import java.util.stream.Stream; 013 014import jmri.SystemConnectionMemo; 015import jmri.util.SystemType; 016 017/** 018 * Implementation of serial port using jSerialComm. 019 * 020 * @author Daniel Bergqvist (C) 2024 021 */ 022public class JSerialPort implements SerialPort { 023 024// public static final int LISTENING_EVENT_DATA_AVAILABLE = com.fazecast.jSerialComm.SerialPort.LISTENING_EVENT_DATA_AVAILABLE; 025// public static final int ONE_STOP_BIT = com.fazecast.jSerialComm.SerialPort.ONE_STOP_BIT; 026// public static final int NO_PARITY = com.fazecast.jSerialComm.SerialPort.NO_PARITY; 027 private final com.fazecast.jSerialComm.SerialPort serialPort; 028 029 /*.* 030 * Enumerate the possible parity choices 031 *./ 032 public enum Parity { 033 NONE(com.fazecast.jSerialComm.SerialPort.NO_PARITY), 034 EVEN(com.fazecast.jSerialComm.SerialPort.EVEN_PARITY), 035 ODD(com.fazecast.jSerialComm.SerialPort.ODD_PARITY); 036 037 private final int value; 038 039 Parity(int value) { 040 this.value = value; 041 } 042 043 public int getValue() { 044 return value; 045 } 046 047 public static Parity getParity(int parity) { 048 for (Parity p : Parity.values()) { 049 if (p.value == parity) { 050 return p; 051 } 052 } 053 throw new IllegalArgumentException("Unknown parity"); 054 } 055 } 056*/ 057 private JSerialPort(com.fazecast.jSerialComm.SerialPort serialPort) { 058 this.serialPort = serialPort; 059 } 060 061 @Override 062 public void addDataListener(SerialPortDataListener listener) { 063 this.serialPort.addDataListener(new com.fazecast.jSerialComm.SerialPortDataListener() { 064 @Override 065 public int getListeningEvents() { 066 return listener.getListeningEvents(); 067 } 068 069 @Override 070 public void serialEvent(com.fazecast.jSerialComm.SerialPortEvent event) { 071 listener.serialEvent(new JSerialPortEvent(event)); 072 } 073 }); 074 } 075 076 @Override 077 public InputStream getInputStream() { 078 return this.serialPort.getInputStream(); 079 } 080 081 @Override 082 public OutputStream getOutputStream() { 083 return this.serialPort.getOutputStream(); 084 } 085 086 @Override 087 public void setRTS() { 088 this.serialPort.setRTS(); 089 } 090 091 @Override 092 public void clearRTS() { 093 this.serialPort.clearRTS(); 094 } 095 096 @Override 097 public void setBaudRate(int baudrate) { 098 this.serialPort.setBaudRate(baudrate); 099 } 100 101 @Override 102 public int getBaudRate() { 103 return this.serialPort.getBaudRate(); 104 } 105 106 @Override 107 public void setNumDataBits(int bits) { 108 this.serialPort.setNumDataBits(bits); 109 } 110 111 @Override 112 public final int getNumDataBits() { 113 return serialPort.getNumDataBits(); 114 } 115 116 @Override 117 public void setNumStopBits(int bits) { 118 this.serialPort.setNumStopBits(bits); 119 } 120 121 @Override 122 public final int getNumStopBits() { 123 return serialPort.getNumStopBits(); 124 } 125 126 @Override 127 public void setParity(Parity parity) { 128 serialPort.setParity(parity.getValue()); // constants are defined with values for the specific port class 129 } 130 131 @Override 132 public Parity getParity() { 133 return Parity.getParity(serialPort.getParity()); // constants are defined with values for the specific port class 134 } 135 136 @Override 137 public void setDTR() { 138 this.serialPort.setDTR(); 139 } 140 141 @Override 142 public void clearDTR() { 143 this.serialPort.clearDTR(); 144 } 145 146 @Override 147 public boolean getDTR() { 148 return this.serialPort.getDTR(); 149 } 150 151 @Override 152 public boolean getRTS() { 153 return this.serialPort.getRTS(); 154 } 155 156 @Override 157 public boolean getDSR() { 158 return this.serialPort.getDSR(); 159 } 160 161 @Override 162 public boolean getCTS() { 163 return this.serialPort.getCTS(); 164 } 165 166 @Override 167 public boolean getDCD() { 168 return this.serialPort.getDCD(); 169 } 170 171 @Override 172 public boolean getRI() { 173 return this.serialPort.getRI(); 174 } 175 176 /** 177 * Configure the flow control settings. Keep this in synch with the 178 * FlowControl enum. 179 * 180 * @param flow set which kind of flow control to use 181 */ 182 @Override 183 public final void setFlowControl(AbstractSerialPortController.FlowControl flow) { 184 boolean result = true; 185 if (null == flow) { 186 log.error("Invalid null FlowControl enum member"); 187 } else { 188 switch (flow) { 189 case RTSCTS: 190 result = serialPort.setFlowControl(com.fazecast.jSerialComm.SerialPort.FLOW_CONTROL_RTS_ENABLED | com.fazecast.jSerialComm.SerialPort.FLOW_CONTROL_CTS_ENABLED); 191 break; 192 case XONXOFF: 193 result = serialPort.setFlowControl(com.fazecast.jSerialComm.SerialPort.FLOW_CONTROL_XONXOFF_IN_ENABLED | com.fazecast.jSerialComm.SerialPort.FLOW_CONTROL_XONXOFF_OUT_ENABLED); 194 break; 195 case NONE: 196 result = serialPort.setFlowControl(com.fazecast.jSerialComm.SerialPort.FLOW_CONTROL_DISABLED); 197 break; 198 default: 199 log.error("Invalid FlowControl enum member: {}", flow); 200 break; 201 } 202 } 203 if (!result) { 204 log.error("Port did not accept flow control setting {}", flow); 205 } 206 } 207 208 @Override 209 public void setBreak() { 210 this.serialPort.setBreak(); 211 } 212 213 @Override 214 public void clearBreak() { 215 this.serialPort.clearBreak(); 216 } 217 218 @Override 219 public final int getFlowControlSettings() { 220 return serialPort.getFlowControlSettings(); 221 } 222 223 @Override 224 public final boolean setComPortTimeouts(int newTimeoutMode, int newReadTimeout, int newWriteTimeout) { 225 return serialPort.setComPortTimeouts(newTimeoutMode, newReadTimeout, newWriteTimeout); 226 } 227 228 @Override 229 public void closePort() { 230 this.serialPort.closePort(); 231 } 232 233 @Override 234 public String getDescriptivePortName() { 235 return this.serialPort.getDescriptivePortName(); 236 } 237 238 @Override 239 public String toString() { 240 return this.serialPort.toString(); 241 } 242 243 /** 244 * Open the port. 245 * 246 * @param memo the system memo 247 * @param inputPortName local system name for the desired port 248 * @param log Logger to use for errors, passed so that errors are logged from low-level class' 249 * @param stop_bits The number of stop bits, either 1 or 2 250 * @param parity one of the defined parity contants 251 * @return the serial port object for later use 252 */ 253 public static JSerialPort activatePort( 254 SystemConnectionMemo memo, 255 String inputPortName, 256 org.slf4j.Logger log, 257 int stop_bits, 258 Parity parity) { 259 260 com.fazecast.jSerialComm.SerialPort serialPort; 261 262 // check environment for overriding portName 263 String portName; 264 final String envVar = "JMRI_SERIALPORT"; 265 String fromEnv = System.getenv(envVar); 266 log.debug("Environment {} {} was {}", envVar, fromEnv, inputPortName); 267 String fromProp = System.getProperty(envVar); 268 log.debug("Property {} {} was {}", envVar, fromProp, inputPortName); 269 if (fromEnv != null ) { 270 jmri.util.LoggingUtil.infoOnce(log,"{} set, using environment \"{}\" as Port Name", envVar, fromEnv); 271 portName = fromEnv; 272 } else if (fromProp != null ) { 273 jmri.util.LoggingUtil.infoOnce(log,"{} set, using property \"{}\" as Port Name", envVar, fromProp); 274 portName = fromProp; 275 } else { 276 portName = inputPortName; 277 } 278 279 // convert the 1 or 2 stop_bits argument to the proper jSerialComm code value 280 int stop_bits_code; 281 switch (stop_bits) { 282 case 1: 283 stop_bits_code = com.fazecast.jSerialComm.SerialPort.ONE_STOP_BIT; 284 break; 285 case 2: 286 stop_bits_code = com.fazecast.jSerialComm.SerialPort.TWO_STOP_BITS; 287 break; 288 default: 289 throw new IllegalArgumentException("Incorrect stop_bits argument: " + stop_bits); 290 } 291 try { 292 serialPort = com.fazecast.jSerialComm.SerialPort.getCommPort(portName); 293 serialPort.openPort(); 294 serialPort.setComPortTimeouts(com.fazecast.jSerialComm.SerialPort.TIMEOUT_READ_BLOCKING, 0, 0); 295 serialPort.setNumDataBits(8); 296 serialPort.setNumStopBits(stop_bits_code); 297 serialPort.setParity(parity.getValue()); 298 AbstractPortController.purgeStream(serialPort.getInputStream()); 299 } catch (java.io.IOException | com.fazecast.jSerialComm.SerialPortInvalidPortException ex) { 300 // IOException includes 301 // com.fazecast.jSerialComm.SerialPortIOException 302 AbstractSerialPortController.handlePortNotFound(memo, portName, log, ex); 303 return null; 304 } 305 return new JSerialPort(serialPort); 306 } 307 308 private static String getSymlinkTarget(File symlink) { 309 try { 310 // Path.toRealPath() follows a symlink 311 return symlink.toPath().toRealPath().toFile().getName(); 312 } catch (IOException e) { 313 return null; 314 } 315 } 316 317 /** 318 * Provide the actual serial port names. 319 * As a public static method, this can be accessed outside the jmri.jmrix 320 * package to get the list of names for e.g. context reports. 321 * 322 * @return the port names in the form they can later be used to open the port 323 */ 324 // @SuppressWarnings("UseOfObsoleteCollectionType") // historical interface 325 @SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME") 326 public static Vector<String> getActualPortNames() { 327 // first, check that the comm package can be opened and ports seen 328 java.util.Vector<java.lang.String> portNameVector = new Vector<String>(); 329 com.fazecast.jSerialComm.SerialPort[] portIDs = com.fazecast.jSerialComm.SerialPort.getCommPorts(); 330 // find the names of suitable ports 331 for (com.fazecast.jSerialComm.SerialPort portID : portIDs) { 332 portNameVector.addElement(portID.getSystemPortName()); 333 } 334 // On Linux and Mac, try to find symlinks and to use the system property purejavacomm.portnamepattern 335 if (SystemType.isLinux() || SystemType.isMacOSX()) { 336 File[] files = new File("/dev").listFiles(); 337 if (files != null ) { 338 // Find symlinks linked to real ports 339 Set<String> symlinkPorts = Stream.of(files).filter(file -> !file.isDirectory() 340 && portNameVector.contains(getSymlinkTarget(file)) 341 && !portNameVector.contains(file.getName())).map(File::getName).collect(Collectors.toSet()); 342 portNameVector.addAll(symlinkPorts); 343 if (symlinkPorts.size() > 0) { 344 log.info("Adding symlink port {}", symlinkPorts); 345 } 346 347 // Let the user add additional serial ports 348 String portnamePattern = System.getProperty("purejavacomm.portnamepattern"); 349 if (portnamePattern != null) { 350 Pattern pattern = Pattern.compile(portnamePattern); 351 Set<String> ports = Stream.of(files).filter(file -> !file.isDirectory() 352 && pattern.matcher(file.getName()).matches() 353 && !portNameVector.contains(file.getName())).map(File::getName).collect(Collectors.toSet()); 354 portNameVector.addAll(ports); 355 log.info("Adding user-specified ports {} matching pattern {}", ports, portnamePattern); 356 } 357 } 358 } 359 return portNameVector; 360 } 361 362 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JSerialPort.class); 363}