levinalex-serial_interface 0.2.3
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.
- data/History.txt +3 -0
- data/Manifest.txt +11 -0
- data/README.txt +54 -0
- data/Rakefile +20 -0
- data/lib/protocol/rca2006.rb +197 -0
- data/lib/serial_interface.rb +224 -0
- data/lib/serial_packet.rb +116 -0
- data/serial_interface.gemspec +36 -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 +86 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
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,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
require './lib/serial_interface.rb'
|
6
|
+
|
7
|
+
Hoe.new('serial_interface', SerialInterface::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'serial_interface'
|
9
|
+
p.summary = "abstracts protocols on a serial link"
|
10
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
11
|
+
p.url = "http://levinalex.net/src/serial_interface"
|
12
|
+
p.developer('Levin Alexander', 'mail@levinalex.net')
|
13
|
+
|
14
|
+
p.extra_deps = ["traits"]
|
15
|
+
end
|
16
|
+
|
17
|
+
task :cultivate do
|
18
|
+
system "touch Manifest.txt; rake check_manifest | grep -v \"(in \" | patch"
|
19
|
+
system "rake debug_gem | grep -v \"(in \" > `basename \\`pwd\\``.gemspec"
|
20
|
+
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.2.3'
|
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,116 @@
|
|
1
|
+
#!usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
gem 'traits', "0.8.1"
|
5
|
+
require 'traits' # from http://www.codeforpeople.com/lib/ruby/traits/traits-0.8.1/
|
6
|
+
|
7
|
+
# SerialPacket is used to define the format of Packets that
|
8
|
+
# can be sent and received over a serial link
|
9
|
+
#
|
10
|
+
# they are essentially a description how to create a string
|
11
|
+
# representation from an array
|
12
|
+
#
|
13
|
+
# a packet has the following properties:
|
14
|
+
#
|
15
|
+
# data_format(string)
|
16
|
+
# this is a string that is passed to 'pack' and 'unpack'
|
17
|
+
#
|
18
|
+
# header_format(string)
|
19
|
+
# this is the format of the header of received packets
|
20
|
+
# this property is used with SerialPacket.matches?
|
21
|
+
#
|
22
|
+
# header_data
|
23
|
+
# an array that is used to decide if a given String is
|
24
|
+
#
|
25
|
+
module SerialPacketModule
|
26
|
+
def self.included(other)
|
27
|
+
other.class_eval do
|
28
|
+
|
29
|
+
def initialize_from_packet(str)
|
30
|
+
self.data = str.unpack(self.class.data_format)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(*d)
|
34
|
+
self.data = d
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_str
|
38
|
+
self.class.header_str << self.data.pack(self.class.data_format)
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
|
43
|
+
# a packet can only be sent if it has a header
|
44
|
+
#
|
45
|
+
def sendable?
|
46
|
+
(self.header && self.header_format) ? true : false
|
47
|
+
end
|
48
|
+
|
49
|
+
def header_str
|
50
|
+
if sendable?
|
51
|
+
h = self.header || []
|
52
|
+
h.pack(self.header_format) || ""
|
53
|
+
else
|
54
|
+
""
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# a packet can only be received, if it has a filter-expression
|
59
|
+
def receiveable?
|
60
|
+
defined? header_format and header_filter
|
61
|
+
end
|
62
|
+
|
63
|
+
# checks if some string conforms to the format of this packet
|
64
|
+
#
|
65
|
+
# this is tested by matching the packet "header" against the
|
66
|
+
# provided filter-expression
|
67
|
+
#
|
68
|
+
def matches?(str)
|
69
|
+
header = str.unpack(header_format)
|
70
|
+
filter = self.header_filter || []
|
71
|
+
filter.zip(header) { |f,a| return false unless f === a }
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
|
75
|
+
def from_str(str) #:nodoc:
|
76
|
+
p = self.allocate
|
77
|
+
p.initialize_from_packet(str)
|
78
|
+
p
|
79
|
+
end
|
80
|
+
|
81
|
+
def create(&block)
|
82
|
+
klass = Class.new(self)
|
83
|
+
klass.instance_eval(&block) if block
|
84
|
+
return klass
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class SerialPacket
|
92
|
+
include SerialPacketModule
|
93
|
+
|
94
|
+
class_trait :data_format => "C*" # packet defaults to an array of bytes
|
95
|
+
class_trait :header_format => "CC" # it has a 2-byte header
|
96
|
+
class_trait :header_filter => "" # it will react to every byte
|
97
|
+
class_trait :header => nil # there is no default header
|
98
|
+
# this should be "[]" but arrays do not work
|
99
|
+
# in traits
|
100
|
+
traits :data => nil
|
101
|
+
|
102
|
+
# to store custom data in the packat, override
|
103
|
+
# one or more of the following methods:
|
104
|
+
#
|
105
|
+
# initialize_from_packet(str)
|
106
|
+
# populate instance variables with data
|
107
|
+
#
|
108
|
+
# to_str
|
109
|
+
# return the string that is sent over the wire
|
110
|
+
#
|
111
|
+
# matches?(str)
|
112
|
+
# default implementation uses +header_format+ and +header+
|
113
|
+
# to determine if a given string matches a packet
|
114
|
+
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{serial_interface}
|
3
|
+
s.version = "0.2.3"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Levin Alexander"]
|
7
|
+
s.date = %q{2008-10-12}
|
8
|
+
s.description = %q{serial_interface intends to be a small library that makes it easy to define packet based protocols over a serial link (RS232) in a declarative fashion.}
|
9
|
+
s.email = ["mail@levinalex.net"]
|
10
|
+
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
|
11
|
+
s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "lib/protocol/rca2006.rb", "lib/serial_interface.rb", "lib/serial_packet.rb", "serial_interface.gemspec", "test/test_serial_interface.rb", "test/test_serial_io.rb", "test/test_serial_packets.rb"]
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.homepage = %q{http://levinalex.net/src/serial_interface}
|
14
|
+
s.rdoc_options = ["--main", "README.txt"]
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.rubyforge_project = %q{serial_interface}
|
17
|
+
s.rubygems_version = %q{1.2.0}
|
18
|
+
s.summary = %q{abstracts protocols on a serial link}
|
19
|
+
s.test_files = ["test/test_serial_interface.rb", "test/test_serial_io.rb", "test/test_serial_packets.rb"]
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 2
|
24
|
+
|
25
|
+
if current_version >= 3 then
|
26
|
+
s.add_runtime_dependency(%q<traits>, [">= 0"])
|
27
|
+
s.add_development_dependency(%q<hoe>, [">= 1.8.0"])
|
28
|
+
else
|
29
|
+
s.add_dependency(%q<traits>, [">= 0"])
|
30
|
+
s.add_dependency(%q<hoe>, [">= 1.8.0"])
|
31
|
+
end
|
32
|
+
else
|
33
|
+
s.add_dependency(%q<traits>, [">= 0"])
|
34
|
+
s.add_dependency(%q<hoe>, [">= 1.8.0"])
|
35
|
+
end
|
36
|
+
end
|
@@ -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,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: levinalex-serial_interface
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Levin Alexander
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-12 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: traits
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0"
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: hoe
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.8.0
|
32
|
+
version:
|
33
|
+
description: serial_interface intends to be a small library that makes it easy to define packet based protocols over a serial link (RS232) in a declarative fashion.
|
34
|
+
email:
|
35
|
+
- mail@levinalex.net
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- History.txt
|
42
|
+
- Manifest.txt
|
43
|
+
- README.txt
|
44
|
+
files:
|
45
|
+
- History.txt
|
46
|
+
- Manifest.txt
|
47
|
+
- README.txt
|
48
|
+
- Rakefile
|
49
|
+
- lib/protocol/rca2006.rb
|
50
|
+
- lib/serial_interface.rb
|
51
|
+
- lib/serial_packet.rb
|
52
|
+
- serial_interface.gemspec
|
53
|
+
- test/test_serial_interface.rb
|
54
|
+
- test/test_serial_io.rb
|
55
|
+
- test/test_serial_packets.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://levinalex.net/src/serial_interface
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options:
|
60
|
+
- --main
|
61
|
+
- README.txt
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project: serial_interface
|
79
|
+
rubygems_version: 1.2.0
|
80
|
+
signing_key:
|
81
|
+
specification_version: 2
|
82
|
+
summary: abstracts protocols on a serial link
|
83
|
+
test_files:
|
84
|
+
- test/test_serial_interface.rb
|
85
|
+
- test/test_serial_io.rb
|
86
|
+
- test/test_serial_packets.rb
|