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 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,5 @@
1
+ require "rake"
2
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
3
+
4
+ desc "Run the specs."
5
+ task :default => :spec
@@ -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)
@@ -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
@@ -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
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+
3
+ begin
4
+ require 'spec'
5
+ rescue LoadError
6
+ gem 'rspec'
7
+ require 'spec'
8
+ end
9
+
10
+ gem 'ruby-debug'
11
+ require 'ruby-debug'
12
+
13
+ require "#{File.dirname(__FILE__)}/../lib/datagrammer"
14
+
15
+ Debugger.start
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
+