libowl 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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