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}