ffi-serial 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 44fd8d0003d2e47f4722d3db91858c0ef47895ae
4
+ data.tar.gz: ad008840aa913c6f549173132911f9e4675cba98
5
+ SHA512:
6
+ metadata.gz: aa8df84c7b52849bc7e51dc3f1c30ea2c1566f8de0c9dbe2082ab931816daa8e949a1444a04dea90e7b8a7ae7d61f2eba97e8f261876ef3684188414eaf64104
7
+ data.tar.gz: 0ced23dda4e32e280b99473331bc8d55d7f871a5b72fe4cec3fa8b1790a95bcbe549718a7c7ed755a4e54f4046f5902c8c3f77918d1368a1aef962d3d290ce5f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Johan van der Vyver
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ ## FFI Serial
2
+ FFI Serial is a simple OS independent gem to allow access to a serial port
3
+
4
+ ## Why?
5
+ Other gems exist, why this gem?
6
+
7
+ 1. Uses FFI to negate the need for native compilation
8
+ 2. Simply acts as a configurator for Ruby IO objects
9
+
10
+ ## Why FFI?
11
+ FFI is very widely supported at this point.
12
+ By making use of FFI a lot of native compilation concerns go away.
13
+
14
+ ## Why IO?
15
+ Serial ports are simply files, in both Posix and Windows, that have special API calls to configure the serial port settings.
16
+
17
+ Ruby IO provides a rich API and it is part of standard library.
18
+ Using IO, this gem benefits from everything Ruby IO provides.
19
+ No modification is made to IO nor does this simply emulate IO.
20
+
21
+ 99% of the code in this gem is to call the native operating system functions to configure the IO object serial port settings
22
+
23
+ ## Installation
24
+ gem install ffi-serial
25
+
26
+ ## Usage
27
+ require 'ffi-serial'
28
+
29
+ # Defaults for baud, data_bits, stop_bits and parity
30
+ port = Serial.new port: '/dev/ttyUSB0' #=> <Serial:/dev/ttyUSB0>
31
+
32
+ # Get configured settings from OS
33
+ port.baud #=> 9600
34
+ port.data_bits #=> 8
35
+ port.stop_bits #=> 1
36
+ port.parity #=> :none
37
+
38
+ # Really is a Ruby IO
39
+ port.is_a?(IO) #=> true
40
+ port.is_a?(File) #=> true
41
+
42
+ port.read_nonblock(512) #=> ... <supported in Windows>
43
+ port.read #=> ...
44
+ port.readpartial(512) #=> ...
45
+ port.write "\n" #=> 1
46
+ # etc.
47
+
48
+ port.close #=> nil
49
+
50
+ # Explicit configuration (and works on Windows)
51
+ port = Serial.new port: 'COM1', data_bits: 8, stop_bits: 1, parity: :none #=> <Serial:COM1>
52
+
53
+ See Ruby standard library IO for complete method list
54
+ http://ruby-doc.org/core-1.9.3/IO.html
@@ -0,0 +1,49 @@
1
+ module Serial #:nodoc:
2
+ module Posix #:nodoc:
3
+ # Values generated using a FreeBSD 10 instance on EC2
4
+
5
+ module LIBC #:nodoc:
6
+ require 'ffi'
7
+
8
+ def self.os_specific_constants #:nodoc:
9
+ { 'BAUD' => {
10
+ 0 => 0, 50 => 50, 75 => 75, 110 => 110, 134 => 134, 150 => 150, 200 => 200, 300 => 300,
11
+ 600 => 600, 1200 => 1200, 1800 => 1800, 2400 => 2400, 4800 => 4800, 7200 => 7200,
12
+ 9600 => 9600, 14400 => 14400, 19200 => 19200, 28800 => 28800, 38400 => 38400,
13
+ 57600 => 57600, 76800 => 76800, 115200 => 115200, 230400 => 230400,
14
+ 460800 => 460800, 921600 => 921600 }.freeze,
15
+
16
+ 'DATA_BITS' => { 5 => 0, 6 => 256, 7 => 512, 8 => 768 }.freeze,
17
+
18
+ 'STOP_BITS' => { 1 => 0, 2 => 1024 }.freeze,
19
+
20
+ 'PARITY' => { :none => 0, :even => 4096, :odd => 12288 }.freeze,
21
+
22
+ 'IXON' => 512, 'IXOFF' => 1024, 'IXANY' => 2048, 'IGNPAR' => 4, 'CREAD' => 2048, 'CLOCAL' => 32768,
23
+ 'HUPCL' => 16384, 'VMIN' => 16, 'VTIME' => 17, 'TCSANOW' => 0, 'F_GETFL' => 3, 'F_SETFL' => 4, }
24
+ end
25
+
26
+ class Termios < FFI::Struct #:nodoc:
27
+ layout :c_iflag, :ulong,
28
+ :c_oflag, :ulong,
29
+ :c_cflag, :ulong,
30
+ :c_lflag, :ulong,
31
+ :cc_c, [:uchar, 20],
32
+ :c_ispeed, :ulong,
33
+ :c_ospeed, :ulong
34
+
35
+ def baud #:nodoc:
36
+ CONSTANTS['BAUD_'].fetch(self[:c_ispeed])
37
+ end
38
+
39
+ def baud=(val) #:nodoc:
40
+ mask = CONSTANTS['BAUD'].fetch(val, nil)
41
+ if mask.nil?
42
+ raise ArgumentError.new "Invalid baud, supported values #{CONSTANTS['BAUD'].keys.inspect}"
43
+ end
44
+ self[:c_cflag] = self[:c_cflag] | mask; self[:c_ispeed] = mask; self[:c_ospeed] = mask; val
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ module Serial #:nodoc:
2
+ module Posix #:nodoc:
3
+ # Values generated using a Macbook Pro running Yosemite
4
+
5
+ module LIBC #:nodoc:
6
+ require 'ffi'
7
+
8
+ def self.os_specific_constants #:nodoc:
9
+ { 'BAUD' => {
10
+ 0 => 0, 50 => 50, 75 => 75, 110 => 110, 134 => 134, 150 => 150, 200 => 200, 300 => 300,
11
+ 600 => 600, 1200 => 1200, 1800 => 1800, 2400 => 2400, 4800 => 4800, 7200 => 7200,
12
+ 9600 => 9600, 14400 => 14400, 19200 => 19200, 28800 => 28800, 38400 => 38400,
13
+ 57600 => 57600, 76800 => 76800, 115200 => 115200, 230400 => 230400 }.freeze,
14
+
15
+ 'DATA_BITS' => { 5 => 0, 6 => 256, 7 => 512, 8 => 768 }.freeze,
16
+
17
+ 'STOP_BITS' => { 1 => 0, 2 => 1024 }.freeze,
18
+
19
+ 'PARITY' => { :none => 0, :even => 4096, :odd => 12288 }.freeze,
20
+
21
+ 'IXON' => 512, 'IXOFF' => 1024, 'IXANY' => 2048, 'IGNPAR' => 4, 'CREAD' => 2048, 'CLOCAL' => 32768,
22
+ 'HUPCL' => 16384, 'VMIN' => 16, 'VTIME' => 17, 'TCSANOW' => 0, 'F_GETFL' => 3, 'F_SETFL' => 4, }
23
+ end
24
+
25
+ class Termios < FFI::Struct #:nodoc:
26
+ layout :c_iflag, :ulong,
27
+ :c_oflag, :ulong,
28
+ :c_cflag, :ulong,
29
+ :c_lflag, :ulong,
30
+ :cc_c, [:uchar, 20],
31
+ :c_ispeed, :ulong,
32
+ :c_ospeed, :ulong
33
+
34
+ def baud #:nodoc:
35
+ CONSTANTS['BAUD_'].fetch(self[:c_ispeed])
36
+ end
37
+
38
+ def baud=(val) #:nodoc:
39
+ mask = CONSTANTS['BAUD'].fetch(val, nil)
40
+ if mask.nil?
41
+ raise ArgumentError.new "Invalid baud, supported values #{CONSTANTS['BAUD'].keys.inspect}"
42
+ end
43
+ self[:c_cflag] = self[:c_cflag] | mask; self[:c_ispeed] = mask; self[:c_ospeed] = mask; val
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ module Serial #:nodoc:
2
+ module Posix #:nodoc:
3
+ # Values generated using a Raspberry Pi B+ running Raspbian Jesse Lite
4
+
5
+ module LIBC #:nodoc:
6
+ require 'ffi'
7
+
8
+ def self.os_specific_constants #:nodoc:
9
+ { 'BAUD' => {
10
+ 0 => 0, 50 => 1, 75 => 2, 110 => 3, 134 => 4, 150 => 5, 200 => 6, 300 => 7, 600 => 8, 1200 => 9, 1800 => 10, 2400 => 11,
11
+ 4800 => 12, 9600 => 13, 19200 => 14, 38400 => 15, 57600 => 4097, 115200 => 4098, 230400 => 4099, 460800 => 4100,
12
+ 500000 => 4101, 576000 => 4102, 921600 => 4103, 1000000 => 4104, 1152000 => 4105, 1500000 => 4106, 2000000 => 4107,
13
+ 2500000 => 4108, 3000000 => 4109, 3500000 => 4110, 4000000 => 4111 }.freeze,
14
+
15
+ 'DATA_BITS' => { 5 => 0, 6 => 16, 7 => 32, 8 => 48 }.freeze,
16
+
17
+ 'STOP_BITS' => { 1 => 0, 2 => 64 }.freeze,
18
+
19
+ 'PARITY' => { :none => 0, :even => 256, :odd => 768, :space => 1073742080, :mark => 1073742592 }.freeze,
20
+
21
+ 'IXON' => 1024, 'IXOFF' => 4096, 'IXANY' => 2048, 'IGNPAR' => 4, 'CREAD' => 128, 'CLOCAL' => 2048,
22
+ 'HUPCL' => 1024, 'VMIN' => 6, 'VTIME' => 5, 'TCSANOW' => 0, 'F_GETFL' => 3, 'F_SETFL' => 4, }
23
+ end
24
+
25
+ class Termios < FFI::Struct #:nodoc:
26
+ # This struct has 2 version I've encountered, both with different sizes for cc_c
27
+ # The simple solution is to simply ignore anything after cc_c and add some padding bytes to avoid memory corruption
28
+ # Because of this hackyness, custom Baud rates are not supported
29
+ layout :c_iflag, :uint,
30
+ :c_oflag, :uint,
31
+ :c_cflag, :uint,
32
+ :c_lflag, :uint,
33
+ :c_line, :uchar,
34
+ :cc_c, [:uchar, 19],
35
+ :ignored, [:uint8, 48] # ignored padding bytes
36
+
37
+ def baud #:nodoc:
38
+ CONSTANTS['BAUD_'].fetch(self[:c_cflag] & CONSTANTS['BAUD_BITMASK'])
39
+ end
40
+
41
+ def baud=(val) #:nodoc:
42
+ mask = CONSTANTS['BAUD'].fetch(val, nil)
43
+ if mask.nil?
44
+ raise ArgumentError.new "Invalid baud, supported values #{CONSTANTS['BAUD'].keys.inspect}"
45
+ end
46
+ self[:c_cflag] = self[:c_cflag] | mask; val
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,174 @@
1
+ module Serial #:nodoc:
2
+
3
+ # Load the OS specific implementation
4
+ begin #:nodoc:
5
+ host_info = begin
6
+ RbConfig::CONFIG['host_os']
7
+ rescue Exception
8
+ require 'rbconfig'
9
+ RbConfig::CONFIG['host_os']
10
+ end
11
+
12
+ if (host_info =~ /darwin/)
13
+ require 'ffi-serial/darwin'
14
+ elsif (host_info =~ /linux/)
15
+ require 'ffi-serial/linux'
16
+ elsif (host_info =~ /bsd/)
17
+ require 'ffi-serial/bsd'
18
+ else
19
+ raise LoadError.new 'The current operating system is not (yet) supported'
20
+ end
21
+ end
22
+
23
+ ##
24
+ # Serial port implementation for Posix
25
+ module Posix #:nodoc:
26
+ ##
27
+ # Create a new serial port on a Posix based operating system
28
+ def self.new(dev, baud, data_bits, stop_bits, parity) #:nodoc:
29
+ # Parse configuration first
30
+ termios = LIBC::Termios.new
31
+
32
+ termios.baud = baud
33
+ termios.data_bits = data_bits
34
+ termios.stop_bits = stop_bits
35
+ termios.parity = parity
36
+
37
+ io = File.open(dev, IO::RDWR | IO::NOCTTY | IO::NONBLOCK)
38
+ begin
39
+ io.sync = true
40
+ io.fcntl(LIBC::CONSTANTS['F_SETFL'], (io.fcntl(LIBC::CONSTANTS['F_GETFL']) & (~IO::NONBLOCK)))
41
+ io.instance_variable_set(:@__serial__dev__, dev.freeze)
42
+
43
+ termios[:c_iflag] = (termios[:c_iflag] | LIBC::CONSTANTS['IXON'] | LIBC::CONSTANTS['IXOFF'] | LIBC::CONSTANTS['IXANY'])
44
+ termios[:c_cflag] = (termios[:c_cflag] | LIBC::CONSTANTS['CLOCAL'] | LIBC::CONSTANTS['CREAD'] | LIBC::CONSTANTS['HUPCL'])
45
+
46
+ # Blocking read
47
+ termios[:cc_c][LIBC::CONSTANTS['VMIN']] = 1
48
+ termios[:cc_c][LIBC::CONSTANTS['VTIME']] = 0
49
+
50
+ LIBC.tcsetattr(io, termios)
51
+
52
+ io.extend(self)
53
+ rescue Exception
54
+ begin; io.close; rescue Exception; end
55
+ raise
56
+ end
57
+ io
58
+ end
59
+
60
+ def baud #:nodoc:
61
+ LIBC.tcgetattr(self).baud
62
+ end
63
+
64
+ def data_bits #:nodoc:
65
+ LIBC.tcgetattr(self).data_bits
66
+ end
67
+
68
+ def stop_bits #:nodoc:
69
+ LIBC.tcgetattr(self).stop_bits
70
+ end
71
+
72
+ def parity #:nodoc:
73
+ LIBC.tcgetattr(self).parity
74
+ end
75
+
76
+ def to_s #:nodoc:
77
+ ['#<Serial:', @__serial__dev__, '>'].join.to_s
78
+ end
79
+
80
+ def inspect #:nodoc:
81
+ self.to_s
82
+ end
83
+
84
+ private
85
+
86
+ ##
87
+ # FFI integration with C to provide access to OS specific serial port APIs
88
+ module LIBC #:nodoc:
89
+ require 'ffi'
90
+
91
+ extend FFI::Library #:nodoc:
92
+ ffi_lib FFI::Library::LIBC
93
+
94
+ def self.tcgetattr(ruby_io) #:nodoc:
95
+ termios = Termios.new
96
+ if (0 != c_tcgetattr(ruby_io.fileno, termios))
97
+ raise ERRNO[FFI.errno].new
98
+ end
99
+ termios
100
+ end
101
+
102
+ def self.tcsetattr(ruby_io, termios) #:nodoc:
103
+ if (0 != c_tcsetattr(ruby_io.fileno, CONSTANTS['TCSANOW'], termios))
104
+ raise ERRNO[FFI.errno].new
105
+ end
106
+ end
107
+
108
+ class Termios #:nodoc:
109
+ def data_bits=(val) #:nodoc:
110
+ mask = CONSTANTS['DATA_BITS'].fetch(val, nil)
111
+ if mask.nil?
112
+ raise ArgumentError.new "Invalid data bits, supported values #{CONSTANTS['DATA_BITS'].keys.inspect}"
113
+ end
114
+ self[:c_cflag] = self[:c_cflag] | mask; val
115
+ end
116
+
117
+ def data_bits #:nodoc:
118
+ CONSTANTS['DATA_BITS_'].fetch(self[:c_cflag] & CONSTANTS['DATA_BITS_BITMASK'])
119
+ end
120
+
121
+ def stop_bits=(val) #:nodoc:
122
+ mask = CONSTANTS['STOP_BITS'].fetch(val, nil)
123
+ if mask.nil?
124
+ raise ArgumentError.new "Invalid stop bits, supported values #{CONSTANTS['STOP_BITS'].keys.inspect}"
125
+ end
126
+ self[:c_cflag] = self[:c_cflag] | mask; val
127
+ end
128
+
129
+ def stop_bits #:nodoc:
130
+ CONSTANTS['STOP_BITS_'].fetch(self[:c_cflag] & CONSTANTS['STOP_BITS_BITMASK'])
131
+ end
132
+
133
+ def parity=(val) #:nodoc:
134
+ mask = CONSTANTS['PARITY'].fetch(val, nil)
135
+ if mask.nil?
136
+ raise ArgumentError.new "Invalid parity, supported values #{CONSTANTS['PARITY'].keys.inspect}"
137
+ end
138
+ if (:none == val)
139
+ self[:c_iflag] = self[:c_iflag] | LIBC::CONSTANTS['IGNPAR']
140
+ end
141
+ self[:c_cflag] = self[:c_cflag] | mask; val
142
+ end
143
+
144
+ def parity #:nodoc:
145
+ CONSTANTS['PARITY_'].fetch(self[:c_cflag] & CONSTANTS['PARITY_BITMASK'])
146
+ end
147
+ end
148
+
149
+ CONSTANTS ||= begin #:nodoc:
150
+ constants = self.os_specific_constants
151
+ constants['BAUD_'] = constants['BAUD'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
152
+ constants['DATA_BITS_'] = constants['DATA_BITS'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
153
+ constants['STOP_BITS_'] = constants['STOP_BITS'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
154
+ constants['PARITY_'] = constants['PARITY'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
155
+ constants['BAUD_BITMASK'] = constants['BAUD'].values.max
156
+ constants['DATA_BITS_BITMASK'] = constants['DATA_BITS'].values.max
157
+ constants['STOP_BITS_BITMASK'] = constants['STOP_BITS'].values.max
158
+ constants['PARITY_BITMASK'] = constants['PARITY'].values.max
159
+ constants.freeze
160
+ end
161
+
162
+ ERRNO ||= Errno.constants.each_with_object({}) { |e, r| e = Errno.const_get(e); r[e::Errno] = e }.freeze #:nodoc:
163
+
164
+ attach_function :c_tcgetattr, :tcgetattr, [:int, :buffer_in], :int #:nodoc:
165
+ attach_function :c_tcsetattr, :tcsetattr, [:int, :int, :buffer_out], :int #:nodoc:
166
+
167
+ private_class_method :os_specific_constants, :c_tcgetattr, :c_tcsetattr #:nodoc:
168
+ private_constant :ERRNO #:nodoc:
169
+ end
170
+
171
+ private_constant :LIBC #:nodoc:
172
+ private_class_method :new #:nodoc:
173
+ end
174
+ end
@@ -0,0 +1,312 @@
1
+ module Serial #:nodoc:
2
+ module Windows #:nodoc:
3
+ def self.new(com_port, baud, data_bits, stop_bits, parity) #:nodoc:
4
+ # Either specify as 'COM1' or a number. eg 1 for 'COM1'
5
+ begin
6
+ as_int = Integer(com_port)
7
+ com_port = '\\\\.\\COM' + as_int.to_s
8
+ rescue StandardError
9
+ com_port = '\\\\.\\' + com_port.to_s.strip.chomp.upcase
10
+ end
11
+
12
+ dcb = Kernel32::DCB.new
13
+
14
+ dcb.baud = baud
15
+ dcb.data_bits = data_bits
16
+ dcb.stop_bits = stop_bits
17
+ dcb.parity = parity
18
+
19
+ io = File.open(com_port, IO::RDWR|IO::BINARY)
20
+ begin
21
+ io.instance_variable_set(:@__serial__port__, com_port[4..-1].to_s.freeze)
22
+
23
+ io.extend(self)
24
+ io.sync = true
25
+
26
+ # Sane defaults
27
+ dcb[:Flags] = dcb[:Flags] | (Kernel32::CONSTANTS['FLAGS'].fetch('fDtrControl').fetch(:enable))
28
+ dcb[:XonChar] = 17
29
+ dcb[:XoffChar] = 19
30
+
31
+ Kernel32.SetCommState(io, dcb)
32
+ Kernel32.ClearCommError(io)
33
+ Kernel32.set_io_block(io)
34
+ rescue Exception
35
+ begin; io.close; rescue Exception; end
36
+ raise
37
+ end
38
+ io
39
+ end
40
+
41
+ def baud #:nodoc:
42
+ Kernel32.GetCommState(self).baud
43
+ end
44
+
45
+ def data_bits #:nodoc:
46
+ Kernel32.GetCommState(self).data_bits
47
+ end
48
+
49
+ def stop_bits #:nodoc:
50
+ Kernel32.GetCommState(self).stop_bits
51
+ end
52
+
53
+ def parity #:nodoc:
54
+ Kernel32.GetCommState(self).parity
55
+ end
56
+
57
+ def read_nonblock(maxlen, outbuf = nil, options = nil) #:nodoc:
58
+ Kernel32.set_io_nonblock(self)
59
+ result = begin
60
+ outbuf.nil? ? read(maxlen) : read(maxlen, outbuf)
61
+ ensure
62
+ Kernel32.set_io_block(self)
63
+ end
64
+
65
+ if result.nil? || (0 == result.length)
66
+ if ((!options.nil?) && (false == options[:exception]))
67
+ return :wait_readable
68
+ end
69
+ raise Errno::EWOULDBLOCK.new
70
+ end
71
+ result
72
+ end
73
+
74
+ def readpartial(maxlen, outbuf = nil) #:nodoc:
75
+ ch = self.read(1)
76
+ if (ch.nil? || (0 == ch.length))
77
+ return ch
78
+ end
79
+ self.ungetc(ch)
80
+ Kernel32.set_io_nonblock(self)
81
+ outbuf.nil? ? read(maxlen) : read(maxlen, outbuf)
82
+ ensure
83
+ Kernel32.set_io_block(self)
84
+ end
85
+
86
+ def readbyte #:nodoc:
87
+ Kernel32.set_io_nonblock(self); super
88
+ ensure
89
+ Kernel32.set_io_block(self)
90
+ end
91
+
92
+ def getc #:nodoc:
93
+ Kernel32.set_io_nonblock(self); super
94
+ ensure
95
+ Kernel32.set_io_block(self)
96
+ end
97
+
98
+ def readchar #:nodoc:
99
+ Kernel32.set_io_nonblock(self); super
100
+ ensure
101
+ Kernel32.set_io_block(self)
102
+ end
103
+
104
+ def to_s #:nodoc:
105
+ ['#<Serial:', @__serial__port__, '>'].join.to_s
106
+ end
107
+
108
+ def inspect #:nodoc:
109
+ self.to_s
110
+ end
111
+
112
+ ##
113
+ # FFI integration with Kernel32.dll to provide access to OS specific serial port APIs
114
+ module Kernel32 #:nodoc:
115
+ require 'ffi'
116
+
117
+ extend FFI::Library #:nodoc:
118
+ ffi_lib 'kernel32'
119
+ ffi_convention :stdcall
120
+
121
+ def self.GetCommState(ruby_io) #:nodoc:
122
+ dcb = DCB.new
123
+ dcb[:DCBlength] = dcb.size
124
+ return dcb unless (0 == c_GetCommState(LIBC._get_osfhandle(ruby_io), dcb))
125
+ raise ERRNO[FFI.errno].new
126
+ end
127
+
128
+ def self.SetCommState(ruby_io, dcb) #:nodoc:
129
+ dcb[:DCBlength] = dcb.size
130
+ dcb[:Flags] = dcb[:Flags] | 1 # fBinary must be true
131
+ return true unless (0 == c_SetCommState(LIBC._get_osfhandle(ruby_io), dcb))
132
+ raise ERRNO[FFI.errno].new
133
+ end
134
+
135
+ def self.GetCommTimeouts(ruby_io) #:nodoc:
136
+ commtimeouts = COMMTIMEOUTS.new
137
+ return commtimeouts unless (0 == c_GetCommTimeouts(LIBC._get_osfhandle(fd), commtimeouts))
138
+ raise ERRNO[FFI.errno].new
139
+ end
140
+
141
+ def self.SetCommTimeouts(ruby_io, commtimeouts) #:nodoc:
142
+ return true unless (0 == c_SetCommTimeouts(LIBC._get_osfhandle(ruby_io), commtimeouts))
143
+ raise ERRNO[FFI.errno].new
144
+ end
145
+
146
+ def self.ClearCommError(ruby_io) #:nodoc:
147
+ return true unless (0 == c_ClearCommError(LIBC._get_osfhandle(ruby_io), 0, 0))
148
+ raise ERRNO[FFI.errno].new
149
+ end
150
+
151
+ def self.set_io_block(ruby_io) #:nodoc:
152
+ self.SetCommTimeouts(ruby_io, (@@read_block_io ||= begin
153
+ timeouts = COMMTIMEOUTS.new
154
+ timeouts[:ReadIntervalTimeout] = CONSTANTS['MAXDWORD']
155
+ timeouts[:ReadTotalTimeoutMultiplier] = CONSTANTS['MAXDWORD']
156
+ timeouts[:ReadTotalTimeoutConstant] = CONSTANTS['MAXDWORD'] - 1
157
+ timeouts[:WriteTotalTimeoutMultiplier] = 0
158
+ timeouts[:WriteTotalTimeoutConstant] = CONSTANTS['MAXDWORD'] - 1
159
+ timeouts
160
+ end))
161
+ end
162
+
163
+ def self.set_io_nonblock(ruby_io) #:nodoc:
164
+ self.SetCommTimeouts(ruby_io, (@@read_nonblock_io ||= begin
165
+ timeouts = COMMTIMEOUTS.new
166
+ timeouts[:ReadIntervalTimeout] = CONSTANTS['MAXDWORD']
167
+ timeouts[:ReadTotalTimeoutMultiplier] = 0
168
+ timeouts[:ReadTotalTimeoutConstant] = 0
169
+ timeouts[:WriteTotalTimeoutMultiplier] = 1
170
+ timeouts[:WriteTotalTimeoutConstant] = 1
171
+ timeouts
172
+ end))
173
+ end
174
+
175
+ class DCB < FFI::Struct #:nodoc:
176
+ layout :DCBlength, :uint32,
177
+ :BaudRate, :uint32,
178
+ :Flags, :uint32,
179
+ :wReserved, :uint16,
180
+ :XonLim, :uint16,
181
+ :XoffLim, :uint16,
182
+ :ByteSize, :uint8,
183
+ :Parity, :uint8,
184
+ :StopBits, :uint8,
185
+ :XonChar, :int8,
186
+ :XoffChar, :int8,
187
+ :ErrorChar, :int8,
188
+ :EofChar, :int8,
189
+ :EvtChar, :int8,
190
+ :wReserved1, :uint16
191
+
192
+ def baud=(val) #:nodoc:
193
+ new_val = begin
194
+ Integer(val)
195
+ rescue StandardError
196
+ -1
197
+ end
198
+ if (0 >= new_val)
199
+ raise ArgumentError.new "Invalid baud, specify a positive Integer"
200
+ end
201
+ self[:BaudRate] = new_val; val
202
+ end
203
+
204
+ def baud #:nodoc:
205
+ self[:BaudRate]
206
+ end
207
+
208
+ def data_bits=(val) #:nodoc:
209
+ parsed = CONSTANTS['DATA_BITS'].fetch(val, nil)
210
+ if parsed.nil?
211
+ raise ArgumentError.new "Invalid data bits, supported values #{CONSTANTS['DATA_BITS'].keys.inspect}"
212
+ end
213
+ self[:ByteSize] = parsed; val
214
+ end
215
+
216
+ def data_bits #:nodoc:
217
+ CONSTANTS['DATA_BITS_'].fetch(self[:ByteSize])
218
+ end
219
+
220
+ def stop_bits=(val) #:nodoc:
221
+ parsed = CONSTANTS['STOP_BITS'].fetch(val, nil)
222
+ if parsed.nil?
223
+ raise ArgumentError.new "Invalid data bits, supported values #{CONSTANTS['STOP_BITS'].keys.inspect}"
224
+ end
225
+ self[:StopBits] = parsed; val
226
+ end
227
+
228
+ def stop_bits #:nodoc:
229
+ CONSTANTS['STOP_BITS_'].fetch(self[:StopBits])
230
+ end
231
+
232
+ def parity=(val) #:nodoc:
233
+ parsed = CONSTANTS['PARITY'].fetch(val, nil)
234
+ if parsed.nil?
235
+ raise ArgumentError.new "Invalid parity, supported values #{CONSTANTS['PARITY'].keys.inspect}"
236
+ end
237
+ if (:none == val)
238
+ self[:Flags] = self[:Flags] & (~CONSTANTS['FLAGS'].fetch('fParity'))
239
+ else
240
+ self[:Flags] = self[:Flags] | CONSTANTS['FLAGS'].fetch('fParity')
241
+ end
242
+ self[:Parity] = parsed; val
243
+ end
244
+
245
+ def parity #:nodoc:
246
+ CONSTANTS['PARITY_'].fetch(self[:Parity])
247
+ end
248
+ end
249
+
250
+ class COMMTIMEOUTS < FFI::Struct #:nodoc:
251
+ layout :ReadIntervalTimeout, :uint32,
252
+ :ReadTotalTimeoutMultiplier, :uint32,
253
+ :ReadTotalTimeoutConstant, :uint32,
254
+ :WriteTotalTimeoutMultiplier, :uint32,
255
+ :WriteTotalTimeoutConstant, :uint32
256
+ end
257
+
258
+ CONSTANTS ||= begin #:nodoc:
259
+ constants = {
260
+ 'MAXDWORD' => 4294967295,
261
+ 'DATA_BITS' => { 5 => 5, 6 => 6, 7 => 7, 8 => 8 }.freeze,
262
+ 'STOP_BITS' => { 1 => 0, 1.5 => 1, 2 => 2 }.freeze,
263
+ 'PARITY' => { none: 0, odd: 1, even: 2, mark: 3, space: 4 }.freeze,
264
+ 'FLAGS' => {
265
+ 'fParity' => 2, 'fOutxCtsFlow' => 4, 'fOutxDsrFlow' => 8,
266
+ 'fDtrControl' => { disable: 0, enable: 16, handshake: 32 }.freeze,
267
+ 'fDsrSensitivity' => 64, 'fTXContinueOnXoff' => 128, 'fOutX' => 256,
268
+ 'fInX' => 512, 'fErrorChar' => 1024, 'fNull' => 2048,
269
+ 'fRtsControl' => { disable: 0, enable: 4096, handshake: 8192, toggle: 12288 }.freeze,
270
+ 'fAbortOnError' => 16384
271
+ }.freeze,
272
+ }
273
+
274
+ constants['DATA_BITS_'] = constants['DATA_BITS'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
275
+ constants['STOP_BITS_'] = constants['STOP_BITS'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
276
+ constants['PARITY_'] = constants['PARITY'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
277
+ constants['FLAGS_'] = {}
278
+ constants['FLAGS_']['fDtrControl'] = constants['FLAGS']['fDtrControl'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
279
+ constants['FLAGS_']['fRtsControl'] = constants['FLAGS']['fRtsControl'].each_with_object({}) { |(k,v),r| r[v] = k }.freeze
280
+ constants['FLAGS_'].freeze
281
+
282
+ constants.freeze
283
+ end
284
+
285
+ module LIBC #:nodoc:
286
+ extend FFI::Library #:nodoc:
287
+ ffi_lib FFI::Library::LIBC
288
+
289
+ def self._get_osfhandle(ruby_io) #:nodoc:
290
+ handle = c__get_osfhandle(ruby_io.fileno)
291
+ return handle unless (-1 == handle)
292
+ raise ERRNO[FFI.errno].new
293
+ end
294
+
295
+ attach_function :c__get_osfhandle, :_get_osfhandle, [:int], :long #:nodoc:
296
+ private_class_method :c__get_osfhandle #:nodoc:
297
+ end
298
+
299
+ ERRNO ||= Errno.constants.each_with_object({}) { |e, r| e = Errno.const_get(e); r[e::Errno] = e }.freeze #:nodoc:
300
+
301
+ attach_function :c_GetCommState, :GetCommState, [:long, :buffer_out], :int32 #:nodoc:
302
+ attach_function :c_SetCommState, :SetCommState, [:long, :buffer_in], :int32 #:nodoc:
303
+ attach_function :c_GetCommTimeouts, :GetCommTimeouts, [:long, :buffer_out], :int32 #:nodoc:
304
+ attach_function :c_SetCommTimeouts, :SetCommTimeouts, [:long, :buffer_in], :int32 #:nodoc:
305
+ attach_function :c_ClearCommError, :ClearCommError, [:long, :int, :int], :int32 #:nodoc:
306
+ private_class_method :c_GetCommState, :c_SetCommState, :c_GetCommTimeouts, :c_SetCommTimeouts, :c_ClearCommError #:nodoc:
307
+ private_constant :LIBC, :ERRNO #:nodoc:
308
+ end
309
+
310
+ private_constant :Kernel32 #:nodoc:
311
+ end
312
+ end
data/lib/ffi-serial.rb ADDED
@@ -0,0 +1,69 @@
1
+ # :main: README.md
2
+ module Serial
3
+ begin #:nodoc:
4
+ require 'ffi'
5
+ rescue LoadError
6
+ raise LoadError.new 'Could not load ruby gem ffi'
7
+ end
8
+
9
+ ##
10
+ # :attr_reader: baud
11
+ # Determine the current serial port baud rate by querying the underlying operating system
12
+
13
+ ##
14
+ # :attr_reader: data_bits
15
+ # Determine the current serial port data bits by querying the underlying operating system
16
+
17
+ ##
18
+ # :attr_reader: stop_bits
19
+ # Determine the current serial port stop bits by querying the underlying operating system
20
+
21
+ ##
22
+ # :attr_reader: parity
23
+ # Determine the current serial port parity by querying the underlying operating system
24
+
25
+ ##
26
+ # Create a new Ruby IO configured with the serial port parameters
27
+ #
28
+ # :call-seq:
29
+ # new(port: '/dev/tty or COM1')
30
+ # new(port: '/dev/tty or COM1', baud: 9600, data_bits: 8, stop_bits: 1, parity: :none)
31
+ def self.new(config)
32
+ driver = if ('Windows_NT' == ENV['OS'])
33
+ @@loaded_ffi_serial_windows ||= begin
34
+ require 'ffi-serial/windows'
35
+ true
36
+ end
37
+ Windows
38
+ else
39
+ @@loaded_ffi_serial_posix ||= begin
40
+ require 'ffi-serial/posix'
41
+ true
42
+ end
43
+ Posix
44
+ end
45
+
46
+ config = config.each_with_object({}) { |(k,v),r| r[k.to_s.strip.chomp.downcase.gsub(/\-|\_|\s/, '')] = v }
47
+
48
+ port = config.delete('port') { raise ArgumentError.new ':port not specified' }
49
+ baud = config.delete('baud') { 9600 }
50
+ data_bits = config.delete('databits') { 8 }
51
+ stop_bits = config.delete('stopbits') { 1 }
52
+ parity = config.delete('parity') { :none }
53
+
54
+ if !config.empty?
55
+ raise ArgumentError.new "Unknown options specified: #{config.keys}"
56
+ end
57
+
58
+ # Create a new Ruby IO pointing to the serial port and configure it
59
+ # using the OS specific function
60
+ new_instance = driver.method(:new).call(
61
+ port,
62
+ Integer(baud),
63
+ Integer(data_bits),
64
+ Integer(stop_bits),
65
+ parity.to_s.strip.chomp.downcase.to_sym)
66
+
67
+ new_instance
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffi-serial
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Johan van der Vyver
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.9.3
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.9'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.9.3
33
+ description: Yet another Ruby Serial Port implementation using FFI. Returns a Ruby
34
+ IO object configured as a serial port to leverage the extensive Ruby IO standard
35
+ library functionality
36
+ email: code@johan.vdvyver.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files:
40
+ - LICENSE
41
+ files:
42
+ - LICENSE
43
+ - README.md
44
+ - lib/ffi-serial.rb
45
+ - lib/ffi-serial/bsd.rb
46
+ - lib/ffi-serial/darwin.rb
47
+ - lib/ffi-serial/linux.rb
48
+ - lib/ffi-serial/posix.rb
49
+ - lib/ffi-serial/windows.rb
50
+ homepage:
51
+ licenses:
52
+ - MIT
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options:
56
+ - "--quiet"
57
+ - "--line-numbers"
58
+ - "--inline-source"
59
+ - "--title"
60
+ - FFI Serial
61
+ - "--main"
62
+ - README.rdoc
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 1.9.3
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 2.4.8
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: FFI Serial
81
+ test_files: []