mattly-datagrammer 0.1.1
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/README.mkdn +38 -0
- data/Rakefile +5 -0
- data/lib/datagrammer/packet.rb +60 -0
- data/lib/datagrammer/packet_scanner.rb +27 -0
- data/lib/datagrammer.rb +53 -0
- data/spec/datagrammer_spec.rb +36 -0
- data/spec/packet_scanner_spec.rb +70 -0
- data/spec/packet_spec.rb +53 -0
- data/spec/spec_helper.rb +15 -0
- metadata +62 -0
data/README.mkdn
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Datagrammer
|
2
|
+
|
3
|
+
by Matthew Lyon <matt@flowerpowered.com>
|
4
|
+
|
5
|
+
## DESCRIPTION:
|
6
|
+
|
7
|
+
Datagrammer helps take the pain out of UDP by mitigating some of the packet
|
8
|
+
encoding/decoding stuff, as well as providing a non-blocking listening thread.
|
9
|
+
|
10
|
+
## FEATURES
|
11
|
+
|
12
|
+
- Encodes/Decodes OSC-style packets (where a main 'address' is given along
|
13
|
+
with typed arguments)
|
14
|
+
- Listens for messages in its own thread, performs a callback when a
|
15
|
+
message is received.
|
16
|
+
- Has a default "speaking" address / port for talkback. the IP of the sender
|
17
|
+
of a received packet is also made available to the callback.
|
18
|
+
|
19
|
+
# SYNOPSIS
|
20
|
+
|
21
|
+
require 'datagrammer'
|
22
|
+
|
23
|
+
server = Datagrammer.new(5000)
|
24
|
+
server.listen do |serv, msg, sender|
|
25
|
+
serv.speak("thanks for your message #{sender}, I received: #{msg.join(', ')}")
|
26
|
+
end
|
27
|
+
|
28
|
+
# set to speak at server's default speak destination
|
29
|
+
client = Datagrammer.new(5001, :speak_port => 5000)
|
30
|
+
client.listen {|serv, msg| puts "rec'd #{msg}"}
|
31
|
+
|
32
|
+
client.speak(%w(hey joe))
|
33
|
+
sleep 0.1
|
34
|
+
# rec'd thanks for your message 127.0.0.1, I received: hey, joe
|
35
|
+
|
36
|
+
## REQUIREMENTS
|
37
|
+
|
38
|
+
* Rspec, if you wish to run the spec suite
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class Datagrammer
|
4
|
+
module Packet
|
5
|
+
|
6
|
+
def self.decode(packet_string='')
|
7
|
+
scanner = StringScanner.new(packet_string)
|
8
|
+
message = scanner.scan_string
|
9
|
+
argument_types = scanner.scan_string.sub(/^,/,'').split('')
|
10
|
+
arguments = argument_types.inject([]) do |memo, type|
|
11
|
+
case type
|
12
|
+
when 's'; memo << scanner.scan_string
|
13
|
+
when 'i'; memo << scanner.scan_integer
|
14
|
+
when 'f'; memo << scanner.scan_float
|
15
|
+
end
|
16
|
+
memo
|
17
|
+
end
|
18
|
+
arguments.unshift(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.encode(message=[])
|
22
|
+
message = [message].flatten
|
23
|
+
string = pad(message.shift)
|
24
|
+
string += encode_arguments(message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.pad(string='')
|
28
|
+
string += "\000"
|
29
|
+
string + "\000" * ((4-string.size) % 4)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def self.encode_arguments(arguments)
|
35
|
+
encode_argument_types(arguments) + encode_argument_data(arguments)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.encode_argument_types(arguments)
|
39
|
+
str = ','
|
40
|
+
str += arguments.collect do |argument|
|
41
|
+
case argument
|
42
|
+
when String; 's'
|
43
|
+
when Integer; 'i'
|
44
|
+
when Float; 'f'
|
45
|
+
end
|
46
|
+
end.join
|
47
|
+
pad(str)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.encode_argument_data(arguments)
|
51
|
+
arguments.collect do |argument|
|
52
|
+
case argument
|
53
|
+
when String; pad(argument)
|
54
|
+
when Integer; [argument].pack('N')
|
55
|
+
when Float; [argument].pack('g')
|
56
|
+
end
|
57
|
+
end.join
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Datagrammer
|
2
|
+
module PacketScanner
|
3
|
+
|
4
|
+
def skip_buffer
|
5
|
+
self.pos += 4 - (pos % 4)
|
6
|
+
end
|
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
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
StringScanner.send(:include, Datagrammer::PacketScanner)
|
data/lib/datagrammer.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__)
|
4
|
+
require 'datagrammer/packet'
|
5
|
+
require 'datagrammer/packet_scanner'
|
6
|
+
|
7
|
+
# A Datagrammer object will listen on a given address and port for messages and
|
8
|
+
# will decode the data (it's assumed to be in a OSC-style format) and perform
|
9
|
+
# a callback when data is received:
|
10
|
+
#
|
11
|
+
# dg = Datagrammer.new(5000)
|
12
|
+
# dg.listen {|d, message, sender| d.speak %|rec'd "#{message.join(', ')}" at #{Time.now.to_s} from #{sender}|}
|
13
|
+
# dg.thread.join
|
14
|
+
# # now, anything sent to port 5000 on 0.0.0.0 that conforms to a basic
|
15
|
+
# # OSC-style packet (f.e. from Max/MSP's udpsender) will be echoed back on port
|
16
|
+
# # 5001 in the string format given in the block.
|
17
|
+
class Datagrammer
|
18
|
+
|
19
|
+
# creates a new Datagrammer object bound to the specified port. The following
|
20
|
+
# options are available:
|
21
|
+
# * (({:address})): IP to listen on. defaults to "0.0.0.0"
|
22
|
+
# * (({:speak_address})): default IP to send to. defaults to "0.0.0.0"
|
23
|
+
# * (({:speak_port})): default port to speak on, defaults to port + 1
|
24
|
+
def initialize(port, opts={})
|
25
|
+
@port = port
|
26
|
+
@address = opts[:address] || "0.0.0.0"
|
27
|
+
@speak_address = opts[:speak_address] || "0.0.0.0"
|
28
|
+
@speak_port = opts[:speak_port] || port + 1
|
29
|
+
@socket = UDPSocket.new
|
30
|
+
@socket.bind(@address, @port)
|
31
|
+
end
|
32
|
+
|
33
|
+
def speak_destination=(addr, port)
|
34
|
+
@speak_address, @speak_port = addr, port
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :thread, :socket, :speak_address, :speak_port
|
38
|
+
|
39
|
+
def listen(&block)
|
40
|
+
@thread = Thread.start do
|
41
|
+
loop do
|
42
|
+
IO.select([@socket])
|
43
|
+
data, info = @socket.recvfrom(65535)
|
44
|
+
block.call(self, Packet.decode(data), info.last)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def speak(message, addr=@speak_address, port=@speak_port)
|
50
|
+
@socket.send(Packet.encode([message]), 0, addr, port)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Datagrammer do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@serv = Datagrammer.new(10000)
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
@serv.socket.close
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup_socket(port, addr="0.0.0.0")
|
14
|
+
s = UDPSocket.new
|
15
|
+
s.bind(addr, port)
|
16
|
+
s
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should send an encoded packet out to its speaking port" do
|
20
|
+
Thread.start { sleep 0.1; @serv.speak("hi") }
|
21
|
+
sock = setup_socket(10001)
|
22
|
+
IO.select [sock]
|
23
|
+
data, info = sock.recvfrom(1024)
|
24
|
+
sock.close
|
25
|
+
data.should == "hi\000\000,\000\000\000"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should receive encoded packets while listening and feed it to the callback" do
|
29
|
+
@serv.listen {|dg, msg| @foo = msg }
|
30
|
+
s = UDPSocket.new
|
31
|
+
s.send("hi\000\000,\000\000\000", 0, '0.0.0.0', 10000)
|
32
|
+
sleep 0.1
|
33
|
+
@foo.should == ['hi']
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe StringScanner do
|
4
|
+
|
5
|
+
describe "skip_buffer" do
|
6
|
+
it "sets the position to the next multiple of four" do
|
7
|
+
s = StringScanner.new('more than four bytes')
|
8
|
+
[[4,8], [6,8], [1,4]].each do |pos, expected|
|
9
|
+
s.pos = pos
|
10
|
+
s.skip_buffer
|
11
|
+
s.pos.should == expected
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "scan_string" do
|
17
|
+
|
18
|
+
before do
|
19
|
+
@s = StringScanner.new("this is a string!\000\000\000end.")
|
20
|
+
@string = @s.scan_string
|
21
|
+
end
|
22
|
+
|
23
|
+
it "extracts the string" do
|
24
|
+
@string.should == "this is a string!"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sets the position to the next word" do
|
28
|
+
@s.pos.should == 20
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "scan_integer" do
|
33
|
+
[[1,"\000\000\000\001"], [-10, "\377\377\377\366"], [2147483647, "\177\377\377\377"]].each do |integer, encoded|
|
34
|
+
describe "decdoing #{integer}" do
|
35
|
+
before do
|
36
|
+
@s = StringScanner.new(encoded)
|
37
|
+
@int = @s.scan_integer
|
38
|
+
end
|
39
|
+
|
40
|
+
it "decodes the integer correctly" do
|
41
|
+
@int.should == integer
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sets the position at the end of the word" do
|
45
|
+
@s.pos.should == 4
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "scan_float" do
|
52
|
+
[[1.0, "?\200\000\000"], [3.141593, "@I\017\333"], [-1.618034, "\277\317\e\275"]].each do |float, encoded|
|
53
|
+
describe "decoding #{float}" do
|
54
|
+
before do
|
55
|
+
@s = StringScanner.new(encoded)
|
56
|
+
@float = @s.scan_float
|
57
|
+
end
|
58
|
+
|
59
|
+
it "decodes the float value correctly" do
|
60
|
+
(@float * 100000).round.should == (float * 100000).round
|
61
|
+
end
|
62
|
+
|
63
|
+
it "sets the position to the end of the word" do
|
64
|
+
@s.pos.should == 4
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/spec/packet_spec.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Datagrammer::Packet do
|
4
|
+
describe "decode" do
|
5
|
+
it "handles a message with no arguments" do
|
6
|
+
Datagrammer::Packet.decode("hello\000\000\000,\000\000\000").should == ['hello']
|
7
|
+
end
|
8
|
+
|
9
|
+
it "handles a generic message with many arguments" do
|
10
|
+
encoded = "message\000,sifii\000\000str1\000\000\000\000\000\000\000\001@I\016V\177\377\377\377\377\377\377\366"
|
11
|
+
msg = Datagrammer::Packet.decode(encoded)
|
12
|
+
msg.shift.should == 'message'
|
13
|
+
msg.shift.should == 'str1'
|
14
|
+
msg.shift.should == 1
|
15
|
+
(msg.shift * 10000).round.should == 31415
|
16
|
+
msg.shift.should == 2147483647
|
17
|
+
msg.shift.should == -10
|
18
|
+
end
|
19
|
+
|
20
|
+
it "handes a message with only integer arguments" do
|
21
|
+
encoded = "/tick\000\000\000,iiii\000\000\000\000\000\000\252\000\000\000\002\000\000\001\245\000\004\367\006"
|
22
|
+
Datagrammer::Packet.decode(encoded).should == ['/tick', 170, 2, 421, 325382]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "handles encoded newlines correctly" do
|
26
|
+
encoded = "tick\n\000\000\000,ii\000\000\000\000\n\000\000\334\n"
|
27
|
+
Datagrammer::Packet.decode(encoded).should == ["tick\n", 10, 56330]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "encode" do
|
32
|
+
[ [['hello'], "hello\000\000\000,\000\000\000"],
|
33
|
+
[['hello','world'], "hello\000\000\000,s\000\000world\000\000\000"],
|
34
|
+
[['hello', 'world', 1, 2.0], "hello\000\000\000,sif\000\000\000\000world\000\000\000\000\000\000\001@\000\000\000"]
|
35
|
+
].each do |message, expected|
|
36
|
+
describe "message: #{message.join(', ')}" do
|
37
|
+
it "properly formats" do
|
38
|
+
Datagrammer::Packet.encode(message).should == expected
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
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
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mattly-datagrammer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew Lyon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-19 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Sends and receives UDP packets in an OSC-compatable encoded format.
|
17
|
+
email: matt@flowerpowered.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- README.mkdn
|
26
|
+
- Rakefile
|
27
|
+
- spec/datagrammer_spec.rb
|
28
|
+
- spec/packet_scanner_spec.rb
|
29
|
+
- spec/packet_spec.rb
|
30
|
+
- spec/spec_helper.rb
|
31
|
+
- lib/datagrammer
|
32
|
+
- lib/datagrammer/packet.rb
|
33
|
+
- lib/datagrammer/packet_scanner.rb
|
34
|
+
- lib/datagrammer.rb
|
35
|
+
has_rdoc: false
|
36
|
+
homepage: http://github.com/mattly/datagrammer
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.8.6
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.2.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 2
|
60
|
+
summary: UDP without the pain
|
61
|
+
test_files: []
|
62
|
+
|