coap 0.0.16 → 0.1.0
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +13 -2
- data/Gemfile +0 -1
- data/LICENSE +2 -2
- data/README.md +37 -33
- data/Rakefile +12 -3
- data/bin/coap +111 -0
- data/coap.gemspec +34 -29
- data/lib/coap.rb +3 -34
- data/lib/core.rb +11 -0
- data/lib/core/coap.rb +42 -0
- data/lib/core/coap/block.rb +98 -0
- data/lib/core/coap/client.rb +314 -0
- data/lib/core/coap/coap.rb +26 -0
- data/lib/core/coap/coding.rb +146 -0
- data/lib/core/coap/fsm.rb +82 -0
- data/lib/core/coap/message.rb +203 -0
- data/lib/core/coap/observer.rb +40 -0
- data/lib/core/coap/options.rb +44 -0
- data/lib/core/coap/registry.rb +32 -0
- data/lib/core/coap/registry/content_formats.yml +7 -0
- data/lib/core/coap/resolver.rb +17 -0
- data/lib/core/coap/transmission.rb +165 -0
- data/lib/core/coap/types.rb +69 -0
- data/lib/core/coap/utility.rb +34 -0
- data/lib/core/coap/version.rb +5 -0
- data/lib/core/core_ext/socket.rb +19 -0
- data/lib/core/hexdump.rb +18 -0
- data/lib/core/link.rb +97 -0
- data/lib/core/os.rb +15 -0
- data/spec/block_spec.rb +160 -0
- data/spec/client_spec.rb +86 -0
- data/spec/fixtures/coap.me.link +1 -0
- data/spec/link_spec.rb +98 -0
- data/spec/registry_spec.rb +39 -0
- data/spec/resolver_spec.rb +19 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/transmission_spec.rb +70 -0
- data/test/helper.rb +15 -0
- data/test/test_client.rb +99 -228
- data/test/test_message.rb +99 -71
- metadata +140 -37
- data/bin/client +0 -42
- data/lib/coap/block.rb +0 -45
- data/lib/coap/client.rb +0 -364
- data/lib/coap/coap.rb +0 -273
- data/lib/coap/message.rb +0 -187
- data/lib/coap/mysocket.rb +0 -81
- data/lib/coap/observer.rb +0 -41
- data/lib/coap/version.rb +0 -3
- data/lib/misc/hexdump.rb +0 -17
- data/test/coap_test_helper.rb +0 -2
- data/test/disabled_econotag_blck.rb +0 -33
@@ -0,0 +1,69 @@
|
|
1
|
+
module CoRE
|
2
|
+
module CoAP
|
3
|
+
module Types
|
4
|
+
# Arrays that describe a specific option type:
|
5
|
+
# [default value, length range, repeatable?, decoder, encoder]
|
6
|
+
|
7
|
+
# We only care about presence or absence, always empty.
|
8
|
+
def presence_once
|
9
|
+
[false, (0..0), false, ->(a) { true }, ->(v) { v ? [''] : []}]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Token-style, goedelized in o256.
|
13
|
+
def o256_once(min, max, default = nil)
|
14
|
+
[default, (min..max), false,
|
15
|
+
->(a) { CoAP.o256_decode(a[0]) },
|
16
|
+
->(v) { v == default ? [] : [CoAP.o256_encode(v)] }
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Unused, for now.
|
21
|
+
def o256_many(min, max)
|
22
|
+
[nil, (min..max), true,
|
23
|
+
->(a) { a.map{ |x| CoAP.o256_decode(x)} },
|
24
|
+
->(v) { Array(v).map{ |x| CoAP.o256_encode(x)} }
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
# vlb as in core-coap Annex A
|
29
|
+
def uint_once(min, max, default = nil)
|
30
|
+
[default, (min..max), false,
|
31
|
+
->(a) { CoAP.vlb_decode(a[0]) },
|
32
|
+
->(v) { v == default ? [] : [CoAP.vlb_encode(v)] }
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
def uint_many(min, max)
|
37
|
+
[nil, (min..max), true,
|
38
|
+
->(a) { a.map{ |x| CoAP.vlb_decode(x)} },
|
39
|
+
->(v) { Array(v).map{ |x| CoAP.vlb_encode(x)} }
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Any other opaque byte sequence (once).
|
44
|
+
def opaq_once(min, max, default = nil)
|
45
|
+
[default, (min..max), false,
|
46
|
+
->(a) { a[0] },
|
47
|
+
->(v) { v == default ? [] : Array(v) }
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Any other opaque byte sequence (many).
|
52
|
+
def opaq_many(min, max)
|
53
|
+
[nil, (min..max), true, ->(a) { a }, ->(v) { Array(v) }]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Same, but interpreted as UTF-8
|
57
|
+
def str_once(min, max, default = nil)
|
58
|
+
[default, (min..max), false,
|
59
|
+
->(a) { a[0] },
|
60
|
+
->(v) { v == default ? [] : Array(v) }
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
def str_many(min, max)
|
65
|
+
[nil, (min..max), true, ->(a) { a }, ->(v) { Array(v) }]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CoRE
|
2
|
+
module CoAP
|
3
|
+
module Utility
|
4
|
+
def empty_buffer
|
5
|
+
String.new # Was: ''.encode(BIN)
|
6
|
+
end
|
7
|
+
|
8
|
+
def invert_into_hash(a)
|
9
|
+
a.each_with_index.each_with_object({}) { |k, h| h[k[0]] = k[1] if k[0] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def critical?(option)
|
13
|
+
if oi = CoAP::OPTIONS_I[option] # this is really an option name symbol
|
14
|
+
option = oi[0] # -> to option number
|
15
|
+
end
|
16
|
+
option.odd?
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsafe?(option)
|
20
|
+
if oi = CoAP::OPTIONS_I[option] # this is really an option name symbol
|
21
|
+
option = oi[0] # -> to option number
|
22
|
+
end
|
23
|
+
option & 2 == 2
|
24
|
+
end
|
25
|
+
|
26
|
+
def no_cache_key?(option)
|
27
|
+
if oi = CoAP::OPTIONS_I[option] # this is really an option name symbol
|
28
|
+
option = oi[0] # -> to option number
|
29
|
+
end
|
30
|
+
option & 0x1e == 0x1c
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Socket
|
2
|
+
def self.if_nametoindex(name)
|
3
|
+
ifaddr_by_name(name).ifindex
|
4
|
+
rescue NoMethodError
|
5
|
+
0
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.if_up?(name)
|
9
|
+
ifaddr_by_name(name).flags & Socket::IFF_UP == 1
|
10
|
+
rescue NoMethodError
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def self.ifaddr_by_name(name)
|
17
|
+
Socket.getifaddrs.select { |x| x.name == name }.first
|
18
|
+
end
|
19
|
+
end
|
data/lib/core/hexdump.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# hexdump.rb
|
2
|
+
# Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
|
3
|
+
|
4
|
+
class String
|
5
|
+
def hexdump(prefix = '', prepend_newline = true)
|
6
|
+
a, i = [], 0
|
7
|
+
a << '' if prepend_newline
|
8
|
+
while i < length
|
9
|
+
slice = self.byteslice(i, 16)
|
10
|
+
a << '%s%-48s |%-16s|' %
|
11
|
+
[prefix,
|
12
|
+
slice.bytes.map { |b| '%02x' % b.ord }.join(' '),
|
13
|
+
slice.gsub(/[^ -~]/mn, ".")]
|
14
|
+
i += 16
|
15
|
+
end
|
16
|
+
a.join("\n")
|
17
|
+
end
|
18
|
+
end
|
data/lib/core/link.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CoRE
|
4
|
+
# Implements CoRE Link Format (RFC6690)
|
5
|
+
# http://tools.ietf.org/html/rfc6690
|
6
|
+
# TODO Handle repeated attributes
|
7
|
+
class Link
|
8
|
+
VALID_ATTRS = [
|
9
|
+
:anchor, :ct, :exp, :hreflang, :if, :ins, :media, :obs, :rt, :rel, :rev,
|
10
|
+
:sz, :title, :type
|
11
|
+
]
|
12
|
+
|
13
|
+
attr_accessor :uri
|
14
|
+
attr_reader :attrs
|
15
|
+
|
16
|
+
def initialize(uri, attrs = {})
|
17
|
+
@uri = uri || raise(ArgumentError.new('URI can not be unset.'))
|
18
|
+
@attrs = attrs
|
19
|
+
|
20
|
+
validate_attrs!(@attrs)
|
21
|
+
end
|
22
|
+
|
23
|
+
def merge!(hash)
|
24
|
+
a = @attrs.merge(hash)
|
25
|
+
validate_attrs!(a)
|
26
|
+
@attrs = a
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(symbol, *args)
|
30
|
+
attr = symbol.to_s.delete('=').to_sym
|
31
|
+
|
32
|
+
if !VALID_ATTRS.include?(attr)
|
33
|
+
raise ArgumentError.new("Invalid attribute «#{attr}».")
|
34
|
+
end
|
35
|
+
|
36
|
+
if symbol[-1] == '='
|
37
|
+
@attrs[attr] = args.first.to_s
|
38
|
+
else
|
39
|
+
@attrs[attr]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
s = "<#{@uri}>"
|
45
|
+
|
46
|
+
@attrs.sort.each do |attr, value|
|
47
|
+
s += ";#{attr}=\"#{value}\""
|
48
|
+
end
|
49
|
+
|
50
|
+
s
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.parse(data)
|
54
|
+
parts = data.split(';')
|
55
|
+
|
56
|
+
uri = parts.shift
|
57
|
+
uri = uri.match(/\A\<(.+)\>\z/)
|
58
|
+
|
59
|
+
if uri.nil?
|
60
|
+
raise ArgumentError.new("Invalid URI in '#{data}'.")
|
61
|
+
end
|
62
|
+
|
63
|
+
link = Link.new(uri[1])
|
64
|
+
|
65
|
+
parts.each do |part|
|
66
|
+
attr, value = part.split('=')
|
67
|
+
|
68
|
+
attr = (attr + '=').to_sym
|
69
|
+
value = value.delete('"') unless value.nil?
|
70
|
+
|
71
|
+
link.send(attr, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
link
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO Handle misplaced commas
|
78
|
+
def self.parse_multiple(data)
|
79
|
+
results = []
|
80
|
+
|
81
|
+
data.split(',').each do |part|
|
82
|
+
results << self.parse(part)
|
83
|
+
end
|
84
|
+
|
85
|
+
results
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def validate_attrs!(attrs)
|
91
|
+
if (VALID_ATTRS | attrs.keys).size > VALID_ATTRS.size
|
92
|
+
invalid = (VALID_ATTRS | attrs.keys) - VALID_ATTRS
|
93
|
+
raise ArgumentError.new("Invalid attributes: #{invalid.join(', ')}.")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/core/os.rb
ADDED
data/spec/block_spec.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
describe Block do
|
5
|
+
subject { Block.new(0, false, 16) }
|
6
|
+
let(:data1) { '+' * 42 }
|
7
|
+
let(:data2) { '+' * 32 }
|
8
|
+
|
9
|
+
describe '#chunk' do
|
10
|
+
it 'should return chunks' do
|
11
|
+
a = [0, 1, 2, 3].map do |i|
|
12
|
+
subject.num = i
|
13
|
+
subject.chunk(data1)
|
14
|
+
end
|
15
|
+
|
16
|
+
expect(a).to eq(['+' * 16, '+' * 16, '+' * 10, nil])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#chunk_count' do
|
21
|
+
it 'should return correct count' do
|
22
|
+
expect(subject.chunk_count(data1)).to eq(3)
|
23
|
+
expect(subject.chunk_count(data2)).to eq(2)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#included_by?' do
|
28
|
+
it 'should return true if body empty and index 0' do
|
29
|
+
expect(subject.included_by?('')).to be(true)
|
30
|
+
expect(subject.included_by?(nil)).to be(true)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should return false if body empty and index > 0' do
|
34
|
+
subject.num = 1
|
35
|
+
expect(subject.included_by?('')).to be(false)
|
36
|
+
expect(subject.included_by?(nil)).to be(false)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should return true if chunk index exists' do
|
40
|
+
[0, 1, 2].each do |num|
|
41
|
+
subject.num = num
|
42
|
+
expect(subject.included_by?(data1)).to be(true)
|
43
|
+
end
|
44
|
+
|
45
|
+
[0, 1].each do |num|
|
46
|
+
subject.num = num
|
47
|
+
expect(subject.included_by?(data2)).to be(true)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should return false if chunk index does not exists' do
|
52
|
+
[3, 4, 5].each do |num|
|
53
|
+
subject.num = num
|
54
|
+
expect(subject.included_by?(data1)).to be(false)
|
55
|
+
end
|
56
|
+
|
57
|
+
[2, 3].each do |num|
|
58
|
+
subject.num = num
|
59
|
+
expect(subject.included_by?(data2)).to be(false)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#last?' do
|
65
|
+
it 'should return false unless last chunk' do
|
66
|
+
[0, 1, 3].each do |num|
|
67
|
+
subject.num = num
|
68
|
+
expect(subject.last?(data1)).to be(false)
|
69
|
+
end
|
70
|
+
|
71
|
+
[0, 2, 3].each do |num|
|
72
|
+
subject.num = num
|
73
|
+
expect(subject.last?(data2)).to be(false)
|
74
|
+
end
|
75
|
+
|
76
|
+
[0, 1, 2].each do |num|
|
77
|
+
subject.num = num
|
78
|
+
expect(subject.last?('')).to be(true)
|
79
|
+
expect(subject.last?(nil)).to be(true)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should return true if last chunk' do
|
84
|
+
subject.num = 2
|
85
|
+
expect(subject.last?(data1)).to be(true)
|
86
|
+
|
87
|
+
subject.num = 1
|
88
|
+
expect(subject.last?(data2)).to be(true)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#more?' do
|
93
|
+
it 'should return true unless last chunk or bigger' do
|
94
|
+
[0, 1].each do |num|
|
95
|
+
subject.num = num
|
96
|
+
expect(subject.more?(data1)).to be(true)
|
97
|
+
end
|
98
|
+
|
99
|
+
[2, 3].each do |num|
|
100
|
+
subject.num = num
|
101
|
+
expect(subject.more?(data1)).to be(false)
|
102
|
+
end
|
103
|
+
|
104
|
+
[0].each do |num|
|
105
|
+
subject.num = num
|
106
|
+
expect(subject.more?(data2)).to be(true)
|
107
|
+
end
|
108
|
+
|
109
|
+
[1, 2].each do |num|
|
110
|
+
subject.num = num
|
111
|
+
expect(subject.more?(data2)).to be(false)
|
112
|
+
end
|
113
|
+
|
114
|
+
[0, 1, 2].each do |num|
|
115
|
+
subject.num = num
|
116
|
+
expect(subject.more?('')).to be(false)
|
117
|
+
expect(subject.more?(nil)).to be(false)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '#encode' do
|
123
|
+
it 'should work with examples' do
|
124
|
+
expect(subject.encode).to eq(0)
|
125
|
+
|
126
|
+
block = Block.new(0, true, 16)
|
127
|
+
expect(block.encode).to eq(8)
|
128
|
+
|
129
|
+
(1..6).each do |i|
|
130
|
+
block = Block.new(0, false, 2**(i+4))
|
131
|
+
expect(block.encode).to eq(i)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#encode and #decode' do
|
137
|
+
it 'should be reversible (encode -> decode)' do
|
138
|
+
num = rand(Block::MAX_NUM + 1)
|
139
|
+
more = [true, false].sample
|
140
|
+
size = Block::VALID_SIZE.sample
|
141
|
+
|
142
|
+
a = Block.new(num, more, size).encode
|
143
|
+
b = Block.new(a).decode
|
144
|
+
|
145
|
+
expect(b.num).to eq(num)
|
146
|
+
expect(b.more).to eq(more)
|
147
|
+
expect(b.size).to eq(size)
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should be reversible (decode -> encode)' do
|
151
|
+
i = 7
|
152
|
+
i = rand(2**24) until (i & 7) != 7
|
153
|
+
|
154
|
+
a = Block.new(i).decode
|
155
|
+
b = Block.new(a.num, a.more, a.size).encode
|
156
|
+
|
157
|
+
expect(b).to eq(i)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Client do
|
6
|
+
subject { Client.new }
|
7
|
+
|
8
|
+
describe '#get' do
|
9
|
+
describe 'with separated answer' do
|
10
|
+
it 'should return correct mcode and payload' do
|
11
|
+
answer = subject.get('/separate', 'coap.me')
|
12
|
+
expect(answer.mcode).to eq([2, 5])
|
13
|
+
expect(answer.payload).to eq('That took a long time')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'modifying methods' do
|
19
|
+
subject { Client.new(max_payload: 512) }
|
20
|
+
let(:payload) { Faker::Lorem.paragraphs(5).join("\n") }
|
21
|
+
let(:payload_utf8) { '♥' + payload }
|
22
|
+
|
23
|
+
describe '#post' do
|
24
|
+
describe 'with block1 option' do
|
25
|
+
describe 'creating resource' do
|
26
|
+
it 'should work with ASCII payload' do
|
27
|
+
answer = subject.post('/large-create', 'coap.me', nil, payload)
|
28
|
+
expect(answer.mcode).to eq([2, 1])
|
29
|
+
|
30
|
+
answer = subject.get('/large-create', 'coap.me')
|
31
|
+
expect(answer.mcode).to eq([2, 5])
|
32
|
+
expect(answer.payload).to eq(payload)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should work with UTF8 payload' do
|
36
|
+
answer = subject.post('/large-create', 'coap.me', nil, payload_utf8)
|
37
|
+
expect(answer.mcode).to eq([2, 1])
|
38
|
+
|
39
|
+
answer = subject.get('/large-create', 'coap.me')
|
40
|
+
expect(answer.mcode).to eq([2, 5])
|
41
|
+
expect(answer.payload.force_encoding('utf-8')).to eq(payload_utf8)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#put' do
|
48
|
+
describe 'with block1 option' do
|
49
|
+
describe 'updating resource' do
|
50
|
+
it 'should work with ASCII payload' do
|
51
|
+
answer = subject.put('/large-update', 'coap.me', nil, payload)
|
52
|
+
expect(answer.mcode).to eq([2, 4])
|
53
|
+
|
54
|
+
answer = subject.get('/large-update', 'coap.me')
|
55
|
+
expect(answer.mcode).to eq([2, 5])
|
56
|
+
expect(answer.payload).to eq(payload)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#observe' do
|
64
|
+
before do
|
65
|
+
@answers = []
|
66
|
+
|
67
|
+
@t1 = Thread.start do
|
68
|
+
subject.observe \
|
69
|
+
'/obs', 'vs0.inf.ethz.ch', nil,
|
70
|
+
->(s, m) { @answers << m }
|
71
|
+
end
|
72
|
+
|
73
|
+
Timeout.timeout(12) do
|
74
|
+
sleep 0.25 while !(@answers.size > 2)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should receive updates' do
|
79
|
+
expect(@answers.size).to be > 2
|
80
|
+
end
|
81
|
+
|
82
|
+
after do
|
83
|
+
@t1.kill
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|