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 +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: []
|