001package jmri.jmrix;
002
003import java.util.Enumeration;
004import java.util.Vector;
005import jmri.SystemConnectionMemo;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008import purejavacomm.CommPortIdentifier;
009import purejavacomm.NoSuchPortException;
010import purejavacomm.PortInUseException;
011import purejavacomm.SerialPort;
012import purejavacomm.SerialPortEvent;
013import purejavacomm.SerialPortEventListener;
014
015/**
016 * Provide an abstract base for *PortController classes.
017 * <p>
018 * This is complicated by the lack of multiple inheritance. SerialPortAdapter is
019 * an Interface, and its implementing classes also inherit from various
020 * PortController types. But we want some common behaviors for those, so we put
021 * them here.
022 *
023 * @see jmri.jmrix.SerialPortAdapter
024 *
025 * @author Bob Jacobsen Copyright (C) 2001, 2002
026 */
027abstract public class AbstractSerialPortController extends AbstractPortController implements SerialPortAdapter {
028
029    protected AbstractSerialPortController(SystemConnectionMemo connectionMemo) {
030        super(connectionMemo);
031    }
032
033    /**
034     * Standard error handling for port-busy case.
035     *
036     * @param p        the exception being handled, if additional information
037     *                 from it is desired
038     * @param portName name of the port being accessed
039     * @param log      where to log a status message
040     * @return Localized message, in case separate presentation to user is
041     *         desired
042     */
043    @Override
044    public String handlePortBusy(PortInUseException p, String portName, Logger log) {
045        log.error("{} port is in use: {}", portName, p.getMessage());
046        /*JOptionPane.showMessageDialog(null, "Port is in use",
047         "Error", JOptionPane.ERROR_MESSAGE);*/
048        ConnectionStatus.instance().setConnectionState(this.getSystemPrefix(), portName, ConnectionStatus.CONNECTION_DOWN);
049        return Bundle.getMessage("SerialPortInUse", portName);
050    }
051
052    /**
053     * Standard error handling for port-not-found case.
054     * @param p no such port exception.
055     * @param portName port name.
056     * @param log system log.
057     * @return human readable string with error detail.
058     */
059    public String handlePortNotFound(NoSuchPortException p, String portName, Logger log) {
060        log.error("Serial port {} not found", portName);
061        /*JOptionPane.showMessageDialog(null, "Serial port "+portName+" not found",
062         "Error", JOptionPane.ERROR_MESSAGE);*/
063        ConnectionStatus.instance().setConnectionState(this.getSystemPrefix(), portName, ConnectionStatus.CONNECTION_DOWN);
064        return Bundle.getMessage("SerialPortNotFound", portName);
065    }
066
067    /**
068     * {@inheritDoc}
069     */
070    @Override
071    public void connect() throws java.io.IOException {
072        openPort(mPort, "JMRI app");
073    }
074
075    /**
076     * {@inheritDoc}
077     */
078    @Override
079    public void setPort(String port) {
080        log.debug("Setting port to {}", port);
081        mPort = port;
082    }
083    protected String mPort = null;
084
085    /**
086     * {@inheritDoc}
087     */
088    @Override
089    public String getCurrentPortName() {
090        if (mPort == null) {
091            if (getPortNames() == null) {
092                // this shouldn't happen in normal operation
093                // but in the tests this can happen if the receive thread has been interrupted
094                log.error("Port names returned as null");
095                return null;
096            }
097            if (getPortNames().size() <= 0) {
098                log.error("No usable ports returned");
099                return null;
100            }
101            return null;
102            // return (String)getPortNames().elementAt(0);
103        }
104        return mPort;
105    }
106
107    /**
108     * Set the control leads and flow control. This handles any necessary
109     * ordering.
110     *
111     * @param serialPort Port to be updated
112     * @param flow       flow control mode from (@link purejavacomm.SerialPort}
113     * @param rts        set RTS active if true
114     * @param dtr        set DTR active if true
115     */
116    protected void configureLeadsAndFlowControl(SerialPort serialPort, int flow, boolean rts, boolean dtr) {
117        // (Jan 2018) PJC seems to mix termios and ioctl access, so it's not clear
118        // what's preserved and what's not. Experimentally, it seems necessary
119        // to write the control leads, set flow control, and then write the control
120        // leads again.
121        serialPort.setRTS(rts);
122        serialPort.setDTR(dtr);
123
124        try {
125            if (flow != purejavacomm.SerialPort.FLOWCONTROL_NONE) {
126                serialPort.setFlowControlMode(flow);
127            }
128        } catch (purejavacomm.UnsupportedCommOperationException e) {
129            log.warn("Could not set flow control, ignoring");
130        }
131        if (flow!=purejavacomm.SerialPort.FLOWCONTROL_RTSCTS_OUT) serialPort.setRTS(rts); // not connected in some serial ports and adapters
132        serialPort.setDTR(dtr);
133    }
134
135    /**
136     * Set the flow control, while also setting RTS and DTR to active.
137     *
138     * @param serialPort Port to be updated
139     * @param flow       flow control mode from (@link purejavacomm.SerialPort}
140     */
141    protected void configureLeadsAndFlowControl(SerialPort serialPort, int flow) {
142        configureLeadsAndFlowControl(serialPort, flow, true, true);
143    }
144
145    /**
146     * {@inheritDoc}
147     */
148    @Override
149    public void configureBaudRate(String rate) {
150        mBaudRate = rate;
151    }
152
153    /**
154     * {@inheritDoc}
155     */
156    @Override
157    public void configureBaudRateFromNumber(String indexString) {
158        int baudNum;
159        int index = 0;
160        final String[] rates = validBaudRates();
161        final int[] numbers = validBaudNumbers();
162        if ((numbers == null) || (numbers.length == 0)) { // simulators return null TODO for SpotBugs make that into an empty array
163            mBaudRate = null;
164            log.debug("no serial port speed values received (OK for simulator)");
165            return;
166        }
167        if (numbers.length != rates.length) {
168            mBaudRate = null;
169            log.error("arrays wrong length in currentBaudNumber: {}, {}", numbers.length, rates.length);
170            return;
171        }
172        if (indexString.isEmpty()) {
173            mBaudRate = null; // represents "(none)"
174            log.debug("empty baud rate received");
175            return;
176        }
177        try {
178            // since 4.16 first try to convert loaded value directly to integer
179            baudNum = Integer.parseInt(indexString); // new storage format, will throw ex on old format
180            log.debug("new profile format port speed value");
181        } catch (NumberFormatException ex) {
182            // old pre 4.15.8 format is i18n string including thousand separator and whatever suffix like "18,600 bps (J1)"
183            log.warn("old profile format port speed value converted");
184            // filter only numerical characters from indexString
185            StringBuilder baudNumber = new StringBuilder();
186            boolean digitSeen = false;
187            for (int n = 0; n < indexString.length(); n++) {
188                if (Character.isDigit(indexString.charAt(n))) {
189                    digitSeen = true;
190                    baudNumber.append(indexString.charAt(n));
191                } else if ((indexString.charAt(n) == ' ') && digitSeen) {
192                    break; // break on first space char encountered after at least 1 digit was found
193                }
194            }
195            if (baudNumber.toString().equals("")) { // no number found in indexString e.g. "(automatic)"
196                baudNum = 0;
197            } else {
198                try {
199                    baudNum = Integer.parseInt(baudNumber.toString());
200                } catch (NumberFormatException e2) {
201                    mBaudRate = null; // represents "(none)"
202                    log.error("error in filtering old profile format port speed value");
203                    return;
204                }
205                log.debug("old format baud number: {}", indexString);
206            }
207        }
208        // fetch baud rate description from validBaudRates[] array copy and set
209        for (int i = 0; i < numbers.length; i++) {
210            if (numbers[i] == baudNum) {
211                index = i;
212                log.debug("found new format baud value at index {}", i);
213                break;
214            }
215        }
216        mBaudRate = validBaudRates()[index];
217        log.debug("mBaudRate set to: {}", mBaudRate);
218    }
219
220    /**
221     * {@inheritDoc}
222     * Invalid indexes are ignored.
223     */
224    @Override
225    public void configureBaudRateFromIndex(int index) {
226        if (validBaudRates().length > index && index > -1 ) {
227            mBaudRate = validBaudRates()[index];
228            log.debug("mBaudRate set by index to: {}", mBaudRate);
229        } else {
230            // expected for simulators extending serialPortAdapter, mBaudRate already null
231            log.debug("no baud rate index {} in array size {}", index, validBaudRates().length);
232        }
233    }
234
235    protected String mBaudRate = null;
236
237    @Override
238    public int defaultBaudIndex() {
239        return -1;
240    }
241
242    /**
243     * {@inheritDoc}
244     */
245    @Override
246    public String getCurrentBaudRate() {
247        if (mBaudRate == null) {
248            return "";
249        }
250        return mBaudRate;
251    }
252
253    /**
254     * {@inheritDoc}
255     */
256    @Override
257    public String getCurrentBaudNumber() {
258        int[] numbers = validBaudNumbers();
259        String[] rates = validBaudRates();
260        if (numbers == null || rates == null || numbers.length != rates.length) { // entries in arrays should correspond
261            return "";
262        }
263        String baudNumString = "";
264        // first try to find the configured baud rate value
265        if (mBaudRate != null) {
266            for (int i = 0; i < numbers.length; i++) {
267                if (rates[i].equals(mBaudRate)) {
268                    baudNumString = Integer.toString(numbers[i]);
269                    break;
270                }
271            }
272        } else if (defaultBaudIndex() > -1) {
273            // use default
274            baudNumString = Integer.toString(numbers[defaultBaudIndex()]);
275            log.debug("using default port speed {}", baudNumString);
276        }
277        log.debug("mBaudRate = {}, matched to string {}", mBaudRate, baudNumString);
278        return baudNumString;
279    }
280
281    @Override
282    public int getCurrentBaudIndex() {
283        if (mBaudRate != null) {
284            String[] rates = validBaudRates();
285            // find the configured baud rate value
286            for (int i = 0; i < rates.length; i++) {
287                if (rates[i].equals(mBaudRate)) {
288                    return i;
289                }
290            }
291        }
292        return defaultBaudIndex(); // default index or -1 if port speed not supported
293    }
294
295    /**
296     * {@inheritDoc}
297     */
298    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
299    justification = "null signals incorrect implementation of portcontroller")
300    @Override
301    public String[] validBaudRates() {
302        log.error("default validBaudRates implementation should not be used", new Exception());
303        return null;
304    }
305
306    /**
307     * {@inheritDoc}
308     */
309    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
310    justification = "null signals incorrect implementation of portcontroller")
311    @Override
312    public int[] validBaudNumbers() {
313        log.error("default validBaudNumbers implementation should not be used", new Exception());
314        return null;
315    }
316
317    /**
318     * Convert a baud rate I18N String to an int number, e.g. "9,600 baud" to 9600.
319     * <p>
320     * Uses the validBaudNumbers() and validBaudRates() methods to do this.
321     *
322     * @param currentBaudRate a rate from validBaudRates()
323     * @return baudrate as integer if available and matching first digits in currentBaudRate,
324     *         0 if baudrate not supported by this adapter,
325     *         -1 if no match (configuration system should prevent this)
326     */
327    public int currentBaudNumber(String currentBaudRate) {
328        String[] rates = validBaudRates();
329        int[] numbers = validBaudNumbers();
330
331        // return if arrays invalid
332        if (numbers == null) {
333            log.error("numbers array null in currentBaudNumber()");
334            return -1;
335        }
336        if (rates == null) {
337            log.error("rates array null in currentBaudNumber()");
338            return -1;
339        }
340        if (numbers.length != rates.length) {
341            log.error("arrays are of different length in currentBaudNumber: {} vs {}", numbers.length, rates.length);
342            return -1;
343        }
344        if (numbers.length < 1) {
345            log.warn("baudrate is not supported by adapter");
346            return 0;
347        }
348        // find the baud rate value
349        for (int i = 0; i < numbers.length; i++) {
350            if (rates[i].equals(currentBaudRate)) {
351                return numbers[i];
352            }
353        }
354
355        // no match
356        log.error("no match to ({}) in currentBaudNumber", currentBaudRate);
357        return -1;
358    }
359
360    /**
361     * Set event logging.
362     * @param port Serial port to configure
363     */
364    protected void setPortEventLogging(SerialPort port) {
365        // arrange to notify later
366        try {
367            port.addEventListener(new SerialPortEventListener() {
368                @Override
369                public void serialEvent(SerialPortEvent e) {
370                    int type = e.getEventType();
371                    switch (type) {
372                        case SerialPortEvent.DATA_AVAILABLE:
373                            log.info("SerialEvent: DATA_AVAILABLE is {}", e.getNewValue()); // NOI18N
374                            return;
375                        case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
376                            log.info("SerialEvent: OUTPUT_BUFFER_EMPTY is {}", e.getNewValue()); // NOI18N
377                            return;
378                        case SerialPortEvent.CTS:
379                            log.info("SerialEvent: CTS is {}", e.getNewValue()); // NOI18N
380                            return;
381                        case SerialPortEvent.DSR:
382                            log.info("SerialEvent: DSR is {}", e.getNewValue()); // NOI18N
383                            return;
384                        case SerialPortEvent.RI:
385                            log.info("SerialEvent: RI is {}", e.getNewValue()); // NOI18N
386                            return;
387                        case SerialPortEvent.CD:
388                            log.info("SerialEvent: CD is {}", e.getNewValue()); // NOI18N
389                            return;
390                        case SerialPortEvent.OE:
391                            log.info("SerialEvent: OE (overrun error) is {}", e.getNewValue()); // NOI18N
392                            return;
393                        case SerialPortEvent.PE:
394                            log.info("SerialEvent: PE (parity error) is {}", e.getNewValue()); // NOI18N
395                            return;
396                        case SerialPortEvent.FE:
397                            log.info("SerialEvent: FE (framing error) is {}", e.getNewValue()); // NOI18N
398                            return;
399                        case SerialPortEvent.BI:
400                            log.info("SerialEvent: BI (break interrupt) is {}", e.getNewValue()); // NOI18N
401                            return;
402                        default:
403                            log.info("SerialEvent of unknown type: {} value: {}", type, e.getNewValue()); // NOI18N
404                    }
405                }
406            }
407            );
408        } catch (java.util.TooManyListenersException ex) {
409            log.warn("cannot set listener for SerialPortEvents; was one already set?");
410        }
411
412        try {
413            port.notifyOnFramingError(true);
414        } catch (Exception e) {
415            log.debug("Could not notifyOnFramingError", e); // NOI18N
416        }
417
418        try {
419            port.notifyOnBreakInterrupt(true);
420        } catch (Exception e) {
421            log.debug("Could not notifyOnBreakInterrupt", e); // NOI18N
422        }
423
424        try {
425            port.notifyOnParityError(true);
426        } catch (Exception e) {
427            log.debug("Could not notifyOnParityError", e); // NOI18N
428        }
429
430        try {
431            port.notifyOnOverrunError(true);
432        } catch (Exception e) {
433            log.debug("Could not notifyOnOverrunError", e); // NOI18N
434        }
435
436        port.notifyOnCarrierDetect(true);
437        port.notifyOnCTS(true);
438        port.notifyOnDSR(true);
439    }
440
441    Vector<String> portNameVector = null;
442
443    /**
444     * {@inheritDoc}
445     */
446    @Override
447    public Vector<String> getPortNames() {
448        //reloadDriver(); // Refresh the list of communication ports
449        // first, check that the comm package can be opened and ports seen
450        portNameVector = new Vector<String>();
451        Enumeration<CommPortIdentifier> portIDs = CommPortIdentifier.getPortIdentifiers();
452        // find the names of suitable ports
453        while (portIDs.hasMoreElements()) {
454            CommPortIdentifier id = portIDs.nextElement();
455            // filter out line printers
456            if (id.getPortType() != CommPortIdentifier.PORT_PARALLEL) // accumulate the names in a vector
457            {
458                portNameVector.addElement(id.getName());
459            }
460        }
461        return portNameVector;
462    }
463
464    /**
465     * {@inheritDoc}
466     * Each serial port adapter should handle this and it should be abstract.
467     */
468    @Override
469    protected void closeConnection(){}
470
471    /**
472     * Re-setup the connection.
473     * Called when the physical connection has reconnected and can be linked to
474     * this connection.
475     * Each port adapter should handle this and it should be abstract.
476     */
477    @Override
478    protected void resetupConnection(){}
479
480    /**
481     * {@inheritDoc}
482     * Attempts a re-connection to the serial port from the main reconnect
483     * thread.
484     */
485    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
486        justification="I18N of Info Message")
487    @Override
488    protected void reconnectFromLoop(int retryNum){
489        try {
490            log.info("Retrying Connection attempt {} for {}", retryNum,mPort);
491            Enumeration<CommPortIdentifier> portIDs = CommPortIdentifier.getPortIdentifiers();
492            while (portIDs.hasMoreElements()) {
493                CommPortIdentifier id = portIDs.nextElement();
494                // filter out line printers
495                if (id.getPortType() != CommPortIdentifier.PORT_PARALLEL) // accumulate the names in a vector
496                {
497                    if (id.getName().equals(mPort)) {
498                        log.info(Bundle.getMessage("ReconnectPortReAppear", mPort));
499                        openPort(mPort, "jmri");
500                    }
501                }
502            }
503            if (retryNum % 10==0) {
504                log.info(Bundle.getMessage("ReconnectSerialTip"));
505            }
506        } catch (RuntimeException e) {
507            log.warn(Bundle.getMessage("ReconnectFail",(mPort == null ? "null" : mPort)));
508
509        }
510    }
511
512    private final static Logger log = LoggerFactory.getLogger(AbstractSerialPortController.class);
513
514}