serial_interface 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/History.txt +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +54 -0
- data/Rakefile +55 -0
- data/lib/protocol/rca2006.rb +197 -0
- data/lib/serial_interface.rb +224 -0
- data/lib/serial_packet.rb +115 -0
- data/serial_interface.gemspec +61 -0
- data/test/helper.rb +10 -0
- data/test/test_serial_interface.rb +63 -0
- data/test/test_serial_io.rb +7 -0
- data/test/test_serial_packets.rb +74 -0
- metadata +92 -0
data/.gitignore
ADDED
data/History.txt
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Levin Alexander
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
serial_interface
|
2
|
+
by Levin Alexander
|
3
|
+
http://levinalex.net/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
serial_interface intends to be a small library that makes it easy
|
8
|
+
to define packet based protocols over a serial link (RS232) in a
|
9
|
+
declarative fashion.
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
12
|
+
|
13
|
+
* remove dependency on traits
|
14
|
+
* fix all the warnings
|
15
|
+
* add some tests that test the actual functionality
|
16
|
+
* improve the interface
|
17
|
+
* improve the architecture
|
18
|
+
|
19
|
+
== SYNOPSIS:
|
20
|
+
|
21
|
+
nothing written yet
|
22
|
+
|
23
|
+
== REQUIREMENTS:
|
24
|
+
|
25
|
+
* traits 0.8.1
|
26
|
+
|
27
|
+
== INSTALL:
|
28
|
+
|
29
|
+
* not yet written
|
30
|
+
|
31
|
+
== LICENSE:
|
32
|
+
|
33
|
+
(The MIT License)
|
34
|
+
|
35
|
+
Copyright (c) 2006-2008 Levin Alexander
|
36
|
+
|
37
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
38
|
+
a copy of this software and associated documentation files (the
|
39
|
+
'Software'), to deal in the Software without restriction, including
|
40
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
41
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
42
|
+
permit persons to whom the Software is furnished to do so, subject to
|
43
|
+
the following conditions:
|
44
|
+
|
45
|
+
The above copyright notice and this permission notice shall be
|
46
|
+
included in all copies or substantial portions of the Software.
|
47
|
+
|
48
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
49
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
50
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
51
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
52
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
53
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
54
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "serial_interface"
|
8
|
+
gem.summary = %Q{abstracts protocols on a serial link}
|
9
|
+
gem.description = %Q{serial_interface intends to be a small library that makes it easy
|
10
|
+
to define packet based protocols over a serial link (RS232) in a
|
11
|
+
declarative fashion.}
|
12
|
+
gem.email = "mail@levinalex.net"
|
13
|
+
gem.homepage = "http://github.com/levinalex/serial_interface"
|
14
|
+
gem.authors = ["Levin Alexander"]
|
15
|
+
gem.add_dependency "traits"
|
16
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'rake/testtask'
|
24
|
+
Rake::TestTask.new(:test) do |test|
|
25
|
+
test.libs << 'lib' << 'test'
|
26
|
+
test.pattern = 'test/**/test_*.rb'
|
27
|
+
test.verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
require 'rcov/rcovtask'
|
32
|
+
Rcov::RcovTask.new do |test|
|
33
|
+
test.libs << 'test'
|
34
|
+
test.pattern = 'test/**/test_*.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
rescue LoadError
|
38
|
+
task :rcov do
|
39
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
task :test => :check_dependencies
|
44
|
+
|
45
|
+
task :default => :test
|
46
|
+
|
47
|
+
require 'rake/rdoctask'
|
48
|
+
Rake::RDocTask.new do |rdoc|
|
49
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "serial_interface #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module SerialProtocol
|
2
|
+
|
3
|
+
# Serial protocol Roboterclub Aachen 2006
|
4
|
+
# http://www.roboterclub.rwth-aachen.de/
|
5
|
+
#
|
6
|
+
# Packet format:
|
7
|
+
# [0x65, 0xEB, <type:8>, <counter:8>, <length:8>, data:length>, <crc:16>]
|
8
|
+
#
|
9
|
+
# <type> is one of the following:
|
10
|
+
# 0b00000000 --> Data packet, discard on checksum mismatch
|
11
|
+
# 0b00011111 --> Data packet, resend on checksum mismatch
|
12
|
+
# 0b11100011 --> ACK packet
|
13
|
+
# 0b11111100 --> NACK packet
|
14
|
+
#
|
15
|
+
class RCA2006
|
16
|
+
|
17
|
+
STARTBYTES = "\x65\xeb"
|
18
|
+
TYPE = {
|
19
|
+
:data_no_crc => 0,
|
20
|
+
:data => 0b00011111,
|
21
|
+
:ack => 0b11100011,
|
22
|
+
:nack => 0b11111100 }
|
23
|
+
|
24
|
+
def initialize(send_callback, receive_callback, options = {})
|
25
|
+
@rec_queue = Queue.new
|
26
|
+
@state = :first_startbyte
|
27
|
+
@send_callback = send_callback
|
28
|
+
@receive_callback = receive_callback
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set callbacks whenever a packet is sent or received.
|
32
|
+
#
|
33
|
+
def on_raw_receive(&block)
|
34
|
+
@raw_receive_callback = block
|
35
|
+
end
|
36
|
+
def on_raw_send(&block)
|
37
|
+
@raw_send_callback = block
|
38
|
+
end
|
39
|
+
|
40
|
+
# Wrap a string into a packet
|
41
|
+
#
|
42
|
+
# the options-hash can be used to override the default packet format
|
43
|
+
#
|
44
|
+
def send_packet(data, options = {})
|
45
|
+
str = data.to_s
|
46
|
+
type = TYPE[ options[:type] || :data_no_crc].chr
|
47
|
+
counter = options[:counter] || 0
|
48
|
+
checksum = options[:checksum] || ("" << counter << str.length << str).crc_xmodem
|
49
|
+
|
50
|
+
@raw_send_callback.call(type, counter, data, checksum) if @raw_send_callback
|
51
|
+
|
52
|
+
p = "" << STARTBYTES << type << counter << str.length << str << [checksum].pack("S").reverse
|
53
|
+
|
54
|
+
# send the packet, using the callback
|
55
|
+
#
|
56
|
+
@send_callback.call(p)
|
57
|
+
end
|
58
|
+
|
59
|
+
def receive_handler(type, counter, data, checksum)
|
60
|
+
@raw_receive_callback.call(type,counter,data,checksum) if @raw_receive_callback
|
61
|
+
|
62
|
+
case type
|
63
|
+
when :ack
|
64
|
+
when :nack
|
65
|
+
when :data
|
66
|
+
@receive_callback.call(data)
|
67
|
+
when :data_no_crc
|
68
|
+
@receive_callback.call(data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Big and ugly state machine that does most of the work
|
73
|
+
#
|
74
|
+
def add_char_to_packet(char)
|
75
|
+
@state = :first_checksum if (@state == 0)
|
76
|
+
case @state
|
77
|
+
when :first_startbyte
|
78
|
+
@data = ""
|
79
|
+
@state = ((char == STARTBYTES[0]) ? :second_startbyte : :first_startbyte)
|
80
|
+
when :second_startbyte
|
81
|
+
@state = (char == STARTBYTES[1]) ? :type :
|
82
|
+
# special case: first startbyte is repeated
|
83
|
+
(char == STARTBYTES[0] ? :second_startbyte : :first_startbyte)
|
84
|
+
when :type
|
85
|
+
@type = TYPE.invert[char]
|
86
|
+
@state = :counter
|
87
|
+
when :counter
|
88
|
+
@counter = char
|
89
|
+
@state = :length
|
90
|
+
when :length
|
91
|
+
@length = char
|
92
|
+
@state = @length
|
93
|
+
when Integer
|
94
|
+
@data << char
|
95
|
+
@state -= 1
|
96
|
+
when :first_checksum
|
97
|
+
@checksum = (char << 8)
|
98
|
+
@state = :second_checksum
|
99
|
+
when :second_checksum
|
100
|
+
@checksum = @checksum + char
|
101
|
+
@state = :first_startbyte
|
102
|
+
|
103
|
+
crc = ("" << @counter << @length << @data).crc_xmodem
|
104
|
+
# received a valid packet
|
105
|
+
|
106
|
+
if @type == :data || @type == :data_no_crc
|
107
|
+
if @checksum == crc
|
108
|
+
|
109
|
+
# send ACK
|
110
|
+
send_packet(nil, :type => :ack, :counter => @counter)
|
111
|
+
receive_handler(@type, @counter, @data,@checksum)
|
112
|
+
else
|
113
|
+
# send NACK and discard packet
|
114
|
+
send_packet(nil, :type => :nack, :counter => @counter)
|
115
|
+
raise ChecksumMismatch, "ChecksumMismatch, expected #{crc}, was #{@checksum}"
|
116
|
+
end
|
117
|
+
else
|
118
|
+
# the packet is ACK, NACK or unknown, call receive-handler
|
119
|
+
# data may be mangled since the checksum is not checked
|
120
|
+
#
|
121
|
+
receive_handler(@type, @counter, @data, @checksum)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class RCA2006Simple < RCA2006
|
128
|
+
def initialize(send_callback, receive_callback, options = {})
|
129
|
+
@type = :data_no_crc
|
130
|
+
@counter = 0
|
131
|
+
super
|
132
|
+
end
|
133
|
+
|
134
|
+
# Wrap a string into a packet
|
135
|
+
#
|
136
|
+
# the options-hash can be used to override the default packet format
|
137
|
+
#
|
138
|
+
def send_packet(data, options = {})
|
139
|
+
str = data.to_s
|
140
|
+
checksum = options[:checksum] || ("" << str.length << str).crc_xmodem
|
141
|
+
|
142
|
+
@raw_send_callback.call(:data_no_crc, 0, data, checksum) if @raw_send_callback
|
143
|
+
|
144
|
+
p = "" << STARTBYTES << str.length << str << [checksum].pack("S").reverse
|
145
|
+
|
146
|
+
# send the packet, using the callback
|
147
|
+
#
|
148
|
+
@send_callback.call(p)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Big and ugly state machine that does most of the work
|
152
|
+
#
|
153
|
+
def add_char_to_packet(char)
|
154
|
+
@state = :first_checksum if (@state == 0)
|
155
|
+
case @state
|
156
|
+
when :first_startbyte
|
157
|
+
@data = ""
|
158
|
+
@state = ((char == STARTBYTES[0]) ? :second_startbyte : :first_startbyte)
|
159
|
+
when :second_startbyte
|
160
|
+
@state = (char == STARTBYTES[1]) ? :length :
|
161
|
+
# special case: first startbyte is repeated
|
162
|
+
(char == STARTBYTES[0] ? :second_startbyte : :first_startbyte)
|
163
|
+
when :length
|
164
|
+
@length = char
|
165
|
+
@state = @length
|
166
|
+
when Integer
|
167
|
+
@data << char
|
168
|
+
@state -= 1
|
169
|
+
when :first_checksum
|
170
|
+
@checksum = (char << 8)
|
171
|
+
@state = :second_checksum
|
172
|
+
when :second_checksum
|
173
|
+
@checksum = @checksum + char
|
174
|
+
@state = :first_startbyte
|
175
|
+
|
176
|
+
crc = ("" << @length << @data).crc_xmodem
|
177
|
+
# received a valid packet
|
178
|
+
|
179
|
+
if @type == :data || @type == :data_no_crc
|
180
|
+
if @checksum == crc
|
181
|
+
|
182
|
+
receive_handler(@type, @counter, @data,@checksum)
|
183
|
+
else
|
184
|
+
# send NACK and discard packet
|
185
|
+
raise ChecksumMismatch, "ChecksumMismatch, expected #{crc}, was #{@checksum}"
|
186
|
+
end
|
187
|
+
else
|
188
|
+
# the packet is ACK, NACK or unknown, call receive-handler
|
189
|
+
# data may be mangled since the checksum is not checked
|
190
|
+
#
|
191
|
+
receive_handler(@type, @counter, @data, @checksum)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
@@ -0,0 +1,224 @@
|
|
1
|
+
#!usr/bin/ruby -w
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'enumerator'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
require File.join(File.dirname(__FILE__),'serial_packet.rb')
|
8
|
+
|
9
|
+
# FIXME: this should probably be put in a separate file
|
10
|
+
#
|
11
|
+
class String
|
12
|
+
def crc_xmodem
|
13
|
+
self.to_enum(:each_byte).inject(0) { |crc,byte|
|
14
|
+
crc = (crc ^ (byte << 8)) % 0x10000
|
15
|
+
8.times {
|
16
|
+
crc <<= 1
|
17
|
+
crc ^= 0x1021 if crc[16] == 1
|
18
|
+
}
|
19
|
+
crc
|
20
|
+
} % 0x10000
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module SerialInterface
|
25
|
+
VERSION = '0.3.0'
|
26
|
+
end
|
27
|
+
|
28
|
+
# PacketIO is used to wrap data in packets and send them
|
29
|
+
# over a serial port or some other IO
|
30
|
+
#
|
31
|
+
class PacketIO
|
32
|
+
attr_accessor :protocol_handler
|
33
|
+
|
34
|
+
# Takes two IO-Objects (uses "readchar" and "<<") to read and write from
|
35
|
+
#
|
36
|
+
def initialize(protocol, read, write = read, options = {})
|
37
|
+
@read, @write = read, write
|
38
|
+
@on_receive = nil
|
39
|
+
|
40
|
+
# Hashes contain SerialPackets that can be sent and received
|
41
|
+
#
|
42
|
+
@sendable_packets = {}
|
43
|
+
@receivable_packets = []
|
44
|
+
|
45
|
+
@waiting_threads = []
|
46
|
+
|
47
|
+
@protocol_handler = protocol.new(method(:send_callback),method(:receive_callback), options)
|
48
|
+
|
49
|
+
# Create the receiver thread, but do not start it yet
|
50
|
+
#
|
51
|
+
@receiver_thread = Thread.new do
|
52
|
+
Thread.abort_on_exception = true
|
53
|
+
Thread.stop
|
54
|
+
|
55
|
+
loop do
|
56
|
+
begin
|
57
|
+
char = @read.readchar
|
58
|
+
@protocol_handler.add_char_to_packet(char) if char
|
59
|
+
rescue EOFError
|
60
|
+
Thread.pass # there is currently nothing to read
|
61
|
+
end
|
62
|
+
end if @read # no need to loop, if there is nothing to read from
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# suspends the current thread, until +num+ packets have been received
|
67
|
+
# the thread will be resumed after all callbacks were called
|
68
|
+
#
|
69
|
+
def wait_for_packet(num_packets = 1, timeout = 10)
|
70
|
+
begin
|
71
|
+
@waiting_threads << {:num => num_packets, :thread => Thread.current}
|
72
|
+
sleep timeout
|
73
|
+
raise Timeout::Error, "Timeout"
|
74
|
+
rescue SerialProtocol::PacketReceived => e
|
75
|
+
ensure
|
76
|
+
# delete all occurrences of the current thread from the list of waiting threads,
|
77
|
+
# as we are obviously not waiting anymore
|
78
|
+
@waiting_threads.delete_if { |h| h[:thread] == Thread.current }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# The block given to this method is called with every received string
|
83
|
+
#
|
84
|
+
def on_receive(&block)
|
85
|
+
@on_receive = block
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_sender(hash = {})
|
89
|
+
hash.each { |k,v|
|
90
|
+
@sendable_packets[k] = v
|
91
|
+
}
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Add a type of packet, that should be checked for in the interface
|
96
|
+
#
|
97
|
+
# If a packet is received
|
98
|
+
#
|
99
|
+
def add_receiver(hash = {}, &block)
|
100
|
+
hash.each { |k,v|
|
101
|
+
@receivable_packets << {:packet => v, :block => block}
|
102
|
+
}
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Data to be wrapped in a packet
|
107
|
+
#
|
108
|
+
# there are different ways of using this method:
|
109
|
+
#
|
110
|
+
# send_packet(sym, *data)
|
111
|
+
# send_packet(sym, options = {}, *data)
|
112
|
+
# looks for a packet-class named sym and creates a new instance of this type
|
113
|
+
# of packet
|
114
|
+
# the optional hash is passed to the protocol layer
|
115
|
+
#
|
116
|
+
# send_packet(string, options = {})
|
117
|
+
# sends a raw string
|
118
|
+
# the optional hash is passed to the protocol layer
|
119
|
+
#
|
120
|
+
#
|
121
|
+
def send_packet(data, *args)
|
122
|
+
options = (Hash === args.first) ? options = args.shift : {}
|
123
|
+
data = (Symbol === data) ? @sendable_packets[data].new(*args) : data
|
124
|
+
|
125
|
+
@protocol_handler.send_packet(data.to_str, options)
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
# starts the receiver thread
|
130
|
+
#
|
131
|
+
def run
|
132
|
+
@receiver_thread.wakeup
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
def join
|
137
|
+
@receiver_thread.join
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# this method is called, when a packet should be sent
|
144
|
+
#
|
145
|
+
def send_callback(str)
|
146
|
+
@write << str if @write
|
147
|
+
end
|
148
|
+
|
149
|
+
def receive_callback(packet_str)
|
150
|
+
# call the on_receive event handler for every packet
|
151
|
+
@on_receive.call(packet_str) if @on_receive
|
152
|
+
|
153
|
+
# try to match the packet-string against the list of known packets
|
154
|
+
@receivable_packets.each { |h|
|
155
|
+
if h[:packet].matches?(packet_str)
|
156
|
+
h[:block].call( h[:packet].from_str(packet_str) )
|
157
|
+
end
|
158
|
+
}
|
159
|
+
|
160
|
+
# check if there are threads to wake up
|
161
|
+
#
|
162
|
+
@waiting_threads.each { |h|
|
163
|
+
h[:num] -= 1 # decrease the number of packets, this thread waits for
|
164
|
+
h[:thread].raise SerialProtocol::PacketReceived if h[:num] == 0
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
module SerialProtocol
|
171
|
+
class ChecksumMismatch < RuntimeError
|
172
|
+
end
|
173
|
+
|
174
|
+
class PacketReceived < Exception
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
module SerialProtocol
|
179
|
+
|
180
|
+
# The classes in this section implement wrappers for specific protocols
|
181
|
+
# to be used on a serial port
|
182
|
+
#
|
183
|
+
# They need to implement the following methods:
|
184
|
+
#
|
185
|
+
# initialize(send_callback, receive_callback, option_hash = {})
|
186
|
+
# creates a new instance of the protocol object
|
187
|
+
# it gets two methods to talk back to the interface
|
188
|
+
# class
|
189
|
+
#
|
190
|
+
# add_char_to_packet(char)
|
191
|
+
# called for each char, that is received.
|
192
|
+
#
|
193
|
+
# send_packet(data, options)
|
194
|
+
# called from the application to send a packet. The class is
|
195
|
+
# expected to wrap the data in the specific packet format string and in
|
196
|
+
# turn call send_callback(data_str) which will take care of the actual
|
197
|
+
# transmission
|
198
|
+
#
|
199
|
+
# A protocol class is expected to call receive_callback(packet_str) as soon
|
200
|
+
# as a valid packet is received
|
201
|
+
#
|
202
|
+
|
203
|
+
|
204
|
+
class LineBased
|
205
|
+
def initialize(send_callback, receive_callback, options = {})
|
206
|
+
@send_callback, @receive_callback = send_callback, receive_callback
|
207
|
+
@packet_buffer = ""
|
208
|
+
end
|
209
|
+
|
210
|
+
def add_char_to_packet(char)
|
211
|
+
if /\n/ === char.chr
|
212
|
+
@receive_callback.call(@receive_buffer)
|
213
|
+
@packet_buffer = ""
|
214
|
+
else
|
215
|
+
@packet_buffer << char
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def send_packet(data, options = {})
|
220
|
+
@send_callback.call(data + "\n")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#!usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'traits' # from http://www.codeforpeople.com/lib/ruby/traits/traits-0.8.1/
|
5
|
+
|
6
|
+
# SerialPacket is used to define the format of Packets that
|
7
|
+
# can be sent and received over a serial link
|
8
|
+
#
|
9
|
+
# they are essentially a description how to create a string
|
10
|
+
# representation from an array
|
11
|
+
#
|
12
|
+
# a packet has the following properties:
|
13
|
+
#
|
14
|
+
# data_format(string)
|
15
|
+
# this is a string that is passed to 'pack' and 'unpack'
|
16
|
+
#
|
17
|
+
# header_format(string)
|
18
|
+
# this is the format of the header of received packets
|
19
|
+
# this property is used with SerialPacket.matches?
|
20
|
+
#
|
21
|
+
# header_data
|
22
|
+
# an array that is used to decide if a given String is
|
23
|
+
#
|
24
|
+
module SerialPacketModule
|
25
|
+
def self.included(other)
|
26
|
+
other.class_eval do
|
27
|
+
|
28
|
+
def initialize_from_packet(str)
|
29
|
+
self.data = str.unpack(self.class.data_format)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(*d)
|
33
|
+
self.data = d
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_str
|
37
|
+
self.class.header_str << self.data.pack(self.class.data_format)
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
|
42
|
+
# a packet can only be sent if it has a header
|
43
|
+
#
|
44
|
+
def sendable?
|
45
|
+
(self.header && self.header_format) ? true : false
|
46
|
+
end
|
47
|
+
|
48
|
+
def header_str
|
49
|
+
if sendable?
|
50
|
+
h = self.header || []
|
51
|
+
h.pack(self.header_format) || ""
|
52
|
+
else
|
53
|
+
""
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# a packet can only be received, if it has a filter-expression
|
58
|
+
def receiveable?
|
59
|
+
defined? header_format and header_filter
|
60
|
+
end
|
61
|
+
|
62
|
+
# checks if some string conforms to the format of this packet
|
63
|
+
#
|
64
|
+
# this is tested by matching the packet "header" against the
|
65
|
+
# provided filter-expression
|
66
|
+
#
|
67
|
+
def matches?(str)
|
68
|
+
header = str.unpack(header_format)
|
69
|
+
filter = self.header_filter || []
|
70
|
+
filter.zip(header) { |f,a| return false unless f === a }
|
71
|
+
return true
|
72
|
+
end
|
73
|
+
|
74
|
+
def from_str(str) #:nodoc:
|
75
|
+
p = self.allocate
|
76
|
+
p.initialize_from_packet(str)
|
77
|
+
p
|
78
|
+
end
|
79
|
+
|
80
|
+
def create(&block)
|
81
|
+
klass = Class.new(self)
|
82
|
+
klass.instance_eval(&block) if block
|
83
|
+
return klass
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class SerialPacket
|
91
|
+
include SerialPacketModule
|
92
|
+
|
93
|
+
class_trait :data_format => "C*" # packet defaults to an array of bytes
|
94
|
+
class_trait :header_format => "CC" # it has a 2-byte header
|
95
|
+
class_trait :header_filter => "" # it will react to every byte
|
96
|
+
class_trait :header => nil # there is no default header
|
97
|
+
# this should be "[]" but arrays do not work
|
98
|
+
# in traits
|
99
|
+
traits :data => nil
|
100
|
+
|
101
|
+
# to store custom data in the packat, override
|
102
|
+
# one or more of the following methods:
|
103
|
+
#
|
104
|
+
# initialize_from_packet(str)
|
105
|
+
# populate instance variables with data
|
106
|
+
#
|
107
|
+
# to_str
|
108
|
+
# return the string that is sent over the wire
|
109
|
+
#
|
110
|
+
# matches?(str)
|
111
|
+
# default implementation uses +header_format+ and +header+
|
112
|
+
# to determine if a given string matches a packet
|
113
|
+
|
114
|
+
end
|
115
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{serial_interface}
|
8
|
+
s.version = "0.3.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Levin Alexander"]
|
12
|
+
s.date = %q{2010-01-15}
|
13
|
+
s.description = %q{serial_interface intends to be a small library that makes it easy
|
14
|
+
to define packet based protocols over a serial link (RS232) in a
|
15
|
+
declarative fashion.}
|
16
|
+
s.email = %q{mail@levinalex.net}
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".gitignore",
|
23
|
+
"History.txt",
|
24
|
+
"Rakefile",
|
25
|
+
"lib/protocol/rca2006.rb",
|
26
|
+
"lib/serial_interface.rb",
|
27
|
+
"lib/serial_packet.rb",
|
28
|
+
"serial_interface.gemspec",
|
29
|
+
"test/test_serial_interface.rb",
|
30
|
+
"test/test_serial_io.rb",
|
31
|
+
"test/test_serial_packets.rb"
|
32
|
+
]
|
33
|
+
s.homepage = %q{http://github.com/levinalex/serial_interface}
|
34
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
s.rubygems_version = %q{1.3.5}
|
37
|
+
s.summary = %q{abstracts protocols on a serial link}
|
38
|
+
s.test_files = [
|
39
|
+
"test/helper.rb",
|
40
|
+
"test/test_serial_interface.rb",
|
41
|
+
"test/test_serial_io.rb",
|
42
|
+
"test/test_serial_packets.rb"
|
43
|
+
]
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_runtime_dependency(%q<traits>, [">= 0"])
|
51
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<traits>, [">= 0"])
|
54
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
55
|
+
end
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<traits>, [">= 0"])
|
58
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'stringio'
|
5
|
+
require 'serial_interface'
|
6
|
+
require 'protocol/rca2006'
|
7
|
+
|
8
|
+
class TestSerialInterface < Test::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
|
11
|
+
@data_packet = SerialPacket.create { data_format "C*"; header [?:,?D] }
|
12
|
+
|
13
|
+
@io_send = StringIO.new
|
14
|
+
@io_receive = StringIO.new
|
15
|
+
|
16
|
+
@sender = PacketIO.new(SerialProtocol::RCA2006, nil, @io_send)
|
17
|
+
@receiver = PacketIO.new(SerialProtocol::RCA2006, @io_receive, nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_send_packet
|
21
|
+
@sender.add_sender(:data => @data_packet).run
|
22
|
+
|
23
|
+
@sender.send_packet :data, ?A, ?B, ?C, ?D, ?E
|
24
|
+
@io_send.rewind
|
25
|
+
|
26
|
+
assert_equal("\x65\xEB\x00\x00\a:DABCDE\2443",@io_send.read)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_receive_packet
|
30
|
+
@io_receive << "\x65\xEB\x00\x00\a:DABCDE\2443"
|
31
|
+
@io_receive.rewind
|
32
|
+
|
33
|
+
@receiver.add_receiver(:data => @data_packet) do |packet|
|
34
|
+
assert_equal( [?:,?D,?A,?B,?C,?D,?E], packet.data )
|
35
|
+
end
|
36
|
+
@receiver.run
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_timeout
|
40
|
+
@receiver.on_receive { |str| @data = str }
|
41
|
+
@receiver.run
|
42
|
+
|
43
|
+
Thread.new {
|
44
|
+
Thread.pass
|
45
|
+
@io_receive << "\x65\xEB\x00\x00\a:DABCDE\2443"
|
46
|
+
@io_receive.rewind
|
47
|
+
}
|
48
|
+
|
49
|
+
assert_equal( nil, @data )
|
50
|
+
assert_nothing_raised {
|
51
|
+
@receiver.wait_for_packet(1,2)
|
52
|
+
}
|
53
|
+
assert_equal( ":DABCDE", @data )
|
54
|
+
|
55
|
+
assert_raises(Timeout::Error) {
|
56
|
+
@receiver.wait_for_packet(1,1)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
require 'serial_packet'
|
7
|
+
|
8
|
+
class TestPacketFilter < Test::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
@default_packet_format = SerialPacket.create
|
11
|
+
@default_packet = @default_packet_format.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_instantiate_empty_packet
|
15
|
+
assert_equal([], @default_packet.data)
|
16
|
+
assert_equal("", @default_packet.to_str)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_instantiate_basic_packet
|
20
|
+
p = @default_packet_format.new ?A,?B,?C
|
21
|
+
|
22
|
+
assert_equal([?A,?B,?C], p.data)
|
23
|
+
assert_equal("ABC", p.to_str)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_packet_format_string
|
27
|
+
my_packet = SerialPacket.create { data_format "A*" }
|
28
|
+
p = my_packet.new "Hallo"
|
29
|
+
|
30
|
+
assert_equal(["Hallo"], p.data)
|
31
|
+
assert_equal("Hallo", p.to_str)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_packet_create_from_str
|
35
|
+
p = @default_packet_format.from_str("bar")
|
36
|
+
|
37
|
+
assert_equal([?b,?a,?r], p.data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_packet_header
|
41
|
+
my_packet = SerialPacket.create { header_format "CC"; header [?a,?b] }
|
42
|
+
|
43
|
+
assert_equal([?a,?b], my_packet.header)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_match
|
47
|
+
empty = SerialPacket.create
|
48
|
+
numbers = SerialPacket.create { header_format "ss"; header_filter [-1,32767] }
|
49
|
+
regex = SerialPacket.create{ header_format "a*"; header_filter [/foo/] }
|
50
|
+
mixed = SerialPacket.create { header_format "@5C @2C"; header_filter [?X,?Y] }
|
51
|
+
|
52
|
+
assert_equal true, empty.matches?("abcde")
|
53
|
+
|
54
|
+
assert_equal(true, numbers.matches?("\xff\xff\xff\x7f"))
|
55
|
+
assert_equal(false, numbers.matches?("\xff\xff\xff\x80"))
|
56
|
+
assert_equal(false, numbers.matches?("Packet with foo in it"))
|
57
|
+
assert_equal(true, regex.matches?("Packet with foo in it"))
|
58
|
+
assert_equal(false, regex.matches?("Packet with bar in it"))
|
59
|
+
assert_equal(false, regex.matches?("\xff\xff\xff\x80"))
|
60
|
+
assert_equal true, mixed.matches?("__Y__X___")
|
61
|
+
assert_equal false, mixed.matches?("YYYYYYY")
|
62
|
+
end
|
63
|
+
|
64
|
+
class Position < SerialPacket
|
65
|
+
header_format "CC"
|
66
|
+
header [?P,?p]
|
67
|
+
data_format "SS"
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_position_packet
|
71
|
+
p = Position.new 1, -1
|
72
|
+
assert_equal("Pp\001\000\xff\xff", p.to_str)
|
73
|
+
end
|
74
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: serial_interface
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Levin Alexander
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-15 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: traits
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: thoughtbot-shoulda
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: |-
|
36
|
+
serial_interface intends to be a small library that makes it easy
|
37
|
+
to define packet based protocols over a serial link (RS232) in a
|
38
|
+
declarative fashion.
|
39
|
+
email: mail@levinalex.net
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- LICENSE
|
46
|
+
- README.rdoc
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- History.txt
|
50
|
+
- Rakefile
|
51
|
+
- lib/protocol/rca2006.rb
|
52
|
+
- lib/serial_interface.rb
|
53
|
+
- lib/serial_packet.rb
|
54
|
+
- serial_interface.gemspec
|
55
|
+
- test/test_serial_interface.rb
|
56
|
+
- test/test_serial_io.rb
|
57
|
+
- test/test_serial_packets.rb
|
58
|
+
- LICENSE
|
59
|
+
- README.rdoc
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://github.com/levinalex/serial_interface
|
62
|
+
licenses: []
|
63
|
+
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options:
|
66
|
+
- --charset=UTF-8
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
version:
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
version:
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.3.5
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: abstracts protocols on a serial link
|
88
|
+
test_files:
|
89
|
+
- test/helper.rb
|
90
|
+
- test/test_serial_interface.rb
|
91
|
+
- test/test_serial_io.rb
|
92
|
+
- test/test_serial_packets.rb
|