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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +13 -2
  4. data/Gemfile +0 -1
  5. data/LICENSE +2 -2
  6. data/README.md +37 -33
  7. data/Rakefile +12 -3
  8. data/bin/coap +111 -0
  9. data/coap.gemspec +34 -29
  10. data/lib/coap.rb +3 -34
  11. data/lib/core.rb +11 -0
  12. data/lib/core/coap.rb +42 -0
  13. data/lib/core/coap/block.rb +98 -0
  14. data/lib/core/coap/client.rb +314 -0
  15. data/lib/core/coap/coap.rb +26 -0
  16. data/lib/core/coap/coding.rb +146 -0
  17. data/lib/core/coap/fsm.rb +82 -0
  18. data/lib/core/coap/message.rb +203 -0
  19. data/lib/core/coap/observer.rb +40 -0
  20. data/lib/core/coap/options.rb +44 -0
  21. data/lib/core/coap/registry.rb +32 -0
  22. data/lib/core/coap/registry/content_formats.yml +7 -0
  23. data/lib/core/coap/resolver.rb +17 -0
  24. data/lib/core/coap/transmission.rb +165 -0
  25. data/lib/core/coap/types.rb +69 -0
  26. data/lib/core/coap/utility.rb +34 -0
  27. data/lib/core/coap/version.rb +5 -0
  28. data/lib/core/core_ext/socket.rb +19 -0
  29. data/lib/core/hexdump.rb +18 -0
  30. data/lib/core/link.rb +97 -0
  31. data/lib/core/os.rb +15 -0
  32. data/spec/block_spec.rb +160 -0
  33. data/spec/client_spec.rb +86 -0
  34. data/spec/fixtures/coap.me.link +1 -0
  35. data/spec/link_spec.rb +98 -0
  36. data/spec/registry_spec.rb +39 -0
  37. data/spec/resolver_spec.rb +19 -0
  38. data/spec/spec_helper.rb +17 -0
  39. data/spec/transmission_spec.rb +70 -0
  40. data/test/helper.rb +15 -0
  41. data/test/test_client.rb +99 -228
  42. data/test/test_message.rb +99 -71
  43. metadata +140 -37
  44. data/bin/client +0 -42
  45. data/lib/coap/block.rb +0 -45
  46. data/lib/coap/client.rb +0 -364
  47. data/lib/coap/coap.rb +0 -273
  48. data/lib/coap/message.rb +0 -187
  49. data/lib/coap/mysocket.rb +0 -81
  50. data/lib/coap/observer.rb +0 -41
  51. data/lib/coap/version.rb +0 -3
  52. data/lib/misc/hexdump.rb +0 -17
  53. data/test/coap_test_helper.rb +0 -2
  54. data/test/disabled_econotag_blck.rb +0 -33
@@ -0,0 +1,82 @@
1
+ module CoRE
2
+ module CoAP
3
+ class FSM
4
+ # CoAP Message Layer FSM
5
+ # https://tools.ietf.org/html/draft-ietf-lwig-coap-01#section-2.5.2
6
+ class Message
7
+ include Celluloid::FSM
8
+
9
+ default_state :closed
10
+
11
+ # Receive CON
12
+ # Send ACK (accept) -> :closed
13
+ state :ack_pending, to: [:closed]
14
+
15
+ # Sending and receiving
16
+ # Send NON (unreliable_send)
17
+ # Receive NON
18
+ # Receive ACK
19
+ # Receive CON -> :ack_pending
20
+ # Send CON (reliable_send) -> :reliable_tx
21
+ state :closed, to: [:reliable_tx, :ack_pending]
22
+
23
+ # Send CON
24
+ # Retransmit until
25
+ # Failure
26
+ # Timeout (fail) -> :closed
27
+ # Receive matching RST (fail) -> :closed
28
+ # Cancel (cancel) -> :closed
29
+ # Success
30
+ # Receive matching ACK, NON (rx) -> :closed
31
+ # Receive matching CON (rx) -> :ack_pending
32
+ state :reliable_tx, to: [:closed, :ack_pending]
33
+ end
34
+
35
+ # CoAP Client Request/Response Layer FSM
36
+ # https://tools.ietf.org/html/draft-ietf-lwig-coap-01#section-2.5.1
37
+ class Client
38
+ include Celluloid::FSM
39
+
40
+ default_state :idle
41
+
42
+ # Idle
43
+ # Outgoing request ((un)reliable_send) -> :waiting
44
+ state :idle, to: [:waiting]
45
+
46
+ # Waiting for response
47
+ # Response received (accept, rx) -> :idle
48
+ # Failure (fail) -> :idle
49
+ # Cancel (cancel) -> :idle
50
+ state :waiting, to: [:idle]
51
+ end
52
+
53
+ # CoAP Server Request/Response Layer FSM
54
+ # https://tools.ietf.org/html/draft-ietf-lwig-coap-01#section-2.5.1
55
+ class Server
56
+ include Celluloid::FSM
57
+
58
+ default_state :idle
59
+
60
+ # Idle
61
+ # On NON (rx) -> :separate
62
+ # On CON (rx) -> :serving
63
+ state :idle, to: [:separate, :serving]
64
+
65
+ # Separate
66
+ # Respond ((un)reliable_send) -> :idle
67
+ state :separate, to: [:idle]
68
+
69
+ # Serving
70
+ # Respond (accept) -> :idle
71
+ # Empty ACK (accept) -> :separate
72
+ state :serving, to: [:idle, :separate]
73
+ end
74
+
75
+ attr_reader :fsm
76
+
77
+ def initialize
78
+ @fsm = FSM::Message.new
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,203 @@
1
+ # coapmessage.rb
2
+ # Copyright (C) 2010..2013 Carsten Bormann <cabo@tzi.org>
3
+
4
+ module CoRE
5
+ module CoAP
6
+ class Message < Struct.new(:ver, :tt, :mcode, :mid, :options, :payload)
7
+ def initialize(*args) # convenience: .new(tt?, mcode?, mid?, payload?, hash?)
8
+ if args.size < 6
9
+ h = {}
10
+ h = args.pop.dup if args.last.is_a? Hash
11
+
12
+ tt = h.delete(:tt) || args.shift
13
+
14
+ mcode = h.delete(:mcode) || args.shift
15
+
16
+ # TODO Allow later setting of mcode by Float.
17
+ case mcode
18
+ when Integer
19
+ mcode = METHODS[mcode] || [mcode >> 5, mcode & 0x1f]
20
+ when Float
21
+ mcode = [mcode.to_i, (mcode * 100 % 100).round] # Accept 2.05 and such
22
+ end
23
+
24
+ mid = h.delete(:mid) || args.shift
25
+
26
+ payload = h.delete(:payload) || args.shift || EMPTY
27
+
28
+ unless args.empty?
29
+ raise 'CoRE::CoAP::Message.new: Either specify Hash or all arguments.'
30
+ end
31
+
32
+ super(1, tt, mcode, mid, h, payload)
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def klausify(n)
39
+ if n < 13
40
+ [n, '']
41
+ else
42
+ n -= 13
43
+ if n < 256
44
+ [13, [n].pack("C")]
45
+ else
46
+ [14, [n-256].pack("n")]
47
+ end
48
+ end
49
+ end
50
+
51
+ def mcode_readable
52
+ return "#{mcode[0]}.#{"%02d" % mcode[1]}" if mcode.is_a? Array
53
+ mcode.to_s
54
+ end
55
+
56
+ def prepare_options
57
+ prepared_options = {}
58
+ options.each do |k, v|
59
+ if oinfo_i = CoAP::OPTIONS_I[k]
60
+ onum, oname, defv, minmax, rep, _, encoder = *oinfo_i
61
+ prepared_options[onum] = a = encoder.call(v)
62
+ rep or a.size <= 1 or raise "repeated option #{oname} #{a.inspect}"
63
+ a.each do |v1|
64
+ unless minmax === v1.bytesize
65
+ raise ArgumentError, "#{v1.inspect} out of #{minmax} for #{oname}"
66
+ end
67
+ end
68
+ else
69
+ raise ArgumentError, "#{k.inspect}: unknown option" unless Integer === k
70
+ prepared_options[k] = Array(v) # store raw option
71
+ end
72
+ end
73
+ prepared_options
74
+ end
75
+
76
+ def to_s
77
+ path = CoAP.path_encode(self.options[:uri_path])
78
+ query = CoAP.query_encode(self.options[:uri_query])
79
+
80
+ [mcode_readable, path, query].join(' ')
81
+ end
82
+
83
+ def to_wire
84
+ # check and encode option values
85
+ prepared_options = prepare_options
86
+ # puts "prepared_options: #{prepared_options}"
87
+
88
+ token = (prepared_options.delete(CoAP::TOKEN_ON) || [nil])[0] || ''
89
+ puts "TOKEN: #{token.inspect}" unless token
90
+
91
+ b1 = 0x40 | TTYPES_I[tt] << 4 | token.bytesize
92
+ b2 = METHODS_I[mcode] || (mcode[0] << 5) + mcode[1]
93
+ result = [b1, b2, mid].pack("CCn")
94
+ result << token
95
+
96
+ # stuff options in packet
97
+ onumber = 0
98
+ num_encoded_options = 0
99
+ prepared_options.keys.sort.each do |k|
100
+ raise "Bad Option Type #{k.inspect}" unless Integer === k && k >= 0
101
+ a = prepared_options[k]
102
+ a.each do |v|
103
+ # result << frob(k, v)
104
+ odelta = k - onumber
105
+ onumber = k
106
+
107
+ odelta1, odelta2 = klausify(odelta)
108
+ odelta1 <<= 4
109
+
110
+ length1, length2 = klausify(v.bytesize)
111
+ result << [odelta1 | length1].pack("C")
112
+ result << odelta2
113
+ result << length2
114
+ result << v.dup.force_encoding(CoAP::BIN) # value
115
+ end
116
+ end
117
+
118
+ if payload != '' && !payload.nil?
119
+ result << 0xFF
120
+ result << payload.dup.force_encoding(CoAP::BIN)
121
+ end
122
+
123
+ result
124
+ end
125
+
126
+ def self.decode_options_and_put_together(b1, tt, mcode, mid, options, payload)
127
+ # check and decode option values
128
+ decoded_options = CoAP::DEFAULTING_OPTIONS.dup
129
+ options.each_pair do |k, v|
130
+ if oinfo = CoAP::OPTIONS[k]
131
+ oname, _, minmax, repeatable, decoder, _ = *oinfo
132
+ repeatable or v.size <= 1 or
133
+ raise ArgumentError, "repeated unrepeatable option #{oname}"
134
+ v.each do |v1|
135
+ unless minmax === v1.bytesize
136
+ raise ArgumentError, "#{v1.inspect} out of #{minmax} for #{oname}"
137
+ end
138
+ end
139
+ decoded_options[oname] = decoder.call(v)
140
+ else
141
+ decoded_options[k] = v # we don't know what that is -- keep it in raw
142
+ end
143
+ end
144
+
145
+ new(b1, tt, mcode, mid, Hash[decoded_options], payload) # XXX: why Hash[] again?
146
+ end
147
+
148
+ def self.deklausify(d, dpos, len)
149
+ case len
150
+ when 0..12
151
+ [len, dpos]
152
+ when 13
153
+ [d.getbyte(dpos) + 13, dpos += 1]
154
+ when 14
155
+ [d.byteslice(dpos, 2).unpack("n")[0] + 269, dpos += 2]
156
+ else
157
+ raise "[#{d.inspect}] Bad delta/length nibble #{len} at #{dpos}"
158
+ end
159
+ end
160
+
161
+ def self.parse(d)
162
+ # dpos keeps our current position in parsing d
163
+ b1, mcode, mid = d.unpack("CCn"); dpos = 4
164
+ toklen = b1 & 0xf
165
+ token = d.byteslice(dpos, toklen); dpos += toklen
166
+ b1 >>= 4
167
+ tt = TTYPES[b1 & 0x3]
168
+ b1 >>= 2
169
+ raise ArgumentError, "unknown CoAP version #{b1}" unless b1 == 1
170
+ mcode = METHODS[mcode] || [mcode>>5, mcode&0x1F]
171
+
172
+ # collect options
173
+ onumber = 0 # current option number
174
+ options = Hash.new { |h, k| h[k] = [] }
175
+ dlen = d.bytesize
176
+ while dpos < dlen
177
+ tl1 = d.getbyte(dpos); dpos += 1
178
+ raise ArgumentError, "option is not there at #{dpos} with oc #{orig_numopt}" unless tl1 # XXX
179
+
180
+ break if tl1 == 0xff
181
+
182
+ odelta, dpos = deklausify(d, dpos, tl1 >> 4)
183
+ olen, dpos = deklausify(d, dpos, tl1 & 0xF)
184
+
185
+ onumber += odelta
186
+
187
+ if dpos + olen > dlen
188
+ raise ArgumentError, "#{olen}-byte option at #{dpos} -- not enough data in #{dlen} total"
189
+ end
190
+
191
+ oval = d.byteslice(dpos, olen); dpos += olen
192
+ options[onumber] << oval
193
+ end
194
+
195
+ options[CoAP::TOKEN_ON] = [token] if token != ''
196
+
197
+ # d.bytesize = more than all the rest...
198
+ decode_options_and_put_together(b1, tt, mcode, mid, options,
199
+ d.byteslice(dpos, d.bytesize))
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,40 @@
1
+ module CoRE
2
+ module CoAP
3
+ class Observer
4
+ MAX_OBSERVE_OPTION_VALUE = 8_388_608
5
+
6
+ def initialize
7
+ @logger = CoAP.logger
8
+ end
9
+
10
+ def observe(message, callback, socket)
11
+ n = message.options[:observe]
12
+
13
+ callback.call(socket, message)
14
+
15
+ # This does not seem to be able to cope with concurrency.
16
+ loop do
17
+ answer = socket.receive(timeout: 0)
18
+
19
+ next unless answer.options[:observe]
20
+
21
+ if update?(n, answer.options[:observe])
22
+ callback.call(socket, answer)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def update?(old, new)
30
+ if new > old
31
+ new - old < MAX_OBSERVE_OPTION_VALUE
32
+ elsif new < old
33
+ old - new > MAX_OBSERVE_OPTION_VALUE
34
+ else
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module CoRE
2
+ module CoAP
3
+ module Options
4
+ extend Types
5
+
6
+ TOKEN_ON = 19
7
+
8
+ # 14 => :user, default, length range, replicable?, decoder, encoder
9
+ OPTIONS = { # name minlength, maxlength, [default] defined where:
10
+ 1 => [:if_match, *o256_many(0, 8)], # core-coap-12
11
+ 3 => [:uri_host, *str_once(1, 255)], # core-coap-12
12
+ 4 => [:etag, *o256_many(1, 8)], # core-coap-12 !! once in rp
13
+ 5 => [:if_none_match, *presence_once], # core-coap-12
14
+ 6 => [:observe, *uint_once(0, 3)], # core-observe-07
15
+ 7 => [:uri_port, *uint_once(0, 2)], # core-coap-12
16
+ 8 => [:location_path, *str_many(0, 255)], # core-coap-12
17
+ 11 => [:uri_path, *str_many(0, 255)], # core-coap-12
18
+ 12 => [:content_format, *uint_once(0, 2)], # core-coap-12
19
+ 14 => [:max_age, *uint_once(0, 4, 60)], # core-coap-12
20
+ 15 => [:uri_query, *str_many(0, 255)], # core-coap-12
21
+ 17 => [:accept, *uint_once(0, 2)], # core-coap-18!
22
+ TOKEN_ON => [:token, *o256_once(1, 8, 0)], # core-coap-12 -> opaq_once(1, 8, EMPTY)
23
+ 20 => [:location_query, *str_many(0, 255)], # core-coap-12
24
+ 23 => [:block2, *uint_once(0, 3)], # core-block-10
25
+ 27 => [:block1, *uint_once(0, 3)], # core-block-10
26
+ 28 => [:size2, *uint_once(0, 4)], # core-block-10
27
+ 35 => [:proxy_uri, *str_once(1, 1034)], # core-coap-12
28
+ 39 => [:proxy_scheme, *str_once(1, 255)], # core-coap-13
29
+ 60 => [:size1, *uint_once(0, 4)], # core-block-10
30
+ }
31
+
32
+ # :user => 14, :user, def, range, rep, deco, enco
33
+ OPTIONS_I =
34
+ Hash[OPTIONS.map { |k, v| [v[0], [k, *v]] }]
35
+
36
+ DEFAULTING_OPTIONS =
37
+ Hash[
38
+ OPTIONS
39
+ .map { |k, v| [v[0].freeze, v[1].freeze] }
40
+ .select { |k, v| v }
41
+ ].freeze
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ module CoRE
2
+ module CoAP
3
+ module Registry
4
+ REGISTRY_PATH = File.join(File.dirname(__FILE__), 'registry').freeze
5
+
6
+ def self.load_yaml(registry)
7
+ registry = "#{registry}.yml"
8
+ YAML.load_file(File.join(REGISTRY_PATH, registry))
9
+ end
10
+
11
+ CONTENT_FORMATS = load_yaml(:content_formats).freeze
12
+ CONTENT_FORMATS_INVERTED = CONTENT_FORMATS.invert.freeze
13
+
14
+ def self.convert_content_format(cf)
15
+ if cf.is_a? String
16
+ CONTENT_FORMATS_INVERTED[cf] || without_charset(cf)
17
+ else
18
+ CONTENT_FORMATS[cf]
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def self.without_charset(cf)
25
+ cf = cf.split(';').first
26
+ CONTENT_FORMATS_INVERTED.select { |k| k =~ /^#{cf}/ }.first[1]
27
+ rescue NoMethodError
28
+ nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ 0: 'text/plain; charset=utf-8'
2
+ 40: 'application/link-format'
3
+ 41: 'application/xml'
4
+ 42: 'application/octet-stream'
5
+ 47: 'application/exi'
6
+ 50: 'application/json'
7
+ 60: 'application/cbor'
@@ -0,0 +1,17 @@
1
+ module CoRE
2
+ module CoAP
3
+ class Resolver
4
+ def self.address(host)
5
+ a = if ENV['IPv4'].nil?
6
+ IPv6FavorResolv.getaddress(host).to_s
7
+ else
8
+ Resolv.getaddress(host).to_s
9
+ end
10
+
11
+ raise Resolv::ResolvError if a.empty?
12
+
13
+ a
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,165 @@
1
+ module CoRE
2
+ module CoAP
3
+ # Socket abstraction.
4
+ class Transmission
5
+ DEFAULT_RECV_TIMEOUT = 2
6
+
7
+ attr_accessor :max_retransmit, :recv_timeout
8
+ attr_reader :address_family, :socket
9
+
10
+ def initialize(options = {})
11
+ @max_retransmit = options[:max_retransmit] || 4
12
+ @recv_timeout = options[:recv_timeout] || DEFAULT_RECV_TIMEOUT
13
+ @socket = options[:socket]
14
+
15
+ @retransmit = if options[:retransmit].nil?
16
+ true
17
+ else
18
+ !!options[:retransmit]
19
+ end
20
+
21
+ if @socket
22
+ @socket_class = @socket.class
23
+ @address_family = @socket.addr.first
24
+ else
25
+ @socket_class = options[:socket_class] || Celluloid::IO::UDPSocket
26
+ @address_family = options[:address_family] || Socket::AF_INET6
27
+ @socket = @socket_class.new(@address_family)
28
+ end
29
+
30
+ # http://lists.apple.com/archives/darwin-kernel/2014/Mar/msg00012.html
31
+ if OS.osx? && ipv6?
32
+ ifname = Socket.if_up?('en1') ? 'en1' : 'en0'
33
+ ifindex = Socket.if_nametoindex(ifname)
34
+
35
+ s = @socket.to_io rescue @socket
36
+ s.setsockopt(:IPPROTO_IPV6, :IPV6_MULTICAST_IF, [ifindex].pack('i_'))
37
+ end
38
+
39
+ @socket
40
+ end
41
+
42
+ def ipv6?
43
+ @address_family == Socket::AF_INET6
44
+ end
45
+
46
+ # Receive from socket and return parsed CoAP message. (ACK is sent on CON
47
+ # messages.)
48
+ def receive(options = {})
49
+ retry_count = options[:retry_count] || 0
50
+ timeout = (options[:timeout] || @recv_timeout) ** (retry_count + 1)
51
+
52
+ mid = options[:mid]
53
+ flags = mid.nil? ? 0 : Socket::MSG_PEEK
54
+
55
+ data = Timeout.timeout(timeout) do
56
+ @socket.recvfrom(1152, flags)
57
+ end
58
+
59
+ answer = CoAP.parse(data[0].force_encoding('BINARY'))
60
+
61
+ if mid == answer.mid
62
+ Timeout.timeout(1) { @socket.recvfrom(1152) }
63
+ end
64
+
65
+ return if answer.nil?
66
+
67
+ if answer.tt == :con
68
+ message = Message.new(:ack, 0, answer.mid, nil,
69
+ {token: answer.options[:token]})
70
+
71
+ send(message, data[1][3])
72
+ end
73
+
74
+ answer
75
+ end
76
+
77
+ # Send +message+ (retransmit if necessary) and wait for answer. Returns
78
+ # answer.
79
+ def request(message, host, port = CoAP::PORT)
80
+ retry_count = 0
81
+ retransmit = @retransmit && message.tt == :con
82
+
83
+ begin
84
+ send(message, host, port)
85
+ response = receive(retry_count: retry_count, mid: message.mid)
86
+ rescue Timeout::Error
87
+ raise unless retransmit
88
+
89
+ retry_count += 1
90
+
91
+ if retry_count > @max_retransmit
92
+ raise "Maximum retransmission count of #{@max_retransmit} reached."
93
+ end
94
+
95
+ retry unless message.tt == :non
96
+ end
97
+
98
+ if seperate?(response)
99
+ response = receive(timeout: 10, mid: message.mid)
100
+ end
101
+
102
+ response
103
+ end
104
+
105
+ # Send +message+.
106
+ def send(message, host, port = CoAP::PORT)
107
+ message = message.to_wire if message.respond_to?(:to_wire)
108
+
109
+ # In MRI and Rubinius, the Socket::MSG_DONTWAIT option is 64.
110
+ # It is not defined by JRuby.
111
+ # TODO Is it really necessary?
112
+ @socket.send(message, 64, host, port)
113
+ end
114
+
115
+ private
116
+
117
+ # Check if answer is seperated.
118
+ def seperate?(response)
119
+ r = response
120
+ r.tt == :ack && r.payload.empty? && r.mcode == [0, 0]
121
+ end
122
+
123
+ class << self
124
+ # Return Transmission instance with socket matching address family.
125
+ def from_host(host, options = {})
126
+ if IPAddr.new(host).ipv6?
127
+ new(options)
128
+ else
129
+ new(options.merge(address_family: Socket::AF_INET))
130
+ end
131
+ # MRI throws IPAddr::InvalidAddressError, JRuby an ArgumentError
132
+ rescue ArgumentError
133
+ host = Resolver.address(host)
134
+ retry
135
+ end
136
+
137
+ # Instanciate matching Transmission and send message.
138
+ def send(*args)
139
+ invoke(:send, *args)
140
+ end
141
+
142
+ # Instanciate matching Transmission and perform request.
143
+ def request(*args)
144
+ invoke(:request, *args)
145
+ end
146
+
147
+ private
148
+
149
+ # Instanciate matching Transmission and invoke +method+ on instance.
150
+ def invoke(method, *args)
151
+ options = {}
152
+ options = args.pop if args.last.is_a? Hash
153
+
154
+ if options[:socket]
155
+ transmission = Transmission.new(options)
156
+ else
157
+ transmission = from_host(args[1], options)
158
+ end
159
+
160
+ [transmission, transmission.__send__(method, *args)]
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end