001/**
002 * 
003 */
004package org.openimaj.hardware.serial;
005
006import gnu.io.CommPort;
007import gnu.io.CommPortIdentifier;
008import gnu.io.PortInUseException;
009import gnu.io.SerialPort;
010
011import java.io.BufferedOutputStream;
012import java.io.File;
013import java.io.FileOutputStream;
014import java.io.IOException;
015import java.io.InputStream;
016import java.io.OutputStream;
017import java.lang.reflect.Field;
018import java.net.URL;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Enumeration;
024import java.util.HashSet;
025import java.util.List;
026
027import org.bridj.Platform;
028
029/**
030 *      Serial device driver. Uses RXTX library underneath. The native parts of
031 *      the RXTX library are published to the Maven repository as a JAR and
032 *      are extracted and the java.library.path property is flushed and reset.
033 * 
034 *  @author David Dupplaw (dpd@ecs.soton.ac.uk)
035 *      
036 *      @created 12 Jul 2011
037 */
038public class SerialDevice implements SerialDataListener
039{
040        /** The RXTX serial port we'll be reading from */
041        private SerialPort serialPort = null;
042        
043        /** Listeners for data events coming from the serial port */ 
044        private List<SerialDataListener> listeners = new ArrayList<SerialDataListener>();
045        
046        /** The regular expression used to split incoming data for the listeners */
047        private String regex = "\n";
048        
049        /** The input stream for the port */
050        private InputStream inputStream = null;
051        
052        /** The output stream for the port */
053        private OutputStream outputStream = null;
054
055        /** The serial reader used to buffer and parse incoming data */
056        private SerialReader serialReader = null;
057
058        /** The parser being used to parse incoming data */
059        private RegExParser regexParser = null;
060        
061        // Static block that loads the native libraries
062        static 
063        {
064                // The package in the JAR where we've stored the native libs
065                String libprefix = "/org/openimaj/driver/serial/native/";
066
067                // Try to find out if we've got a library to load
068                String libraryResource = null;
069                for( String s : getEmbeddedLibraryResource("rxtxSerial") ) 
070                {
071                        URL r = SerialDevice.class.getResource( libprefix + s );
072                        if( r != null ) 
073                        {
074                                libraryResource = libprefix + s;
075                                break;
076                        }
077                }
078
079                // Stop if we can't find the library
080                if( libraryResource == null ) 
081                        throw new RuntimeException("Unable to load platform library");
082
083                String directory = null;
084                try 
085                {
086                        // Extract the library from the JAR and find the directory into which it was put
087                        // ... it's this we'll add as the library path
088                        File file = extractEmbeddedLibraryResource(libraryResource);
089                        directory = file.getAbsoluteFile().getParent();
090                } 
091                catch (IOException e) 
092                {
093                        throw new RuntimeException("Error unpacking platform library");
094                }
095
096                // http://blog.cedarsoft.com/2010/11/setting-java-library-path-programmatically/
097                try
098        {
099                // Flush the paths in the Classloader
100                System.setProperty( "java.library.path", directory );
101                
102                Field fieldSysPath = ClassLoader.class.getDeclaredField( "sys_paths" );
103                fieldSysPath.setAccessible( true );
104                fieldSysPath.set( null, null );
105        }
106        catch( SecurityException e )
107        {
108                e.printStackTrace();
109        }
110        catch( IllegalArgumentException e )
111        {
112                e.printStackTrace();
113        }
114        catch( NoSuchFieldException e )
115        {
116                e.printStackTrace();
117        }
118        catch( IllegalAccessException e )
119        {
120                e.printStackTrace();
121        }
122        }
123
124        /**
125         *      Returns a singleton list containing the library name for
126         *      the given platform.
127         *  
128         *  @param name The name of the library.
129         *  @return The library name in a singleton list.
130         */
131        static Collection<String> getEmbeddedLibraryResource(String name) 
132        {
133                if( Platform.isWindows() )
134                        return Collections.singletonList((Platform.is64Bits() ? "win64/" : "win32/") + name + ".dll");
135                
136                if( Platform.isMacOSX() )
137                {
138                        String generic = "darwin_universal/lib" + name + ".jnilib";
139                        
140                        if (Platform.isAmd64Arch())
141                                        return Arrays.asList("darwin_x64/lib" + name + ".jnilib", generic);
142                        else    return Collections.singletonList(generic);
143                }
144                
145                if( Platform.isLinux() )
146                        return Collections.singletonList(
147                                        (Platform.is64Bits() ? "linux_x64/lib" : "linux_x86/lib") 
148                                                + name + ".so");
149
150                throw new RuntimeException("Platform not supported ! " +
151                                "(os.name='" + System.getProperty("os.name") + 
152                                "', os.arch='" + System.getProperty("os.arch") + "')");
153        }
154
155        /**
156         *      Copies the file from the resource into a temporary file.
157         * 
158         *  @param libraryResource The library resource to copy
159         *  @return The file where the resource was copied to.
160         *  @throws IOException If the file could not be read or written to.
161         */
162        static File extractEmbeddedLibraryResource( String libraryResource ) 
163                throws IOException 
164        {
165                File libdir = File.createTempFile( new File(libraryResource).getName(), null );
166                libdir.delete();
167                libdir.mkdir();
168                libdir.deleteOnExit();
169                
170                File libFile = new File(libdir, new File(libraryResource).getName());
171                libFile.deleteOnExit();
172                
173                InputStream in = SerialDevice.class.getResourceAsStream(libraryResource);
174                OutputStream out = new BufferedOutputStream(new FileOutputStream(libFile));
175                
176                int len;
177                byte[] b = new byte[8196];
178                while ((len = in.read(b)) > 0)
179                        out.write(b, 0, len);
180                
181                out.close();
182                in.close();
183
184                System.out.println( "Using library "+libraryResource );
185                
186                return libFile;
187        }
188        
189        /**
190         *      Constructor that takes the port name to connect to and the rate at which
191         *      to connect. The data rate will be set to 19,200 with 8 data bits, 1 stop bit
192         *      and no parity.
193         * 
194         *  @param portName The port name to connect to.
195         *  @throws Exception
196         */
197        public SerialDevice( String portName ) throws Exception 
198        {
199                this( portName, 4800, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE );
200        }
201
202        /**
203         *      Complete constructor that takes all the information required to connect to 
204         *      a port.
205         * 
206         *  @param portName The port name to connect to.
207         *  @param dataRate The data rate to read from the port.
208         *  @param dataBits The number of data bits
209         *  @param stopBits The number of stop bits
210         *  @param parity The bit parity
211         *  @throws Exception 
212         */
213        public SerialDevice( String portName, int dataRate, int dataBits, int stopBits, int parity )
214                throws Exception
215        {
216                // Connect to the given port name with a 2 second timeout. 
217                CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier( portName );
218                CommPort commPort = portIdentifier.open( this.getClass().getName(), 2000 );
219
220                // Set the serial port information
221                serialPort = (SerialPort) commPort;
222                serialPort.setSerialPortParams( dataRate, dataBits, stopBits, parity );
223
224                // Get the input and output streams from and to the serial port.
225                outputStream = serialPort.getOutputStream();            
226                inputStream  = serialPort.getInputStream();
227                
228                // Set up our data listener
229                regexParser  = new RegExParser( regex );
230                serialReader = new SerialReader( inputStream, regexParser );
231                serialReader.addSerialDataListener( this );
232                serialPort.addEventListener( serialReader );
233                serialPort.notifyOnDataAvailable(true);
234        }
235        
236        /**
237         *  {@inheritDoc}
238         *  @see java.lang.Object#finalize()
239         */
240        @Override
241        protected void finalize() throws Throwable
242        {
243                serialPort.removeEventListener();
244                serialPort.close();
245            super.finalize();
246        }
247        
248        /**
249         *  Close the connection to the serial port.
250         */
251        public void close()
252        {
253                serialPort.removeEventListener();
254                serialPort.close();
255        }
256        
257        /**
258         *      Add the given {@link SerialDataListener} to the listener list.
259         *  @param sdl The {@link SerialDataListener} to add.
260         */
261        public void addSerialDataListener( SerialDataListener sdl )
262        {
263                listeners.add( sdl );
264        }
265        
266        /**
267         *      Remove the given {@link SerialDataListener} from the listener list
268         *  @param sdl The {@link SerialDataListener} to remove.
269         */
270        public void removeSerialDataListener( SerialDataListener sdl )
271        {
272                listeners.remove( sdl );
273        }
274        
275        /**
276         *      Fires the serial data event when data is received on the port.
277         *  @param data The data that was received
278         */
279        protected void fireSerialDataEvent( String data )
280        {
281                for( SerialDataListener listener: listeners )
282                        listener.dataReceived( data );
283        }
284
285        /**
286     *  Returns the regular expression being used to split incoming strings.
287     *  @return the regular expression being used to split incoming strings.
288     */
289    public String getRegex()
290    {
291        return regex;
292    }
293
294        /**
295     *  Set the regular expression to use to split incoming strings.
296     *  @param regex the regex to split incoming strings
297     */
298    public void setRegex( String regex )
299    {
300        this.regex = regex;
301        this.regexParser.setRegEx( regex );
302    }
303
304        /**
305     *  Returns the input stream for this device.
306     *  @return the input stream
307     */
308    public InputStream getInputStream()
309    {
310        return inputStream;
311    }
312
313        /**
314     *  Returns the output stream for this device.
315     *  @return the output stream
316     */
317    public OutputStream getOutputStream()
318    {
319        return outputStream;
320    }
321
322    /**
323     *  {@inheritDoc}
324     *  @see org.openimaj.hardware.serial.SerialDataListener#dataReceived(java.lang.String)
325     */
326        @Override
327        public void dataReceived( String data )
328    {
329                fireSerialDataEvent( data );
330    }
331        
332        /**
333         * @return A HashSet containing the CommPortIdentifier for all serial ports
334         *         that are not currently being used.
335         */
336        public static HashSet<CommPortIdentifier> getAvailableSerialPorts()
337        {
338                HashSet<CommPortIdentifier> h = new HashSet<CommPortIdentifier>();
339                Enumeration<?> thePorts = CommPortIdentifier.getPortIdentifiers();
340                while( thePorts.hasMoreElements() )
341                {
342                        CommPortIdentifier com = (CommPortIdentifier)thePorts.nextElement();
343                        switch (com.getPortType())
344                        {
345                        case CommPortIdentifier.PORT_SERIAL:
346                                try
347                                {
348                                        CommPort thePort = com.open( "CommUtil", 50 );
349                                        thePort.close();
350                                        h.add( com );
351                                }
352                                catch( PortInUseException e )
353                                {
354                                        System.out.println( "Port, " + com.getName()
355                                                + ", is in use." );
356                                }
357                                catch( Exception e )
358                                {
359                                        System.err.println( "Failed to open port " + com.getName() );
360                                        e.printStackTrace();
361                                }
362                        }
363                }
364                return h;
365        }
366}