mattly-datagrammer 0.1.1 → 0.2
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/lib/datagrammer.rb +9 -4
- data/lib/datagrammer/generic_handler.rb +54 -0
- data/lib/datagrammer/packet.rb +14 -11
- data/lib/datagrammer/packet_scanner.rb +26 -25
- data/spec/generic_handler_spec.rb +60 -0
- data/spec/packet_scanner_spec.rb +5 -5
- data/spec/packet_spec.rb +0 -9
- metadata +7 -5
data/lib/datagrammer.rb
CHANGED
@@ -3,6 +3,7 @@ require 'socket'
|
|
3
3
|
$:.unshift File.dirname(__FILE__)
|
4
4
|
require 'datagrammer/packet'
|
5
5
|
require 'datagrammer/packet_scanner'
|
6
|
+
require 'datagrammer/generic_handler'
|
6
7
|
|
7
8
|
# A Datagrammer object will listen on a given address and port for messages and
|
8
9
|
# will decode the data (it's assumed to be in a OSC-style format) and perform
|
@@ -15,8 +16,9 @@ require 'datagrammer/packet_scanner'
|
|
15
16
|
# # OSC-style packet (f.e. from Max/MSP's udpsender) will be echoed back on port
|
16
17
|
# # 5001 in the string format given in the block.
|
17
18
|
class Datagrammer
|
19
|
+
attr_accessor :thread, :socket, :speak_address, :speak_port
|
18
20
|
|
19
|
-
#
|
21
|
+
# Creates a new Datagrammer object bound to the specified port. The following
|
20
22
|
# options are available:
|
21
23
|
# * (({:address})): IP to listen on. defaults to "0.0.0.0"
|
22
24
|
# * (({:speak_address})): default IP to send to. defaults to "0.0.0.0"
|
@@ -24,18 +26,20 @@ class Datagrammer
|
|
24
26
|
def initialize(port, opts={})
|
25
27
|
@port = port
|
26
28
|
@address = opts[:address] || "0.0.0.0"
|
27
|
-
@speak_address = opts[:speak_address] ||
|
29
|
+
@speak_address = opts[:speak_address] || @address
|
28
30
|
@speak_port = opts[:speak_port] || port + 1
|
29
31
|
@socket = UDPSocket.new
|
30
32
|
@socket.bind(@address, @port)
|
31
33
|
end
|
32
34
|
|
35
|
+
# Sets the default speak destination
|
33
36
|
def speak_destination=(addr, port)
|
34
37
|
@speak_address, @speak_port = addr, port
|
35
38
|
end
|
36
39
|
|
37
|
-
|
38
|
-
|
40
|
+
# Starts the thread to listen on the selected port. A block is required, that will get
|
41
|
+
# three arguments: the datagrammer object itself, the packet data (as an array), and the
|
42
|
+
# address of the sender.
|
39
43
|
def listen(&block)
|
40
44
|
@thread = Thread.start do
|
41
45
|
loop do
|
@@ -46,6 +50,7 @@ class Datagrammer
|
|
46
50
|
end
|
47
51
|
end
|
48
52
|
|
53
|
+
# Encodes and sends a packet to the specified address and port
|
49
54
|
def speak(message, addr=@speak_address, port=@speak_port)
|
50
55
|
@socket.send(Packet.encode([message]), 0, addr, port)
|
51
56
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class Datagrammer
|
2
|
+
# A simple class for registering multiple "handlers" for messages that come from a Datagrammer object.
|
3
|
+
class GenericHandler
|
4
|
+
attr_accessor :rules
|
5
|
+
|
6
|
+
# Takes a hash of key, proc values for an initial set of rules. Rule keys are matched
|
7
|
+
# against the first item in the "data" value passed in from Datagrammer, or the
|
8
|
+
# so-called "path" of the message. Data like (({['/foo/bar', 'baz', 'bee']})) would
|
9
|
+
# use "/foo/bar" as the path to match against.
|
10
|
+
#
|
11
|
+
# Rules are evaluated in this order:
|
12
|
+
# * If a rule key is a string and it matches the path exactly, that rule will be used.
|
13
|
+
# * If no string rule keys match, any rule keys that are regexes will be run against the
|
14
|
+
# path and ALL regexes that match will be called. The match(es) from the regex will be
|
15
|
+
# prepended to the argument list passed to the proc.
|
16
|
+
# * If there is no match yet and a rule key called '\default' exists, that rule will be used.
|
17
|
+
# * If no match has been made at this point, nothing will happen, the data will be ignored.
|
18
|
+
def initialize(rules={})
|
19
|
+
@rules = rules
|
20
|
+
end
|
21
|
+
|
22
|
+
# sets a proc to be called when a rule is matched. See #new for more information.
|
23
|
+
def register_rule(address, block)
|
24
|
+
@rules[address] = block
|
25
|
+
end
|
26
|
+
|
27
|
+
# What Datagrammer calls. Runs the path against registered rules as described in #new
|
28
|
+
def handle(address, arguments=[])
|
29
|
+
list = if @rules.has_key?(address)
|
30
|
+
[[@rules[address], arguments]]
|
31
|
+
else
|
32
|
+
matches = @rules.select {|k,v| k.kind_of?(Regexp) && address =~ k }
|
33
|
+
if ! matches.empty?
|
34
|
+
matches.collect do |regex, p|
|
35
|
+
msg = [address.scan(regex), arguments].flatten
|
36
|
+
msg.delete('')
|
37
|
+
[p, msg]
|
38
|
+
end
|
39
|
+
elsif @rules.has_key?('\default')
|
40
|
+
[[@rules['\default'], arguments]]
|
41
|
+
else
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
list.map {|callback, args| callback.call(args) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# returns a lambda for a Datagrammer object's listen method
|
49
|
+
def handler
|
50
|
+
lambda {|dg, args, sender| self.handle(args.shift, args) }
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/lib/datagrammer/packet.rb
CHANGED
@@ -1,36 +1,39 @@
|
|
1
1
|
require 'strscan'
|
2
2
|
|
3
3
|
class Datagrammer
|
4
|
+
|
5
|
+
# Decodes and Encodes packets into a basic OSC (Open Sound Control)-like format.
|
4
6
|
module Packet
|
5
7
|
|
8
|
+
# decodes packet data from f.e. (({"hello\000\000\000,s\000\000world\000\000\000"})) to
|
9
|
+
# (({%w(hello world)}))
|
6
10
|
def self.decode(packet_string='')
|
7
|
-
scanner =
|
11
|
+
scanner = PacketScanner.new(packet_string)
|
8
12
|
message = scanner.scan_string
|
9
13
|
argument_types = scanner.scan_string.sub(/^,/,'').split('')
|
10
|
-
arguments = argument_types.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
memo
|
14
|
+
arguments = argument_types.collect do |type|
|
15
|
+
{ 's' => lambda { scanner.scan_string },
|
16
|
+
'i' => lambda { scanner.scan_integer },
|
17
|
+
'f' => lambda { scanner.scan_float }
|
18
|
+
}[type].call()
|
17
19
|
end
|
18
20
|
arguments.unshift(message)
|
19
21
|
end
|
20
22
|
|
21
|
-
|
23
|
+
# Turns a list or array into an encoded string
|
24
|
+
def self.encode(*message)
|
22
25
|
message = [message].flatten
|
23
26
|
string = pad(message.shift)
|
24
27
|
string += encode_arguments(message)
|
25
28
|
end
|
26
29
|
|
30
|
+
protected
|
31
|
+
|
27
32
|
def self.pad(string='')
|
28
33
|
string += "\000"
|
29
34
|
string + "\000" * ((4-string.size) % 4)
|
30
35
|
end
|
31
36
|
|
32
|
-
protected
|
33
|
-
|
34
37
|
def self.encode_arguments(arguments)
|
35
38
|
encode_argument_types(arguments) + encode_argument_data(arguments)
|
36
39
|
end
|
@@ -1,27 +1,28 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def scan_string
|
9
|
-
string = scan(/[^\000]+/)
|
10
|
-
skip_buffer
|
11
|
-
string
|
12
|
-
end
|
13
|
-
|
14
|
-
def scan_integer
|
15
|
-
integer = scan(/.{4}/m).unpack('N').first
|
16
|
-
integer = -1 * (2**32 - integer) if integer > (2**31 - 1)
|
17
|
-
integer
|
18
|
-
end
|
19
|
-
|
20
|
-
def scan_float
|
21
|
-
scan(/.{4}/).unpack('g').first
|
22
|
-
end
|
23
|
-
|
1
|
+
# Subclass of StringScanner to take the boring out of certain aspects of packet decoding.
|
2
|
+
class PacketScanner < StringScanner
|
3
|
+
|
4
|
+
# Moves the scan head to the next multiple of 4
|
5
|
+
def skip_buffer
|
6
|
+
self.pos += 4 - (pos % 4)
|
24
7
|
end
|
8
|
+
|
9
|
+
# Grabs all non-null characters and moves the scan head past the null buffer
|
10
|
+
def scan_string
|
11
|
+
string = scan(/[^\000]+/)
|
12
|
+
skip_buffer
|
13
|
+
string
|
14
|
+
end
|
15
|
+
|
16
|
+
# Decodes an integer, adjusting for polarity
|
17
|
+
def scan_integer
|
18
|
+
integer = scan(/.{4}/m).unpack('N').first
|
19
|
+
integer = -1 * (2**32 - integer) if integer > (2**31 - 1)
|
20
|
+
integer
|
21
|
+
end
|
22
|
+
|
23
|
+
# Decodes a 32-bit float
|
24
|
+
def scan_float
|
25
|
+
scan(/.{4}/).unpack('g').first
|
26
|
+
end
|
27
|
+
|
25
28
|
end
|
26
|
-
|
27
|
-
StringScanner.send(:include, Datagrammer::PacketScanner)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Datagrammer::GenericHandler do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@handler = Datagrammer::GenericHandler.new
|
7
|
+
@foo = lambda {|args| "foo: #{args.join(', ')}" }
|
8
|
+
@handler.register_rule('/foo', @foo)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "registers handlers" do
|
12
|
+
@handler.rules['/foo'].should == @foo
|
13
|
+
end
|
14
|
+
|
15
|
+
it "calls the handler for a given address" do
|
16
|
+
@handler.handle('/foo', %w(bar baz)).should == ["foo: bar, baz"]
|
17
|
+
end
|
18
|
+
|
19
|
+
it "spits out a lambda for calling the handler from DG" do
|
20
|
+
@handler.should_receive(:handle).with('/foo',%w(bar baz))
|
21
|
+
@handler.handler.call(nil, %w(/foo bar baz), nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "regex handlers" do
|
25
|
+
before do
|
26
|
+
@i = Hash.new {|hash, key| hash[key] = [] }
|
27
|
+
@handler.register_rule(/^\/reg\/(.*)$/, lambda {|a| @i[a.shift] += a})
|
28
|
+
end
|
29
|
+
|
30
|
+
it "accepts regexes as hanlder keys and calls their values when matched" do
|
31
|
+
@handler.handle('/reg/foo', %w(bar baz))
|
32
|
+
@i.should == {'foo' => %w(bar baz)}
|
33
|
+
end
|
34
|
+
|
35
|
+
it "handles values for ALL regexes that match given string" do
|
36
|
+
@handler.register_rule(/.*/, lambda {|a| @i['default'] += a })
|
37
|
+
@handler.handle('/reg/foo', %w(bar baz))
|
38
|
+
@i.should == {'foo' => %w(bar baz), 'default' => %w(/reg/foo bar baz)}
|
39
|
+
end
|
40
|
+
|
41
|
+
it "does not use regexes if exact match from string" do
|
42
|
+
@handler.register_rule('/reg/foo', lambda {|a| @i['exact'] += a })
|
43
|
+
@handler.handle('/reg/foo', %w(bar baz))
|
44
|
+
@i.should == {'exact' => %w(bar baz)}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "no handler found" do
|
49
|
+
it "uses 'default' if exists" do
|
50
|
+
default = lambda { 'default' }
|
51
|
+
@handler.register_rule('\default', default)
|
52
|
+
@handler.handle('/non-existant').should == ['default']
|
53
|
+
end
|
54
|
+
|
55
|
+
it "does nothing if no default handler" do
|
56
|
+
@handler.handle('/non-existant').should == []
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/spec/packet_scanner_spec.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe PacketScanner do
|
4
4
|
|
5
5
|
describe "skip_buffer" do
|
6
6
|
it "sets the position to the next multiple of four" do
|
7
|
-
s =
|
7
|
+
s = PacketScanner.new('more than four bytes')
|
8
8
|
[[4,8], [6,8], [1,4]].each do |pos, expected|
|
9
9
|
s.pos = pos
|
10
10
|
s.skip_buffer
|
@@ -16,7 +16,7 @@ describe StringScanner do
|
|
16
16
|
describe "scan_string" do
|
17
17
|
|
18
18
|
before do
|
19
|
-
@s =
|
19
|
+
@s = PacketScanner.new("this is a string!\000\000\000end.")
|
20
20
|
@string = @s.scan_string
|
21
21
|
end
|
22
22
|
|
@@ -33,7 +33,7 @@ describe StringScanner do
|
|
33
33
|
[[1,"\000\000\000\001"], [-10, "\377\377\377\366"], [2147483647, "\177\377\377\377"]].each do |integer, encoded|
|
34
34
|
describe "decdoing #{integer}" do
|
35
35
|
before do
|
36
|
-
@s =
|
36
|
+
@s = PacketScanner.new(encoded)
|
37
37
|
@int = @s.scan_integer
|
38
38
|
end
|
39
39
|
|
@@ -52,7 +52,7 @@ describe StringScanner do
|
|
52
52
|
[[1.0, "?\200\000\000"], [3.141593, "@I\017\333"], [-1.618034, "\277\317\e\275"]].each do |float, encoded|
|
53
53
|
describe "decoding #{float}" do
|
54
54
|
before do
|
55
|
-
@s =
|
55
|
+
@s = PacketScanner.new(encoded)
|
56
56
|
@float = @s.scan_float
|
57
57
|
end
|
58
58
|
|
data/spec/packet_spec.rb
CHANGED
@@ -41,13 +41,4 @@ describe Datagrammer::Packet do
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
describe "pad" do
|
45
|
-
it "fills out to the nearest word length" do
|
46
|
-
Datagrammer::Packet.pad("h").should == "h\000\000\000"
|
47
|
-
end
|
48
|
-
|
49
|
-
it "appends nulls if string is already at wordlength" do
|
50
|
-
Datagrammer::Packet.pad("word").should == "word\000\000\000\000"
|
51
|
-
end
|
52
|
-
end
|
53
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mattly-datagrammer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.2"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Lyon
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-11-01 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -19,20 +19,22 @@ executables: []
|
|
19
19
|
|
20
20
|
extensions: []
|
21
21
|
|
22
|
-
extra_rdoc_files:
|
23
|
-
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.mkdn
|
24
24
|
files:
|
25
25
|
- README.mkdn
|
26
26
|
- Rakefile
|
27
27
|
- spec/datagrammer_spec.rb
|
28
|
+
- spec/generic_handler_spec.rb
|
28
29
|
- spec/packet_scanner_spec.rb
|
29
30
|
- spec/packet_spec.rb
|
30
31
|
- spec/spec_helper.rb
|
31
32
|
- lib/datagrammer
|
33
|
+
- lib/datagrammer/generic_handler.rb
|
32
34
|
- lib/datagrammer/packet.rb
|
33
35
|
- lib/datagrammer/packet_scanner.rb
|
34
36
|
- lib/datagrammer.rb
|
35
|
-
has_rdoc:
|
37
|
+
has_rdoc: true
|
36
38
|
homepage: http://github.com/mattly/datagrammer
|
37
39
|
post_install_message:
|
38
40
|
rdoc_options: []
|