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