libowl 1.2

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.
@@ -0,0 +1,243 @@
1
+ ################################################################################
2
+ #This file defines the ClientWorldModel class, an object that connects to an
3
+ #Owl world model, simplifying requesting and handling data. This class does not
4
+ #keep a threaded connection open as in the ClientWorldConnection so message
5
+ #handling must be scheduled by the client.
6
+ #
7
+ # Copyright (c) 2013 Bernhard Firner
8
+ # All rights reserved.
9
+ #
10
+ # This program is free software; you can redistribute it and/or
11
+ # modify it under the terms of the GNU General Public License
12
+ # as published by the Free Software Foundation; either version 2
13
+ # of the License, or (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program; if not, write to the Free Software
22
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23
+ # or visit http://www.gnu.org/licenses/gpl-2.0.html
24
+ #
25
+ ################################################################################
26
+
27
+ require 'socket'
28
+ require 'libowl/message_constants.rb'
29
+ require 'libowl/buffer_manip.rb'
30
+ require 'libowl/wm_data.rb'
31
+ require 'libowl/transient_request.rb'
32
+
33
+ ##
34
+ #This class abstracts the network details of connecting to a World Model.
35
+ #However, this class does not use a thread to handle networking in the
36
+ #background as in the ClientWorldConnection class, so handleMessage must
37
+ #be called manually. If threads are available then the ClientWorldConnection
38
+ #class is simpler to use.
39
+ class ClientWorldModel
40
+ attr_accessor :alias_to_attr_name, :alias_to_origin_name
41
+ attr_reader :connected
42
+ @alias_to_attr_name
43
+ @alias_to_origin_name
44
+ @connected
45
+
46
+ @data_callback
47
+ @uri_response_callback
48
+
49
+ def initialize(host, port, data_callback = nil, uri_response_callback = nil)
50
+ @connected = false
51
+ @host = host
52
+ @port = port
53
+ @socket = TCPSocket.open(host, port)
54
+ handshake = ""
55
+ ver_string = "GRAIL client protocol"
56
+ #The handshake is the length of the message, the protocol string, and the version (0).
57
+ handshake << [ver_string.length].pack('N') << ver_string << "\x00\x00"
58
+ #Send a handshake and then receive one
59
+ @socket.send(handshake, 0)
60
+ inshake = @socket.recvfrom(handshake.length)[0]
61
+ while (inshake.length < handshake.length)
62
+ #puts "Waiting for #{handshake.length - inshake.length} byte more of handshake."
63
+ inshake += @socket.recvfrom(handshake.length - inshake.length)[0]
64
+ end
65
+
66
+ @connected = true
67
+ for i in 1..handshake.length
68
+ if handshake[i] != inshake[i]
69
+ puts "Handshake failure!"
70
+ puts "For byte i we sent #{handshake[i]} but got #{inshake[i]}"
71
+ @connected = false
72
+ end
73
+ end
74
+
75
+ @alias_to_attr_name = {}
76
+ @alias_to_origin_name = {}
77
+
78
+ @data_callback = data_callback
79
+ @uri_response_callback = uri_response_callback
80
+ end
81
+
82
+ def close()
83
+ @socket.close()
84
+ @connected = false
85
+ end
86
+
87
+ #Handle a message of currently unknown type
88
+ def handleMessage()
89
+ #puts "Handling message..."
90
+ #Get the message length as n unsigned integer
91
+ inlen = (@socket.recvfrom(4)[0]).unpack('N')[0]
92
+ inbuff = @socket.recvfrom(inlen)[0]
93
+ #Keep reading until the entire packet is read
94
+ #TODO This can block forever if a communication error occurs.
95
+ while (inbuff.length < inlen)
96
+ inbuff += @socket.recvfrom(inlen-inbuff.length)[0]
97
+ end
98
+ #Byte that indicates message type
99
+ control = inbuff.unpack('C')[0]
100
+ if control == ATTRIBUTE_ALIAS
101
+ decodeAttributeAlias(inbuff[1, inbuff.length - 1])
102
+ elsif control == ORIGIN_ALIAS
103
+ decodeOriginAlias(inbuff[1, inbuff.length - 1])
104
+ elsif control == REQUEST_COMPLETE
105
+ ticket = decodeTicketMessage(inbuff[1, inbuff.length-1])
106
+ #puts "World Model completed request for ticket #{ticket}"
107
+ elsif control == DATA_RESPONSE
108
+ if (@data_callback != nil)
109
+ @data_callback.call(decodeDataResponse(inbuff[1, inbuff.length - 1]))
110
+ end
111
+ elsif control == URI_RESPONSE
112
+ if (@uri_response_callback != nil)
113
+ @uri_response_callback.call(decodeURIResponse(inbuff[1, inbuff.length - 1]))
114
+ end
115
+ end
116
+ #puts "processed message with id #{control}"
117
+ return control
118
+ end
119
+
120
+ #Decode attribute alias message
121
+ def decodeAttributeAlias(inbuff)
122
+ num_aliases = inbuff.unpack('N')[0]
123
+ rest = inbuff[4, inbuff.length - 1]
124
+ for i in 1..num_aliases do
125
+ attr_alias = rest.unpack('N')[0]
126
+ name, rest = splitURIFromRest(rest[4, rest.length - 1])
127
+ #Assign this name to the given alias
128
+ @alias_to_attr_name[attr_alias] = name
129
+ end
130
+ end
131
+
132
+ #Decode origin alias message
133
+ def decodeOriginAlias(inbuff)
134
+ num_aliases = inbuff.unpack('N')[0]
135
+ rest = inbuff[4, inbuff.length - 1]
136
+ for i in 1..num_aliases do
137
+ origin_alias = rest.unpack('N')[0]
138
+ name, rest = splitURIFromRest(rest[4, rest.length - 1])
139
+ #Assign this name to the given alias
140
+ @alias_to_origin_name[origin_alias] = name
141
+ end
142
+ end
143
+
144
+ #Decode a ticket message or a request complete message.
145
+ def decodeTicketMessage(inbuff)
146
+ return inbuff.unpack('N')[0]
147
+ end
148
+
149
+ def decodeURIResponse(inbuff)
150
+ uris = []
151
+ if (inbuff != nil)
152
+ rest = inbuff
153
+ while (rest.length > 4)
154
+ name, rest = splitURIFromRest(rest)
155
+ uris.push(name)
156
+ end
157
+ end
158
+ return uris
159
+ end
160
+
161
+ def decodeDataResponse(inbuff)
162
+ attributes = []
163
+ object_uri, rest = splitURIFromRest(inbuff)
164
+ ticket = rest.unpack('N')[0]
165
+ total_attributes = rest[4, rest.length - 1].unpack('N')[0]
166
+ rest = rest[8, rest.length]
167
+ #puts "Decoding #{total_attributes} attributes"
168
+ for i in 1..total_attributes do
169
+ name_alias = rest.unpack('N')[0]
170
+ creation_date = unpackuint64(rest[4, rest.length - 1])
171
+ expiration_date = unpackuint64(rest[12, rest.length - 1])
172
+ origin_alias = rest[20, rest.length - 1].unpack('N')[0]
173
+ data_len = rest[24, rest.length - 1].unpack('N')[0]
174
+ data = rest[28, data_len]
175
+ rest = rest[28+data_len, rest.length - 1]
176
+ attributes.push(WMAttribute.new(@alias_to_attr_name[name_alias], data, creation_date, expiration_date, @alias_to_origin_name[origin_alias]))
177
+ end
178
+ return WMData.new(object_uri, attributes, ticket)
179
+ end
180
+
181
+ def sendSnapshotRequest(name_pattern, attribute_patterns, ticket, start_time = 0, stop_time = 0)
182
+ buff = [SNAPSHOT_REQUEST].pack('C')
183
+
184
+ buff += [ticket].pack('N')
185
+ buff += strToSizedUTF16(name_pattern)
186
+ buff += [attribute_patterns.length].pack('N')
187
+
188
+ attribute_patterns.each{|pattern|
189
+ buff += strToSizedUTF16(pattern)
190
+ }
191
+
192
+ buff += packuint64(start_time)
193
+ buff += packuint64(stop_time)
194
+
195
+ #Send the message with its length prepended to the front
196
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
197
+ end
198
+
199
+ def sendRangeRequest(name_pattern, attribute_patterns, ticket, start_time, stop_time)
200
+ buff = [RANGE_REQUEST].pack('C')
201
+
202
+ buff += [ticket].pack('N')
203
+ buff += strToSizedUTF16(name_pattern)
204
+ buff += [attribute_patterns.length].pack('N')
205
+
206
+ attribute_patterns.each{|pattern|
207
+ buff += strToSizedUTF16(pattern)
208
+ }
209
+
210
+ buff += packuint64(start_time)
211
+ buff += packuint64(stop_time)
212
+
213
+ #Send the message with its length prepended to the front
214
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
215
+ end
216
+
217
+ def sendStreamRequest(name_pattern, attribute_patterns, update_interval, ticket)
218
+ buff = [STREAM_REQUEST].pack('C')
219
+
220
+ buff += [ticket].pack('N')
221
+ buff += strToSizedUTF16(name_pattern)
222
+ buff += [attribute_patterns.length].pack('N')
223
+
224
+ attribute_patterns.each{|pattern|
225
+ buff += strToSizedUTF16(pattern)
226
+ }
227
+
228
+ buff += packuint64(0)
229
+ buff += packuint64(update_interval)
230
+
231
+ #Send the message with its length prepended to the front
232
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
233
+ end
234
+
235
+ def sendURISearch(name_pattern)
236
+ buff = [URI_SEARCH].pack('C')
237
+ buff += strToUnicode(name_pattern)
238
+ #Send the message with its length prepended to the front
239
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
240
+ end
241
+
242
+ end
243
+
@@ -0,0 +1,27 @@
1
+ #Message constants that indicate the purpose of a message
2
+
3
+ #Keep alive message
4
+ KEEP_ALIVE = 0;
5
+ #Request a snapshot of the current world model state
6
+ SNAPSHOT_REQUEST = 1;
7
+ #Request a snapshot of the wm state in a time range
8
+ RANGE_REQUEST = 2;
9
+ #Request a stream of data from the world model
10
+ STREAM_REQUEST = 3;
11
+ #Alias an attribute from the world model
12
+ ATTRIBUTE_ALIAS = 4;
13
+ #Alias an origin from the world model
14
+ ORIGIN_ALIAS = 5;
15
+ #Finish a request
16
+ REQUEST_COMPLETE = 6;
17
+ #Cancel a request
18
+ CANCEL_REQUEST = 7;
19
+ #Message contains a data response from the world model
20
+ DATA_RESPONSE = 8;
21
+ #Search names in the world model
22
+ URI_SEARCH = 9;
23
+ #Response to a uri search message.
24
+ URI_RESPONSE = 10;
25
+ #Set a preference for some data origins
26
+ ORIGIN_PREFERENCE = 11;
27
+
@@ -0,0 +1,72 @@
1
+ ################################################################################
2
+ #This file defines the Response class, an object that represents data from
3
+ #an owl world model that is sent for a non-streaming request.
4
+ #
5
+ # Copyright (c) 2013 Bernhard Firner
6
+ # All rights reserved.
7
+ #
8
+ # This program is free software; you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License
10
+ # as published by the Free Software Foundation; either version 2
11
+ # of the License, or (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program; if not, write to the Free Software
20
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21
+ # or visit http://www.gnu.org/licenses/gpl-2.0.html
22
+ #
23
+ ################################################################################
24
+
25
+ #Response of a client request to the world model
26
+ class Response
27
+ ##
28
+ #Initialize with the ClientWorldConnection that spawed this Response and
29
+ #the key of the request.
30
+ def initialize(cwc, key)
31
+ @cwc = cwc
32
+ @request_key = key
33
+ end
34
+
35
+ ##
36
+ #Get the data of this Response, blocking until that data is ready or
37
+ #an error occurs.
38
+ def get()
39
+ while (not (ready() or isError()))
40
+ sleep(1)
41
+ end
42
+ if (isError())
43
+ raise getError()
44
+ else
45
+ return @cwc.getNext(@request_key)
46
+ end
47
+ end
48
+
49
+ ##
50
+ #Returns true if data is available for a call to get().
51
+ def ready()
52
+ return @cwc.hasNext(@request_key)
53
+ end
54
+
55
+ ##
56
+ #Returns true if an error has occured.
57
+ def isError()
58
+ return @cwc.hasError(@request_key)
59
+ end
60
+
61
+ ##
62
+ #Get the error that occured.
63
+ def getError()
64
+ return @cwc.getError(@request_key)
65
+ end
66
+
67
+ ##
68
+ #Cancel this request.
69
+ def cancel()
70
+ return @cwc.cancelRequest(@request_key)
71
+ end
72
+ end
@@ -0,0 +1,17 @@
1
+ #Sensor sample format
2
+ class SensorSample
3
+ attr_accessor :phy_layer, :device_id, :receiver_id, :timestamp, :rssi, :sense_data
4
+
5
+ def initialize(phy, device_id, receiver_id, timestamp, rssi, sense_data)
6
+ @phy_layer = phy
7
+ @device_id = device_id
8
+ @receiver_id = receiver_id
9
+ @timestamp = timestamp
10
+ @rssi = rssi
11
+ @sense_data = sense_data
12
+ end
13
+
14
+ def to_s()
15
+ return "#{@timestamp}: (phy #{@phy_layer}) #{@device_id} -> #{@receiver_id}, RSS:#{@rssi}, Datalength:#{@sense_data.length} Data:#{@sense_data.unpack('C*').join(', ')}"
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ #Solution types used when communicating with the distributor
2
+
3
+ class SolutionType
4
+ #Used for auto-incremented alias numbers
5
+ @@num_aliases = 0
6
+ attr_accessor :type_alias, :data_uri
7
+ #Number to refer to this type
8
+ @type_alias
9
+ #The uri of this type
10
+ @data_uri
11
+
12
+ def initialize(uri, type_alias = -1)
13
+ @data_uri = uri
14
+ #Auto-increment to the next alias number if one wasn't explicitly provided
15
+ if (type_alias >= 0)
16
+ @type_alias = type_alias
17
+ else
18
+ @type_alias = @@num_aliases
19
+ @@num_aliases += 1
20
+ end
21
+ end
22
+ end
23
+
24
+ class SolutionData
25
+ attr_accessor :region, :time, :target, :sol_name, :data
26
+ #Region of the solution
27
+ @region
28
+ #Time of the solution (milliseconds since midnight Jan 1, 1970)
29
+ @time
30
+ #Target of the solution
31
+ @target
32
+ #Name of this solution
33
+ @sol_name
34
+ #Binary buffer with solution data
35
+ @data
36
+
37
+ def initialize(region, time, target, sol_name, data)
38
+ @region = region
39
+ @time = time
40
+ @target = target
41
+ @sol_name = sol_name
42
+ @data = data
43
+ end
44
+
45
+ def to_s()
46
+ return "#{@region}:#{@target}:#{@sol_name} @ time #{@time} -> #{@data.unpack('H*')}"
47
+ end
48
+ end
49
+
@@ -0,0 +1,126 @@
1
+ #This class abstracts the details of connecting to a
2
+ #GRAIL3 aggregator as a solver.
3
+ #Solvers subscribe to the aggregator and then receive packets.
4
+
5
+ require 'socket'
6
+ require 'libowl/buffer_manip.rb'
7
+ require 'libowl/aggregator_rules.rb'
8
+ require 'libowl/sensor_sample.rb'
9
+
10
+ class SolverAggregator
11
+ #Message constants
12
+ KEEP_ALIVE = 0
13
+ CERTIFICATE = 1
14
+ ACK_CERTIFICATE = 2 #There is no message for certificate denial
15
+ SUBSCRIPTION_REQUEST = 3
16
+ SUBSCRIPTION_RESPONSE = 4
17
+ DEVICE_POSITION = 5
18
+ SERVER_SAMPLE = 6
19
+ BUFFER_OVERRUN = 7
20
+
21
+ attr_accessor :available_packets, :cur_rules, :connected
22
+ @available_packets
23
+ @cur_rules
24
+
25
+ def initialize(host, port)
26
+ @connected = false
27
+ @host = host
28
+ @port = port
29
+ @socket = TCPSocket.open(host, port)
30
+ handshake = ""
31
+ ver_string = "GRAIL solver protocol"
32
+ #The handshake is the length of the message, the protocol string, and the version (0).
33
+ handshake << [ver_string.length].pack('N') << ver_string << "\x00\x00"
34
+ #Receive a handshake and then send one
35
+ @socket.recvfrom(handshake.length)
36
+ @socket.send(handshake, 0)
37
+
38
+ @available_packets = []
39
+ @cur_rules = []
40
+ end
41
+
42
+ def close()
43
+ @socket.close()
44
+ @connected = false
45
+ end
46
+
47
+ #Handle a message of currently unknown type
48
+ def handleMessage()
49
+ #Get the message length as n unsigned integer
50
+ inlen = (@socket.recvfrom(4)[0]).unpack('N')[0]
51
+ if (nil == inlen) then
52
+ return nil
53
+ end
54
+ inbuff = @socket.recvfrom(inlen)[0]
55
+ #Byte that indicates message type
56
+ control = inbuff.unpack('C')[0]
57
+ case control
58
+ when SUBSCRIPTION_RESPONSE
59
+ decodeSubResponse(inbuff[1, inbuff.length - 1])
60
+ return SUBSCRIPTION_RESPONSE
61
+ when SERVER_SAMPLE
62
+ decodeServerSample(inbuff[1, inbuff.length - 1])
63
+ return SERVER_SAMPLE
64
+ else
65
+ KEEP_ALIVE
66
+ end
67
+ end
68
+
69
+ #Decode a subscription response and store the current rules in @cur_rules
70
+ def decodeSubResponse(inbuff)
71
+ puts "Got subscription response!"
72
+ num_rules = inbuff.unpack('N')[0]
73
+ rest = inbuff[4, inbuff.length - 1]
74
+ rules = []
75
+ for i in 1..num_rules do
76
+ phy_layer, num_txers = rest.unpack('CN')
77
+ txlist = []
78
+ rest = rest[5, rest.length - 1]
79
+ for j in 1..num_txers do
80
+ txlist.push([rest[0, 16], rest[16, 16]])
81
+ rest = rest[32, rest.length - 1]
82
+ end
83
+ update_interval = unpackuint64(rest)
84
+ rest = rest[8, rest.length - 1]
85
+ rule = AggrRule.new(phy_layer, txlist, update_interval)
86
+ rules.push(rule)
87
+ end
88
+ @cur_rules = rules
89
+ end
90
+
91
+ #Decode a server sample message
92
+ def decodeServerSample(inbuff)
93
+ if (inbuff != nil)
94
+ phy_layer = inbuff.unpack('C')
95
+ rest = inbuff[1, inbuff.length - 1]
96
+ txid = unpackuint128(rest)
97
+ rxid = unpackuint128(rest[16, rest.length - 1])
98
+ rest = rest[32, rest.length - 1]
99
+ timestamp, rssi = rest.unpack('Gg')
100
+ sense_data = rest[12, rest.length - 1]
101
+ @available_packets.push(SensorSample.new(phy_layer, txid, rxid, timestamp, rssi, sense_data))
102
+ end
103
+ end
104
+
105
+ def sendSubscription(rules)
106
+ #Start assembling a request message
107
+ buff = [SUBSCRIPTION_REQUEST].pack('C')
108
+ #Number of rules
109
+ buff += [rules.length].pack('N')
110
+ for rule in rules do
111
+ buff += [rule.phy_layer].pack('C')
112
+ buff += [rule.txers.length].pack('N')
113
+ #Push each transmitter/mask pair
114
+ for txer in rule.txers do
115
+ buff += txer.id + txer.mask
116
+ end
117
+ buff += packuint64(rule.update_interval)
118
+ end
119
+ #Send the message prepended with the length
120
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
121
+
122
+ #Get the subscription response
123
+ handleMessage()
124
+ end
125
+
126
+ end