/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2013, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.data.nmea; import static org.geotoolkit.data.nmea.NMEAFeatureStore.NMEA_TYPE; import static org.geotoolkit.data.nmea.NMEAFeatureStore.TYPE_NAME; import gnu.io.CommPort; import gnu.io.CommPortIdentifier; import gnu.io.SerialPort; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.Enumeration; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.event.EventListenerList; import net.sf.marineapi.nmea.event.SentenceListener; import net.sf.marineapi.nmea.io.SentenceReader; import net.sf.marineapi.nmea.sentence.SentenceValidator; import net.sf.marineapi.provider.event.ProviderListener; import org.apache.sis.storage.DataStoreException; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.logging.Logging; import org.geotoolkit.data.memory.MemoryFeatureStore; /** * Scan serial ports to find GPS data, and then initialize a reader on matching stream. * For further details on how data is read, see {@link NMEABuilder}. * * @author Alexis Manin (Geomatys) */ public class NMEASerialPortReader implements ProviderListener<NMEABuilder.FeatureEvent> { private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.nmea"); /** A timeout used for connexion request on ports */ public static final int CONNEXION_TIMEOUT = 100; public static final int SCAN_TIMEOUT = 5000; public static final int PORT_DSR_TIMEOUT = 30000; public static final int PORT_DSR_PAUSE = 500; private final EventListenerList listeners = new EventListenerList(); private final CommPort port; private InputStream input = null; private WeakReference<MemoryFeatureStore> store = null; private SentenceReader reader = null; private NMEABuilder builder = null; private boolean isReading = false; /** * Scan serial ports in search of NMEA data. If found, a reader is initialized on the found port. * @throws IOException if no NMEA data can be found on any port. * @throws Exception If unexpected problem happend while scanning ports (native libs failed to load, etc.) */ public NMEASerialPortReader() throws Exception { this(scanSerialPorts()); } /** * Initialize reader to listen on given port. If port don't provide NMEA data * when {@link NMEASerialPortReader#read() } method is called, an exception will * be thrown. * @param port The port to read on. */ public NMEASerialPortReader(CommPort port) { ArgumentChecks.ensureNonNull("Port to get data from", port); this.port = port; } /** * Read directly data from given input stream. No port scanning nor port use is done when * the reader is instantiated from this constructor. * @param stream */ public NMEASerialPortReader(InputStream stream) { ArgumentChecks.ensureNonNull("Input stream", stream); port = null; input = stream; } /** * Scan all available ports on the machine to find one which send readable GPS data. * @return The port which give readable data, or throws an exception otherwise. * @throws IOException If we cannot find any port providing NMEA data */ private static CommPort scanSerialPorts() throws IOException { CommPort result = null; final Enumeration e = CommPortIdentifier.getPortIdentifiers(); SerialPort port; Future<Boolean> testResult; ExecutorService es = Executors.newFixedThreadPool(1); while (e.hasMoreElements()) { final CommPortIdentifier portId = (CommPortIdentifier) e.nextElement(); try { if (portId.getPortType() != CommPortIdentifier.PORT_SERIAL) { continue; } port = (SerialPort) portId.open("Geotk reader", CONNEXION_TIMEOUT); port.setSerialPortParams(4800, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); LOGGER.log(Level.INFO, "Scanning port {0}", port.getName()); // Check if we can find GPS data on current port. If true, then we return this port. testResult = es.submit(new PortScanner(port)); if (testResult.get(SCAN_TIMEOUT, TimeUnit.MILLISECONDS)) { result = port; LOGGER.log(Level.FINE, "GPS device have been found on port {0}", port.getName()); break; } } catch (Exception ex) { LOGGER.log(Level.INFO, "Port scan failed.", ex); continue; } } es.shutdown(); if (result == null) { throw new IOException("No NMEA data can be found on scanned serial ports."); } return result; } /** * Start reading data on input port or stream. * @return The {@link MemoryFeatureStore} in which read data will be stored. * @throws IOException If a problem occurs when opening port input stream. */ public MemoryFeatureStore read() throws IOException { // Start GPS measure reading if (input == null && port != null) { input = port.getInputStream(); } reader = new SentenceReader(input); final MemoryFeatureStore fStore = new MemoryFeatureStore(NMEA_TYPE, true); store = new WeakReference<>(fStore); builder = new NMEABuilder(reader); builder.addListener(this); reader.start(); isReading = true; return fStore; } /** * * @return True if the reader is still listening for data on input. False otherwise. */ public boolean isReading() { return isReading; } /** * Close the reader and port / input we've read on. */ public void dispose() { // We use this check because only current method should put the isReading attribute to false. if (!isReading) { return; } LOGGER.log(Level.INFO, "Stop reading on port {0}", port.getName()); builder.removeListener(this); reader.stop(); isReading = false; if (input != null) { try { input.close(); } catch (IOException ex) { LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex); } } if (port != null) { ((SerialPort)port).close(); LOGGER.log(Level.INFO, "Scanned port successfully released."); } fireStop(); } /** * The {@link NMEABuilder} Send us data as it read / interpret it. We try to * add it to output {@link FeatureStore}. If the store has been deleted by user, * we just dispose the reader. * As we can't use properly {@link SentenceListener} because of concurrent modification * exceptions, we use {@link NMEABuilder.FeatureEvent} propagation to know if reader is still * running. if an event (or its data) is null, it means reader has paused or stop. In this case, * we just dispose the reader. * TODO : Maybe try a reconnection pass in the case we are listening to a {@link SerialPort}. * @param evt The event containing data to store. */ @Override public void providerUpdate(NMEABuilder.FeatureEvent evt) { if (store.get() != null && evt != null && evt.getData() != null) { try { store.get().addFeatures(TYPE_NAME.toString(), Collections.singleton(evt.getData())); } catch (DataStoreException ex) { LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex); } } else { dispose(); } } /** * TODO : Correct the following method to allow reconnection to GPS input. * Disfunction seems to originate from the fact that nor DTR nor DSR states are * sufficient to detect that a terminal have been properly reconnected to the listened port. * It seems they are states which come after, so we need to find the state which precisely * describe reconnection. */ public void tryReconnect() { // if (port instanceof SerialPort) { // final SerialPort sp = (SerialPort) port; // LOGGER.log(Level.INFO, "Port connexion interrupted, wait for reconnexion."); // final Thread DSRChecker = new Thread(new Runnable() { // @Override // public void run() { // while (!sp.isDTR()) { // try { // Thread.sleep(PORT_DSR_PAUSE); // } catch (InterruptedException ex) { // // Nothing to do, thread can't wait any longer, and // // we consider connexion is definitely broken. // } // } // } // }); // DSRChecker.start(); // try { // DSRChecker.join(SCAN_TIMEOUT); // DSRChecker.interrupt(); // } catch (InterruptedException ex) { // Exceptions.printStackTrace(ex); // } // // Connexion recovered. // if (sp.isDSR()) { // LOGGER.log(Level.INFO, "Port reconnexion recovered."); // reader.start(); // return; // } // } // If no serial port connexion can be recovered, just stop the reader. LOGGER.log(Level.INFO, "Port connexion have been lost. Dispose reader."); dispose(); } public void addPropertyChangeListener(PropertyChangeListener listener){ listeners.add(PropertyChangeListener.class, listener); } public void removePropertyChangeListener(PropertyChangeListener listener){ listeners.remove(PropertyChangeListener.class, listener); } private void fireStop(){ final PropertyChangeListener[] lsts = listeners.getListeners(PropertyChangeListener.class); for(PropertyChangeListener lst : lsts){ lst.propertyChange(new PropertyChangeEvent(this, "stop", true, false)); } } /** * An utility class for testing repeatedly a given port, searching fo NMEA data. * Operation performed by this thread can be BLOCKING, because of buffered reader * which wait for data from input stream. In consequence, it should be used with * a timeout trigger. */ private static class PortScanner implements Callable<Boolean> { private final CommPort port; public PortScanner(final CommPort toScan) { port = toScan; } @Override public Boolean call() throws Exception { try (BufferedReader bReader = new BufferedReader(new InputStreamReader(port.getInputStream()))) { // Repeat few times, because at first try, we could get bad data. for (int tryCount = 0; tryCount < 7; tryCount++) { String data = bReader.readLine(); if (SentenceValidator.isValid(data)) { return true; } } } catch (Exception e) { LOGGER.log(Level.WARNING, e.getLocalizedMessage()); } return false; } } }