UPnP 1.0.0 → 1.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.
- data.tar.gz.sig +0 -0
- data/.autotest +9 -0
- data/History.txt +8 -0
- data/Manifest.txt +8 -0
- data/README.txt +18 -7
- data/Rakefile +3 -0
- data/lib/UPnP.rb +1 -1
- data/lib/UPnP/SSDP.rb +288 -25
- data/lib/UPnP/UUID.rb +187 -0
- data/lib/UPnP/control/service.rb +4 -1
- data/lib/UPnP/device.rb +692 -0
- data/lib/UPnP/root_server.rb +143 -0
- data/lib/UPnP/service.rb +376 -0
- data/test/test_UPnP_SSDP.rb +80 -17
- data/test/test_UPnP_SSDP_response.rb +2 -0
- data/test/test_UPnP_SSDP_search.rb +25 -0
- data/test/test_UPnP_control_device.rb +2 -0
- data/test/test_UPnP_control_service.rb +2 -0
- data/test/test_UPnP_device.rb +295 -0
- data/test/test_UPnP_root_server.rb +99 -0
- data/test/test_UPnP_service.rb +171 -0
- data/test/utilities.rb +61 -14
- metadata +36 -4
- metadata.gz.sig +0 -0
data/test/test_UPnP_SSDP.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require 'test/unit'
|
2
2
|
require 'test/utilities'
|
3
3
|
require 'UPnP/SSDP'
|
4
|
+
require 'UPnP/device'
|
4
5
|
|
5
6
|
class TestUPnPSSDP < UPnP::TestCase
|
6
7
|
|
7
8
|
def setup
|
9
|
+
super
|
10
|
+
|
8
11
|
@ssdp = UPnP::SSDP.new
|
9
12
|
@ssdp.timeout = 0
|
10
13
|
end
|
@@ -20,15 +23,6 @@ class TestUPnPSSDP < UPnP::TestCase
|
|
20
23
|
notifications = @ssdp.discover
|
21
24
|
|
22
25
|
assert_equal [], socket.sent
|
23
|
-
assert_equal [Socket::INADDR_ANY, @ssdp.port], socket.bound
|
24
|
-
|
25
|
-
expected = [
|
26
|
-
[Socket::IPPROTO_IP, Socket::IP_TTL, [@ssdp.ttl].pack('i')],
|
27
|
-
[Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP,
|
28
|
-
"\357\377\377\372\000\000\000\000"]
|
29
|
-
]
|
30
|
-
|
31
|
-
assert_equal expected, socket.socket_options
|
32
26
|
|
33
27
|
assert_equal 1, notifications.length
|
34
28
|
assert_equal 'upnp:rootdevice', notifications.first.type
|
@@ -44,6 +38,24 @@ class TestUPnPSSDP < UPnP::TestCase
|
|
44
38
|
assert_equal 'upnp:rootdevice', notification.type
|
45
39
|
end
|
46
40
|
|
41
|
+
def test_new_socket
|
42
|
+
UPnP::SSDP.send :const_set, :UDPSocket, UPnP::FakeSocket
|
43
|
+
|
44
|
+
socket = @ssdp.new_socket
|
45
|
+
|
46
|
+
ttl = [@ssdp.ttl].pack 'i'
|
47
|
+
expected = [
|
48
|
+
[Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, "\357\377\377\372\000\000\000\000"],
|
49
|
+
[Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, "\000"],
|
50
|
+
[Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, ttl],
|
51
|
+
[Socket::IPPROTO_IP, Socket::IP_TTL, ttl],
|
52
|
+
]
|
53
|
+
|
54
|
+
assert_equal expected, socket.socket_options
|
55
|
+
ensure
|
56
|
+
UPnP::SSDP.send :remove_const, :UDPSocket
|
57
|
+
end
|
58
|
+
|
47
59
|
def test_parse_bad
|
48
60
|
assert_raise UPnP::SSDP::Error do
|
49
61
|
@ssdp.parse ''
|
@@ -62,6 +74,12 @@ class TestUPnPSSDP < UPnP::TestCase
|
|
62
74
|
assert_equal 'upnp:rootdevice', notification.type
|
63
75
|
end
|
64
76
|
|
77
|
+
def test_parse_search
|
78
|
+
response = @ssdp.parse util_search
|
79
|
+
|
80
|
+
assert_equal 'upnp:rootdevice', response.target
|
81
|
+
end
|
82
|
+
|
65
83
|
def test_parse_search_response
|
66
84
|
response = @ssdp.parse util_search_response
|
67
85
|
|
@@ -74,8 +92,6 @@ class TestUPnPSSDP < UPnP::TestCase
|
|
74
92
|
|
75
93
|
responses = @ssdp.search
|
76
94
|
|
77
|
-
assert_equal nil, socket.bound
|
78
|
-
|
79
95
|
m_search = <<-M_SEARCH
|
80
96
|
M-SEARCH * HTTP/1.1\r
|
81
97
|
HOST: 239.255.255.250:1900\r
|
@@ -87,12 +103,6 @@ ST: ssdp:all\r
|
|
87
103
|
|
88
104
|
assert_equal [[m_search, 0, @ssdp.broadcast, @ssdp.port]], socket.sent
|
89
105
|
|
90
|
-
expected = [
|
91
|
-
[Socket::IPPROTO_IP, Socket::IP_TTL, [@ssdp.ttl].pack('i')],
|
92
|
-
]
|
93
|
-
|
94
|
-
assert_equal expected, socket.socket_options
|
95
|
-
|
96
106
|
assert_equal 1, responses.length
|
97
107
|
assert_equal 'upnp:rootdevice', responses.first.target
|
98
108
|
end
|
@@ -205,6 +215,55 @@ ST: uuid:foo\r
|
|
205
215
|
assert_equal [[m_search, 0, @ssdp.broadcast, @ssdp.port]], socket.sent
|
206
216
|
end
|
207
217
|
|
218
|
+
def test_send_notify
|
219
|
+
socket = UPnP::FakeSocket.new
|
220
|
+
@ssdp.socket = socket
|
221
|
+
|
222
|
+
uri = 'http://127.255.255.255:65536/description'
|
223
|
+
device = UPnP::Device.new 'TestDevice', 'test device'
|
224
|
+
|
225
|
+
@ssdp.send_notify uri, 'upnp:rootdevice', device
|
226
|
+
|
227
|
+
search = <<-SEARCH
|
228
|
+
NOTIFY * HTTP/1.1\r
|
229
|
+
HOST: 239.255.255.250:1900\r
|
230
|
+
CACHE-CONTROL: max-age=120\r
|
231
|
+
LOCATION: #{uri}\r
|
232
|
+
NT: upnp:rootdevice\r
|
233
|
+
NTS: ssdp:alive\r
|
234
|
+
SERVER: Ruby UPnP/#{UPnP::VERSION} UPnP/1.0 #{util_device_version}\r
|
235
|
+
USN: #{device.name}::upnp:rootdevice\r
|
236
|
+
\r
|
237
|
+
SEARCH
|
238
|
+
|
239
|
+
assert_equal [[search, 0, @ssdp.broadcast, @ssdp.port]], socket.sent
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_send_response
|
243
|
+
socket = UPnP::FakeSocket.new
|
244
|
+
@ssdp.socket = socket
|
245
|
+
|
246
|
+
uri = 'http://127.255.255.255:65536/description'
|
247
|
+
device = UPnP::Device.new 'TestDevice', 'test device'
|
248
|
+
|
249
|
+
@ssdp.send_response uri, 'upnp:rootdevice', device.name, device
|
250
|
+
|
251
|
+
search = <<-SEARCH
|
252
|
+
HTTP/1.1 200 OK\r
|
253
|
+
CACHE-CONTROL: max-age=120\r
|
254
|
+
EXT:\r
|
255
|
+
LOCATION: #{uri}\r
|
256
|
+
SERVER: Ruby UPnP/#{UPnP::VERSION} UPnP/1.0 #{util_device_version}\r
|
257
|
+
ST: upnp:rootdevice\r
|
258
|
+
NTS: ssdp:alive\r
|
259
|
+
USN: #{device.name}\r
|
260
|
+
Content-Length: 0\r
|
261
|
+
\r
|
262
|
+
SEARCH
|
263
|
+
|
264
|
+
assert_equal [[search, 0, @ssdp.broadcast, @ssdp.port]], socket.sent
|
265
|
+
end
|
266
|
+
|
208
267
|
def test_send_search
|
209
268
|
socket = UPnP::FakeSocket.new
|
210
269
|
@ssdp.socket = socket
|
@@ -233,5 +292,9 @@ ST: bunnies\r
|
|
233
292
|
assert_equal nil, @ssdp.listener
|
234
293
|
end
|
235
294
|
|
295
|
+
def util_device_version
|
296
|
+
"UPnP::Device::TestDevice/#{UPnP::VERSION}"
|
297
|
+
end
|
298
|
+
|
236
299
|
end
|
237
300
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test/utilities'
|
3
|
+
require 'UPnP/SSDP'
|
4
|
+
|
5
|
+
class TestUPnPSSDPSearch < UPnP::TestCase
|
6
|
+
|
7
|
+
def test_self_parse_search
|
8
|
+
search = UPnP::SSDP::Search.parse util_search
|
9
|
+
|
10
|
+
assert_equal Time, search.date.class
|
11
|
+
assert_equal 'upnp:rootdevice', search.target
|
12
|
+
assert_equal 2, search.wait_time
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_inspect
|
16
|
+
search = UPnP::SSDP::Search.parse util_search
|
17
|
+
|
18
|
+
id = search.object_id.to_s 16
|
19
|
+
expected = "#<UPnP::SSDP::Search:0x#{id} upnp:rootdevice>"
|
20
|
+
|
21
|
+
assert_equal expected, search.inspect
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,295 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'test/utilities'
|
3
|
+
require 'UPnP/device'
|
4
|
+
|
5
|
+
class TestUPnPDevice < UPnP::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
super
|
9
|
+
|
10
|
+
@device = UPnP::Device.new 'TestDevice', 'test device'
|
11
|
+
@device.manufacturer = 'UPnP Manufacturer'
|
12
|
+
@device.model_name = 'UPnP Model'
|
13
|
+
|
14
|
+
@sub_device = @device.add_device 'TestDevice', 'test sub-device'
|
15
|
+
@sub_device.manufacturer = 'UPnP Sub Manufacturer'
|
16
|
+
@sub_device.model_name = 'UPnP Sub Model'
|
17
|
+
|
18
|
+
@service = @device.add_service 'TestService'
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_self_create
|
22
|
+
device1 = UPnP::Device.create 'TestDevice', 'test device'
|
23
|
+
|
24
|
+
dump = File.join @home, '.UPnP', 'TestDevice', 'test device'
|
25
|
+
|
26
|
+
assert File.exist?(dump)
|
27
|
+
|
28
|
+
device2 = UPnP::Device.create 'TestDevice', 'test device'
|
29
|
+
|
30
|
+
assert_equal device1.name, device2.name, 'UUIDs not identical'
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_self_create_edit
|
34
|
+
device1 = UPnP::Device.create 'TestDevice', 'test device' do |d|
|
35
|
+
d.manufacturer = 'manufacturer 1'
|
36
|
+
|
37
|
+
d.add_device 'TestDevice', 'embedded device' do |d2|
|
38
|
+
d2.manufacturer = 'embedded manufacturer'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
device2 = UPnP::Device.create 'TestDevice', 'test device' do |d|
|
43
|
+
d.manufacturer = 'manufacturer 2'
|
44
|
+
|
45
|
+
d.add_device 'TestDevice', 'embedded device' do |d2|
|
46
|
+
d2.manufacturer = 'embedded manufacturer 2'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
assert_equal device1.name, device2.name, 'UUIDs not identical'
|
51
|
+
|
52
|
+
assert_equal 2, device2.devices.length, 'wrong number of devices'
|
53
|
+
|
54
|
+
assert_equal device1.devices.last.name,
|
55
|
+
device2.devices.last.name, 'sub-device UUIDs not identical'
|
56
|
+
|
57
|
+
assert_not_equal device2.name, device2.devices.last.name
|
58
|
+
|
59
|
+
assert_equal 'manufacturer 2', device2.manufacturer, 'block not called'
|
60
|
+
assert_equal 'embedded manufacturer 2',
|
61
|
+
device2.devices.last.manufacturer,
|
62
|
+
'sub-device block not called'
|
63
|
+
|
64
|
+
device3 = UPnP::Device.create 'TestDevice', 'test device'
|
65
|
+
|
66
|
+
assert_equal 'manufacturer 2', device3.manufacturer,
|
67
|
+
'not dumped from Marshal'
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_initialize
|
71
|
+
assert_kind_of UPnP::Device::TestDevice, @device
|
72
|
+
assert_equal 'test device', @device.friendly_name
|
73
|
+
assert_match %r%\Auuid:.{36}\Z%, @device.name
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_add_device
|
77
|
+
assert_equal @device, @sub_device.parent
|
78
|
+
assert_equal 'test sub-device', @sub_device.friendly_name
|
79
|
+
assert_equal 'TestDevice', @sub_device.type
|
80
|
+
|
81
|
+
assert @device.sub_devices.include?(@sub_device)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_add_device_exists
|
85
|
+
device = @device.add_device @sub_device.type,
|
86
|
+
@sub_device.friendly_name do |d|
|
87
|
+
d.manufacturer = 'new manufacturer'
|
88
|
+
end
|
89
|
+
|
90
|
+
assert_equal 2, @device.devices.length, 'wrong number of devices'
|
91
|
+
|
92
|
+
assert_equal 'new manufacturer', device.manufacturer
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_add_service
|
96
|
+
assert_equal @device, @service.device
|
97
|
+
|
98
|
+
assert @device.sub_services.include?(@service)
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_description
|
102
|
+
desc = @device.description
|
103
|
+
|
104
|
+
desc = desc.gsub(/uuid:.{8}-.{4}-.{4}-.{4}-.{12}/,
|
105
|
+
'uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX')
|
106
|
+
|
107
|
+
expected = <<-XML
|
108
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
109
|
+
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
110
|
+
<specVersion>
|
111
|
+
<major>1</major>
|
112
|
+
<minor>0</minor>
|
113
|
+
</specVersion>
|
114
|
+
<device>
|
115
|
+
<deviceType>urn:schemas-upnp-org:device:TestDevice:1</deviceType>
|
116
|
+
<UDN>uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</UDN>
|
117
|
+
<friendlyName>test device</friendlyName>
|
118
|
+
<manufacturer>UPnP Manufacturer</manufacturer>
|
119
|
+
<modelName>UPnP Model</modelName>
|
120
|
+
<serviceList>
|
121
|
+
<service>
|
122
|
+
<serviceType>urn:schemas-upnp-org:service:TestService:1</serviceType>
|
123
|
+
<serviceId>urn:upnp-org:serviceId:TestService</serviceId>
|
124
|
+
<SCPDURL>/TestDevice/TestService</SCPDURL>
|
125
|
+
<controlURL>/TestDevice/TestService/control</controlURL>
|
126
|
+
<eventSubURL>/TestDevice/TestService/event_sub</eventSubURL>
|
127
|
+
</service>
|
128
|
+
</serviceList>
|
129
|
+
<deviceList>
|
130
|
+
<device>
|
131
|
+
<deviceType>urn:schemas-upnp-org:device:TestDevice:1</deviceType>
|
132
|
+
<UDN>uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</UDN>
|
133
|
+
<friendlyName>test sub-device</friendlyName>
|
134
|
+
<manufacturer>UPnP Sub Manufacturer</manufacturer>
|
135
|
+
<modelName>UPnP Sub Model</modelName>
|
136
|
+
</device>
|
137
|
+
</deviceList>
|
138
|
+
</device>
|
139
|
+
</root>
|
140
|
+
XML
|
141
|
+
|
142
|
+
assert_equal expected, desc
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_device_description
|
146
|
+
desc = ''
|
147
|
+
xml = Builder::XmlMarkup.new :indent => 2, :target => desc
|
148
|
+
|
149
|
+
@sub_device.device_description xml
|
150
|
+
|
151
|
+
desc = desc.gsub(/uuid:.{8}-.{4}-.{4}-.{4}-.{12}/,
|
152
|
+
'uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX')
|
153
|
+
|
154
|
+
expected = <<-XML
|
155
|
+
<device>
|
156
|
+
<deviceType>urn:schemas-upnp-org:device:TestDevice:1</deviceType>
|
157
|
+
<UDN>uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX</UDN>
|
158
|
+
<friendlyName>test sub-device</friendlyName>
|
159
|
+
<manufacturer>UPnP Sub Manufacturer</manufacturer>
|
160
|
+
<modelName>UPnP Sub Model</modelName>
|
161
|
+
</device>
|
162
|
+
XML
|
163
|
+
|
164
|
+
assert_equal expected, desc
|
165
|
+
end
|
166
|
+
|
167
|
+
def test_devices
|
168
|
+
assert_equal ['test device', 'test sub-device'],
|
169
|
+
@device.devices.map { |d| d.friendly_name }, 'device'
|
170
|
+
assert_equal ['test sub-device'],
|
171
|
+
@sub_device.devices.map { |d| d.friendly_name }, 'sub-device'
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_dump
|
175
|
+
@device.dump
|
176
|
+
|
177
|
+
dump_file = File.join @home, '.UPnP', @device.type, @device.friendly_name
|
178
|
+
|
179
|
+
assert File.exist?(dump_file)
|
180
|
+
|
181
|
+
marshal_version = IO.read dump_file, 2
|
182
|
+
|
183
|
+
major, minor = marshal_version.unpack 'CC'
|
184
|
+
|
185
|
+
assert_equal Marshal::MAJOR_VERSION, major
|
186
|
+
assert_equal Marshal::MINOR_VERSION, minor
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_marshal_dump
|
190
|
+
dump = @device.marshal_dump
|
191
|
+
assert_equal 'TestDevice', dump.shift, 'type'
|
192
|
+
assert_equal 'test device', dump.shift, 'friendly name'
|
193
|
+
assert_equal [@sub_device], dump.shift, 'sub-devices'
|
194
|
+
assert_equal [@service], dump.shift, 'sub-services'
|
195
|
+
assert_equal nil, dump.shift, 'parent'
|
196
|
+
assert_match %r%uuid:.{36}%, dump.shift, 'name'
|
197
|
+
assert_equal 'UPnP Manufacturer', dump.shift, 'manufacturer'
|
198
|
+
assert_equal nil, dump.shift, 'manufacturer url'
|
199
|
+
assert_equal nil, dump.shift, 'model description'
|
200
|
+
assert_equal 'UPnP Model', dump.shift, 'model name'
|
201
|
+
assert_equal nil, dump.shift, 'model number'
|
202
|
+
assert_equal nil, dump.shift, 'model url'
|
203
|
+
assert_equal nil, dump.shift, 'serial number'
|
204
|
+
assert_equal nil, dump.shift, 'upc'
|
205
|
+
|
206
|
+
assert dump.empty?, 'device not empty'
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_marshal_load
|
210
|
+
data = (1..14).to_a
|
211
|
+
|
212
|
+
@device.marshal_load data
|
213
|
+
|
214
|
+
assert_equal 1, @device.type
|
215
|
+
assert_equal 2, @device.friendly_name
|
216
|
+
assert_equal 3, @device.sub_devices
|
217
|
+
assert_equal 4, @device.sub_services
|
218
|
+
assert_equal 5, @device.parent
|
219
|
+
assert_equal 6, @device.name
|
220
|
+
assert_equal 7, @device.manufacturer
|
221
|
+
assert_equal 8, @device.manufacturer_url
|
222
|
+
assert_equal 9, @device.model_description
|
223
|
+
assert_equal 10, @device.model_name
|
224
|
+
assert_equal 11, @device.model_number
|
225
|
+
assert_equal 12, @device.model_url
|
226
|
+
assert_equal 13, @device.serial_number
|
227
|
+
assert_equal 14, @device.upc
|
228
|
+
|
229
|
+
assert data.empty?, 'data not consumed'
|
230
|
+
end
|
231
|
+
|
232
|
+
def test_root_device
|
233
|
+
assert_equal @device, @device.root_device
|
234
|
+
assert_equal @device, @sub_device.root_device
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_service_id
|
238
|
+
assert_equal 'TestService', @device.service_id(@service)
|
239
|
+
end
|
240
|
+
|
241
|
+
def test_service_ids
|
242
|
+
expected = { UPnP::Service::TestService => 'TestService' }
|
243
|
+
assert_equal expected, @device.service_ids
|
244
|
+
end
|
245
|
+
|
246
|
+
def test_services
|
247
|
+
assert_equal [@service], @device.services
|
248
|
+
assert_equal [], @sub_device.services
|
249
|
+
end
|
250
|
+
|
251
|
+
def test_setup_server
|
252
|
+
server = @device.setup_server
|
253
|
+
|
254
|
+
mount_tab = server.instance_variable_get :@mount_tab
|
255
|
+
|
256
|
+
assert mount_tab[@service.scpd_url]
|
257
|
+
end
|
258
|
+
|
259
|
+
def test_type_urn
|
260
|
+
assert_equal "#{UPnP::DEVICE_SCHEMA_PREFIX}:TestDevice:1",
|
261
|
+
@device.type_urn
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_validate
|
265
|
+
@device.friendly_name = nil
|
266
|
+
|
267
|
+
e = assert_raise UPnP::Device::ValidationError do
|
268
|
+
@device.validate
|
269
|
+
end
|
270
|
+
|
271
|
+
assert_equal 'friendly_name missing', e.message
|
272
|
+
|
273
|
+
@device.friendly_name = 'name'
|
274
|
+
|
275
|
+
@device.manufacturer = nil
|
276
|
+
|
277
|
+
e = assert_raise UPnP::Device::ValidationError do
|
278
|
+
@device.validate
|
279
|
+
end
|
280
|
+
|
281
|
+
assert_equal 'manufacturer missing', e.message
|
282
|
+
|
283
|
+
@device.manufacturer = 'manufacturer'
|
284
|
+
|
285
|
+
@device.model_name = nil
|
286
|
+
|
287
|
+
e = assert_raise UPnP::Device::ValidationError do
|
288
|
+
@device.validate
|
289
|
+
end
|
290
|
+
|
291
|
+
assert_equal 'model_name missing', e.message
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|