ffi-serial 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/lib/ffi-serial/bsd.rb +49 -0
- data/lib/ffi-serial/darwin.rb +48 -0
- data/lib/ffi-serial/linux.rb +51 -0
- data/lib/ffi-serial/posix.rb +174 -0
- data/lib/ffi-serial/windows.rb +312 -0
- data/lib/ffi-serial.rb +69 -0
- metadata +81 -0
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: []
|