ffi-serial 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []