SensorStream 0.0.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.
- checksums.yaml +7 -0
- data/lib/SensorStream.rb +507 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d6699293fcd7d91151220cc95fc2c3f9b0ffd801
|
4
|
+
data.tar.gz: 06d13baa1c9f613fb28e8e90b5f1edca12a5c5e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: deeb66a8e324f490ec269779d0c67e4672c77a9675b12a1b6c6a26d1a74e5cc439746230b2709b9f534b8d5d15f6079fa462f5c603a238e13820b8d9abbe6d2e
|
7
|
+
data.tar.gz: 91945130364b7a516eb353002a0b19497d7d385014734967a43282a9c71b4e27b1982fee09772627883dfd8c927435e02dd7a98f05a546d7ddd3b602b3471753
|
data/lib/SensorStream.rb
ADDED
@@ -0,0 +1,507 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require 'pp'
|
7
|
+
require 'base64'
|
8
|
+
require 'open3'
|
9
|
+
|
10
|
+
module SensorStream
|
11
|
+
@hostName = "dodeca.coas.oregonstate.edu";
|
12
|
+
@portNumber = 80;
|
13
|
+
|
14
|
+
# get/set hostname
|
15
|
+
def self.hostName=(newHostName)
|
16
|
+
@hostName = name;
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.hostName
|
20
|
+
return @hostName;
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.portNumber
|
24
|
+
return @portNumber;
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.portNumber=(newPortNum)
|
28
|
+
@portNumber = newPortNum;
|
29
|
+
end
|
30
|
+
|
31
|
+
# create a new device on the server
|
32
|
+
def self.createDevice(userName, deviceName, description)
|
33
|
+
dict = {"UserName" => userName,
|
34
|
+
"DeviceName" => deviceName,
|
35
|
+
"Description" => description};
|
36
|
+
|
37
|
+
uri = URI.parse("http://" + @hostName + "/device.ashx?create=");
|
38
|
+
http = Net::HTTP.new(@hostName, @portNumber);
|
39
|
+
http.read_timeout = 600; # 10 minute timeout
|
40
|
+
resp = http.post(uri.request_uri,
|
41
|
+
JSON.generate(dict),
|
42
|
+
{"Content-Type" => "application/json"});
|
43
|
+
|
44
|
+
if (resp.code != "200")
|
45
|
+
STDERR.puts "Error creating SensorStream device! (" + resp.code + ")\n" + resp.body;
|
46
|
+
return nil;
|
47
|
+
else
|
48
|
+
#Get the GUID from the response and make a device object
|
49
|
+
respDict = JSON.parse(resp.body);
|
50
|
+
return SensorStream::Device.new(deviceName, userName,
|
51
|
+
description, respDict["guid"]);
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.deleteDevice(deviceGUID)
|
56
|
+
uri = URI.parse("http://" + @hostName + "/device.ashx?delete=device");
|
57
|
+
http = Net::HTTP.new(@hostName, @portNumber);
|
58
|
+
http.read_timeout = 600;
|
59
|
+
resp = http.post(uri.request_uri, "",
|
60
|
+
{"Content-Type" => "application/json", "key" => deviceGUID});
|
61
|
+
|
62
|
+
if (resp.code != "200")
|
63
|
+
STDERR.puts "Error deleting SensorStream device! (" + resp.code + ")\n" + resp.body;
|
64
|
+
return false;
|
65
|
+
else
|
66
|
+
return true;
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.getDevices()
|
71
|
+
uri = URI.parse("http://" + @hostName + "/device.ashx?getdevices=");
|
72
|
+
http = Net::HTTP.new(@hostName, @portNumber);
|
73
|
+
http.read_timeout = 600; # 10 minute timeout
|
74
|
+
resp = http.get(uri.request_uri);
|
75
|
+
|
76
|
+
if (resp.code != "200")
|
77
|
+
return nil;
|
78
|
+
else
|
79
|
+
deviceDict = JSON.parse(resp.body);
|
80
|
+
devices = [];
|
81
|
+
deviceDict["Devices"].each do |device|
|
82
|
+
devices << SensorStream::Device.new(device["DeviceName"],
|
83
|
+
device["UserName"],
|
84
|
+
device["Description"]);
|
85
|
+
end
|
86
|
+
return devices;
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Device
|
91
|
+
@guid = "";
|
92
|
+
@deviceName = "";
|
93
|
+
@userName = "";
|
94
|
+
@description = "";
|
95
|
+
|
96
|
+
def initialize(newDN, newUN, newDSC, newGuid = "")
|
97
|
+
@guid = newGuid;
|
98
|
+
@deviceName = newDN;
|
99
|
+
@userName = newUN;
|
100
|
+
@description = newDSC;
|
101
|
+
end
|
102
|
+
|
103
|
+
def guid(newGUID = @guid)
|
104
|
+
return @guid = newGUID;
|
105
|
+
end
|
106
|
+
|
107
|
+
def guid=(newGUID = @guid)
|
108
|
+
self.guid(newGUID);
|
109
|
+
end
|
110
|
+
|
111
|
+
def deviceName(newDeviceName = @deviceName)
|
112
|
+
return @deviceName = newDeviceName;
|
113
|
+
end
|
114
|
+
|
115
|
+
def deviceName=(newDeviceName)
|
116
|
+
self.deviceName(newDeviceName);
|
117
|
+
end
|
118
|
+
|
119
|
+
def userName(newUserName = @userName)
|
120
|
+
return @userName = newUserName;
|
121
|
+
end
|
122
|
+
|
123
|
+
def userName=(newUserName)
|
124
|
+
self.userName(newUserName);
|
125
|
+
end
|
126
|
+
|
127
|
+
def description(newDescription = @description)
|
128
|
+
return @description = newDescription;
|
129
|
+
end
|
130
|
+
|
131
|
+
def description=(newDescription)
|
132
|
+
self.description(newDescription);
|
133
|
+
end
|
134
|
+
|
135
|
+
def createComplexStream(name, description, elements)
|
136
|
+
dict = {"Name" => name,
|
137
|
+
"Description" => description,
|
138
|
+
"Streams" => elements};
|
139
|
+
|
140
|
+
uri = URI.parse("http://" + SensorStream.hostName + "/stream.ashx?create=");
|
141
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
142
|
+
http.read_timeout = 600; # 10 minute timeout
|
143
|
+
resp = http.post(uri.request_uri,
|
144
|
+
JSON.generate(dict),
|
145
|
+
{"Content-Type" => "application/json",
|
146
|
+
"key" => @guid});
|
147
|
+
|
148
|
+
if (resp.code != "200")
|
149
|
+
STDERR.puts "Error creating SensorStream device! (" + resp.code + ")\n" + resp.body;
|
150
|
+
return "";
|
151
|
+
else
|
152
|
+
#Get the GUID from the response
|
153
|
+
respDict = JSON.parse(resp.body);
|
154
|
+
tempStream = ComplexStream.new(@guid, respDict["StreamID"],
|
155
|
+
elements, name, description);
|
156
|
+
return tempStream;
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def createSimpleStream(name, type, units, description)
|
161
|
+
dict = {"Name" => name,
|
162
|
+
"Description" => description,
|
163
|
+
"Type" => type,
|
164
|
+
"Units" => units};
|
165
|
+
|
166
|
+
uri = URI.parse("http://" + SensorStream.hostName + "/stream.ashx?create=");
|
167
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
168
|
+
http.read_timeout = 600; # 10 minute timeout
|
169
|
+
resp = http.post(uri.request_uri,
|
170
|
+
JSON.generate(dict),
|
171
|
+
{"Content-Type" => "application/json",
|
172
|
+
"key" => @guid});
|
173
|
+
|
174
|
+
if (resp.code != "200")
|
175
|
+
STDERR.puts "Error creating SensorStream stream! (" + resp.code + ")\n" + resp.body;
|
176
|
+
#puts "Request URI: ";
|
177
|
+
#puts uri;
|
178
|
+
#puts "Request JSON: " + JSON.generate(dict);
|
179
|
+
return nil;
|
180
|
+
else
|
181
|
+
#Get the GUID from the response, return an object for the stream
|
182
|
+
respDict = JSON.parse(resp.body);
|
183
|
+
tempStream = SimpleStream.new(@guid, respDict["StreamID"],
|
184
|
+
name, type, units, description);
|
185
|
+
return tempStream;
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def deleteStream(streamID)
|
190
|
+
uri = URI.parse("http://" + SensorStream.hostName + "/stream.ashx?delete=" + streamID);
|
191
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
192
|
+
http.read_timeout = 600; # 10 minute timeout
|
193
|
+
resp = http.post(uri.request_uri, "",
|
194
|
+
{"Content-Type" => "application/json", "key" => @guid});
|
195
|
+
|
196
|
+
if (resp.code != "200")
|
197
|
+
STDERR.puts "Error deleting stream " + streamID + " (" + resp.code + ")\n" + resp.body;
|
198
|
+
return false;
|
199
|
+
else
|
200
|
+
return true;
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def getStreams()
|
205
|
+
uri = URI.parse("http://" + SensorStream.hostName +
|
206
|
+
"/stream.ashx?getstreams=" + @deviceName +
|
207
|
+
"&user=" + @userName);
|
208
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
209
|
+
http.read_timeout = 600; # 10 minute timeout
|
210
|
+
resp = http.get(uri.request_uri);
|
211
|
+
|
212
|
+
if (resp.code != "200")
|
213
|
+
return nil;
|
214
|
+
else
|
215
|
+
ret = JSON.parse(resp.body);
|
216
|
+
streams = [];
|
217
|
+
ret["Streams"].each do |streamDict|
|
218
|
+
# Detect whether the stream we just got is complex
|
219
|
+
if(streamDict["Streams"].nil?)
|
220
|
+
tempStream = SimpleStream.new(@guid,
|
221
|
+
streamDict["StreamID"],
|
222
|
+
streamDict["Name"],
|
223
|
+
streamDict["Type"],
|
224
|
+
streamDict["Units"],
|
225
|
+
streamDict["Description"]);
|
226
|
+
streams << tempStream;
|
227
|
+
else
|
228
|
+
tempStream = ComplexStream.new(@guid,
|
229
|
+
streamDict["StreamID"],
|
230
|
+
streamDict["Streams"],
|
231
|
+
streamDict["Name"],
|
232
|
+
streamDict["Description"]);
|
233
|
+
streams << tempStream;
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
return streams;
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class SimpleStream
|
243
|
+
@guid = "";
|
244
|
+
@streamID = "";
|
245
|
+
@name = "";
|
246
|
+
@type = "";
|
247
|
+
@units = "";
|
248
|
+
@description = "";
|
249
|
+
|
250
|
+
def initialize(newGuid = "",
|
251
|
+
newStreamID = "",
|
252
|
+
newName = "",
|
253
|
+
newType = "",
|
254
|
+
newUnits = "",
|
255
|
+
newDescription = "")
|
256
|
+
@guid = newGuid;
|
257
|
+
@streamID = newStreamID;
|
258
|
+
@name = newName;
|
259
|
+
@type = newType;
|
260
|
+
@units = newUnits;
|
261
|
+
@description = newDescription;
|
262
|
+
end
|
263
|
+
|
264
|
+
def guid(newGUID = @guid)
|
265
|
+
return @guid = newGUID;
|
266
|
+
end
|
267
|
+
|
268
|
+
def guid=(newGUID = @guid)
|
269
|
+
self.guid(newGUID);
|
270
|
+
end
|
271
|
+
|
272
|
+
def streamID(newStreamID = @streamID)
|
273
|
+
return @streamID = newStreamID;
|
274
|
+
end
|
275
|
+
|
276
|
+
def streamID=(newStreamID)
|
277
|
+
self.streamID(newStreamID);
|
278
|
+
end
|
279
|
+
|
280
|
+
def name(newName = @name)
|
281
|
+
return @name = newName;
|
282
|
+
end
|
283
|
+
|
284
|
+
def name=(newName)
|
285
|
+
self.name(newName);
|
286
|
+
end
|
287
|
+
|
288
|
+
def type(newType = @type)
|
289
|
+
return @type = newType;
|
290
|
+
end
|
291
|
+
|
292
|
+
def type=(newType)
|
293
|
+
self.type(newType);
|
294
|
+
end
|
295
|
+
|
296
|
+
def units(newUnits = @units)
|
297
|
+
return @units = newUnits;
|
298
|
+
end
|
299
|
+
|
300
|
+
def units=(newUnits)
|
301
|
+
self.units(newUnits);
|
302
|
+
end
|
303
|
+
|
304
|
+
def description(newDescription = @description)
|
305
|
+
return @description = newDescription;
|
306
|
+
end
|
307
|
+
|
308
|
+
def description=(newDescription)
|
309
|
+
self.description(newDescription);
|
310
|
+
end
|
311
|
+
|
312
|
+
def to_s
|
313
|
+
return "Name: " + @name +
|
314
|
+
"\n\tStreamID: " + @streamID +
|
315
|
+
"\n\tType: " + @type +
|
316
|
+
"\n\tUnits: " + @units +
|
317
|
+
"\n\tDescription: " + @description;
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
def publishEvent(value, time=nil)
|
322
|
+
dict = {"StreamID" => @streamID, "Value" => value.to_s};
|
323
|
+
|
324
|
+
# This is unlikely to work yet.
|
325
|
+
# Need to convert from ruby time representation to ISO8601 format
|
326
|
+
if(time != nil)
|
327
|
+
dict["Time"] = time.strftime("%FT%T%:z");
|
328
|
+
end
|
329
|
+
|
330
|
+
uri = URI.parse("http://" + SensorStream.hostName + "/data.ashx?create=");
|
331
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
332
|
+
http.read_timeout = 600; # 10 minute timeout
|
333
|
+
resp = http.post(uri.request_uri,
|
334
|
+
JSON.generate(dict),
|
335
|
+
{"Content-Type" => "application/json", "key" => @guid});
|
336
|
+
|
337
|
+
if (resp.code != "200")
|
338
|
+
STDERR.puts "Error publishing SensorStream event! (" + resp.code + ")\n" + resp.body;
|
339
|
+
return "";
|
340
|
+
else
|
341
|
+
#Get the GUID from the response
|
342
|
+
respDict = JSON.parse(resp.body);
|
343
|
+
return respDict["Time"];
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def streamID
|
348
|
+
return @streamID;
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
class ComplexStream
|
353
|
+
@guid = "";
|
354
|
+
@streamID = "";
|
355
|
+
@elements = [];
|
356
|
+
@name = "";
|
357
|
+
@description = "";
|
358
|
+
@deferedMsgs = [];
|
359
|
+
|
360
|
+
def initialize(newGuid = "", newStreamID = "",
|
361
|
+
newElements = [], newName = "",
|
362
|
+
newDescription = "")
|
363
|
+
@guid = newGuid;
|
364
|
+
@streamID = newStreamID;
|
365
|
+
@elements = newElements;
|
366
|
+
@name = newName;
|
367
|
+
@description = newDescription;
|
368
|
+
end
|
369
|
+
|
370
|
+
def guid(newGUID = @guid)
|
371
|
+
return @guid = newGUID;
|
372
|
+
end
|
373
|
+
|
374
|
+
def guid=(newGUID = @guid)
|
375
|
+
self.guid(newGUID);
|
376
|
+
end
|
377
|
+
|
378
|
+
def streamID(newStreamID = @streamID)
|
379
|
+
return @streamID = newStreamID;
|
380
|
+
end
|
381
|
+
|
382
|
+
def streamID=(newStreamID)
|
383
|
+
self.streamID(newStreamID);
|
384
|
+
end
|
385
|
+
|
386
|
+
def name(newName = @name)
|
387
|
+
return @name = newName;
|
388
|
+
end
|
389
|
+
|
390
|
+
def name=(newName)
|
391
|
+
self.name(newName);
|
392
|
+
end
|
393
|
+
|
394
|
+
def type(newType = @type)
|
395
|
+
return @type = newType;
|
396
|
+
end
|
397
|
+
|
398
|
+
def type=(newType)
|
399
|
+
self.type(newType);
|
400
|
+
end
|
401
|
+
|
402
|
+
def units(newUnits = @units)
|
403
|
+
return @units = newUnits;
|
404
|
+
end
|
405
|
+
|
406
|
+
def units=(newUnits)
|
407
|
+
self.units(newUnits);
|
408
|
+
end
|
409
|
+
|
410
|
+
def description(newDescription = @description)
|
411
|
+
return @description = newDescription;
|
412
|
+
end
|
413
|
+
|
414
|
+
def description=(newDescription)
|
415
|
+
self.description(newDescription);
|
416
|
+
end
|
417
|
+
|
418
|
+
def to_s
|
419
|
+
elementsString = "";
|
420
|
+
@elements.each {|element|
|
421
|
+
elementsString += "\n\t\t" + element["Name"] +
|
422
|
+
"\n\t\t\tUnits: " + element["Type"] +
|
423
|
+
"\n\t\t\tUnits: " + element["Units"];
|
424
|
+
};
|
425
|
+
|
426
|
+
return "Name: " + @name +
|
427
|
+
"\n\tStreamID: " + @streamID +
|
428
|
+
"\n\tElements: " + elementsString +
|
429
|
+
"\n\tDescription: " + @description;
|
430
|
+
end
|
431
|
+
|
432
|
+
def publishEvent(values, time=nil)
|
433
|
+
dict = {"StreamID" => @streamID, "Values" => values};
|
434
|
+
|
435
|
+
# Need to convert from ruby time representation to ISO8601 format
|
436
|
+
if(time != nil)
|
437
|
+
dict["Time"] = time.strftime("%FT%T%:z");
|
438
|
+
end
|
439
|
+
|
440
|
+
uri = URI.parse("http://" + SensorStream.hostName + "/data.ashx?create=");
|
441
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
442
|
+
http.read_timeout = 600; # 10 minute timeout
|
443
|
+
resp = http.post(uri.request_uri, JSON.generate(dict),
|
444
|
+
{"Content-Type" => "application/json", "key" => @guid});
|
445
|
+
|
446
|
+
if (resp.code != "200")
|
447
|
+
STDERR.puts "Error publishing SensorStream event! (" + resp.code + ")\n" + resp.body;
|
448
|
+
puts JSON.generate(dict);
|
449
|
+
return "";
|
450
|
+
else
|
451
|
+
#Get the time from the response
|
452
|
+
respDict = JSON.parse(resp.body);
|
453
|
+
return respDict["Time"];
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def publishEventDefered(values, time=nil)
|
458
|
+
dict = {"StreamID" => @streamID, "Values" => values};
|
459
|
+
|
460
|
+
# Need to convert from ruby time representation to ISO8601 format
|
461
|
+
if(time != nil)
|
462
|
+
dict["Time"] = time.strftime("%FT%T%:z");
|
463
|
+
end
|
464
|
+
|
465
|
+
if(@deferedMsgs.nil?)
|
466
|
+
#puts "Defered messages array was nil, not supposed to happen.";
|
467
|
+
@deferedMsgs = [];
|
468
|
+
end
|
469
|
+
@deferedMsgs << dict;
|
470
|
+
end
|
471
|
+
|
472
|
+
def flushDeferedMsgs()
|
473
|
+
if @deferedMsgs.nil?
|
474
|
+
return 0;
|
475
|
+
end
|
476
|
+
|
477
|
+
if (@deferedMsgs.count == 0)
|
478
|
+
return 0;
|
479
|
+
end
|
480
|
+
|
481
|
+
jsonString = JSON.generate(@deferedMsgs)
|
482
|
+
#puts "Created JSON string";
|
483
|
+
|
484
|
+
uri = URI.parse("http://" + SensorStream.hostName + "/data.ashx?create=");
|
485
|
+
http = Net::HTTP.new(SensorStream.hostName, SensorStream.portNumber);
|
486
|
+
http.read_timeout = 600; # 10 minute timeout
|
487
|
+
|
488
|
+
#puts "Created http object";
|
489
|
+
resp = http.post(uri.request_uri, jsonString,
|
490
|
+
{"Content-Type" => "application/json", "key" => @guid});
|
491
|
+
|
492
|
+
if (resp.code != "200")
|
493
|
+
STDERR.puts "Error publishing SensorStream event! (" + resp.code + ")\n" + resp.body;
|
494
|
+
#puts JSON.generate(@deferedMsgs);
|
495
|
+
return -1;
|
496
|
+
else
|
497
|
+
#Get the GUID from the response
|
498
|
+
respDict = JSON.parse(resp.body);
|
499
|
+
count = @deferedMsgs.count;
|
500
|
+
@deferedMsgs = [];
|
501
|
+
return count;
|
502
|
+
end
|
503
|
+
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
end
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: SensorStream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- William Dillon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-30 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A Ruby implementation of the SensorStream API
|
14
|
+
email: william@housedillon.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/SensorStream.rb
|
20
|
+
homepage: http://ramone.coas.oregonstate.edu
|
21
|
+
licenses:
|
22
|
+
- Proprietary
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.0.3
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: SensorStream API
|
44
|
+
test_files: []
|