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 portName 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 portName, org.slf4j.Logger log, int stop_bits, Parity parity) {
253        com.fazecast.jSerialComm.SerialPort serialPort;
254        // convert the 1 or 2 stop_bits argument to the proper jSerialComm code value
255        int stop_bits_code;
256        switch (stop_bits) {
257            case 1:
258                stop_bits_code = com.fazecast.jSerialComm.SerialPort.ONE_STOP_BIT;
259                break;
260            case 2:
261                stop_bits_code = com.fazecast.jSerialComm.SerialPort.TWO_STOP_BITS;
262                break;
263            default:
264                throw new IllegalArgumentException("Incorrect stop_bits argument: " + stop_bits);
265        }
266        try {
267            serialPort = com.fazecast.jSerialComm.SerialPort.getCommPort(portName);
268            serialPort.openPort();
269            serialPort.setComPortTimeouts(com.fazecast.jSerialComm.SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
270            serialPort.setNumDataBits(8);
271            serialPort.setNumStopBits(stop_bits_code);
272            serialPort.setParity(parity.getValue());
273            AbstractPortController.purgeStream(serialPort.getInputStream());
274        } catch (java.io.IOException | com.fazecast.jSerialComm.SerialPortInvalidPortException ex) {
275            // IOException includes
276            //      com.fazecast.jSerialComm.SerialPortIOException
277            AbstractSerialPortController.handlePortNotFound(systemPrefix, portName, log, ex);
278            return null;
279        }
280        return new JSerialPort(serialPort);
281    }
282
283    private static String getSymlinkTarget(File symlink) {
284        try {
285            // Path.toRealPath() follows a symlink
286            return symlink.toPath().toRealPath().toFile().getName();
287        } catch (IOException e) {
288            return null;
289        }
290    }
291
292    /**
293     * Provide the actual serial port names.
294     * As a public static method, this can be accessed outside the jmri.jmrix
295     * package to get the list of names for e.g. context reports.
296     *
297     * @return the port names in the form they can later be used to open the port
298     */
299    //    @SuppressWarnings("UseOfObsoleteCollectionType") // historical interface
300    @SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME")
301    public static Vector<String> getActualPortNames() {
302        // first, check that the comm package can be opened and ports seen
303        java.util.Vector<java.lang.String> portNameVector = new Vector<String>();
304        com.fazecast.jSerialComm.SerialPort[] portIDs = com.fazecast.jSerialComm.SerialPort.getCommPorts();
305        // find the names of suitable ports
306        for (com.fazecast.jSerialComm.SerialPort portID : portIDs) {
307            portNameVector.addElement(portID.getSystemPortName());
308        }
309        // On Linux and Mac, try to find symlinks and to use the system property purejavacomm.portnamepattern
310        if (SystemType.isLinux() || SystemType.isMacOSX()) {
311            File[] files = new File("/dev").listFiles();
312            if (files != null ) {
313                // Find symlinks linked to real ports
314                Set<String> symlinkPorts = Stream.of(files).filter(file -> !file.isDirectory()
315                        && portNameVector.contains(getSymlinkTarget(file))
316                        && !portNameVector.contains(file.getName())).map(File::getName).collect(Collectors.toSet());
317                portNameVector.addAll(symlinkPorts);
318                log.info("Adding symlink port {}", symlinkPorts);
319
320                // Let the user add additional serial ports
321                String portnamePattern = System.getProperty("purejavacomm.portnamepattern");
322                if (portnamePattern != null) {
323                    Pattern pattern = Pattern.compile(portnamePattern);
324                    Set<String> ports = Stream.of(files).filter(file -> !file.isDirectory()
325                            && pattern.matcher(file.getName()).matches()
326                            && !portNameVector.contains(file.getName())).map(File::getName).collect(Collectors.toSet());
327                    portNameVector.addAll(ports);
328                    log.info("Adding user-specified ports {} matching pattern {}", ports, portnamePattern);
329                }
330            }
331        }
332        return portNameVector;
333    }
334
335    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JSerialPort.class);
336}