tinkerforge 2.0.7
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/lib/tinkerforge.rb +5 -0
- data/lib/tinkerforge/brick_dc.rb +359 -0
- data/lib/tinkerforge/brick_imu.rb +512 -0
- data/lib/tinkerforge/brick_master.rb +1120 -0
- data/lib/tinkerforge/brick_servo.rb +475 -0
- data/lib/tinkerforge/brick_stepper.rb +556 -0
- data/lib/tinkerforge/bricklet_ambient_light.rb +246 -0
- data/lib/tinkerforge/bricklet_analog_in.rb +273 -0
- data/lib/tinkerforge/bricklet_analog_out.rb +90 -0
- data/lib/tinkerforge/bricklet_barometer.rb +313 -0
- data/lib/tinkerforge/bricklet_current12.rb +274 -0
- data/lib/tinkerforge/bricklet_current25.rb +274 -0
- data/lib/tinkerforge/bricklet_distance_ir.rb +274 -0
- data/lib/tinkerforge/bricklet_dual_relay.rb +127 -0
- data/lib/tinkerforge/bricklet_gps.rb +301 -0
- data/lib/tinkerforge/bricklet_humidity.rb +245 -0
- data/lib/tinkerforge/bricklet_industrial_digital_in_4.rb +165 -0
- data/lib/tinkerforge/bricklet_industrial_digital_out_4.rb +177 -0
- data/lib/tinkerforge/bricklet_industrial_quad_relay.rb +177 -0
- data/lib/tinkerforge/bricklet_io16.rb +237 -0
- data/lib/tinkerforge/bricklet_io4.rb +236 -0
- data/lib/tinkerforge/bricklet_joystick.rb +274 -0
- data/lib/tinkerforge/bricklet_lcd_16x2.rb +175 -0
- data/lib/tinkerforge/bricklet_lcd_20x4.rb +231 -0
- data/lib/tinkerforge/bricklet_linear_poti.rb +241 -0
- data/lib/tinkerforge/bricklet_piezo_buzzer.rb +84 -0
- data/lib/tinkerforge/bricklet_ptc.rb +277 -0
- data/lib/tinkerforge/bricklet_rotary_poti.rb +241 -0
- data/lib/tinkerforge/bricklet_temperature.rb +188 -0
- data/lib/tinkerforge/bricklet_temperature_ir.rb +275 -0
- data/lib/tinkerforge/bricklet_voltage.rb +241 -0
- data/lib/tinkerforge/bricklet_voltage_current.rb +386 -0
- data/lib/tinkerforge/ip_connection.rb +1027 -0
- data/lib/tinkerforge/version.rb +4 -0
- metadata +98 -0
@@ -0,0 +1,1027 @@
|
|
1
|
+
# -*- ruby encoding: utf-8 -*-
|
2
|
+
# Copyright (C) 2012-2013 Matthias Bolte <matthias@tinkerforge.com>
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms of this file,
|
5
|
+
# with or without modification, are permitted.
|
6
|
+
|
7
|
+
require 'socket'
|
8
|
+
require 'thread'
|
9
|
+
require 'timeout'
|
10
|
+
|
11
|
+
module Tinkerforge
|
12
|
+
class Base58
|
13
|
+
ALPHABET = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
|
14
|
+
|
15
|
+
def self.encode(value)
|
16
|
+
encoded = ''
|
17
|
+
while value >= 58
|
18
|
+
div, mod = value.divmod 58
|
19
|
+
encoded = ALPHABET[mod, 1] + encoded
|
20
|
+
value = div
|
21
|
+
end
|
22
|
+
ALPHABET[value, 1] + encoded
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.decode(encoded)
|
26
|
+
value = 0
|
27
|
+
base = 1
|
28
|
+
encoded.reverse.split(//).each do |c|
|
29
|
+
index = ALPHABET.index c
|
30
|
+
value += index * base
|
31
|
+
base *= 58
|
32
|
+
end
|
33
|
+
value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class TinkerforgeException < RuntimeError
|
38
|
+
end
|
39
|
+
|
40
|
+
class TimeoutException < TinkerforgeException
|
41
|
+
end
|
42
|
+
|
43
|
+
class AlreadyConnectedException < TinkerforgeException
|
44
|
+
end
|
45
|
+
|
46
|
+
class NotConnectedException < TinkerforgeException
|
47
|
+
end
|
48
|
+
|
49
|
+
class NotSupportedException < TinkerforgeException
|
50
|
+
end
|
51
|
+
|
52
|
+
def pack(unpacked, format)
|
53
|
+
data = ''
|
54
|
+
format.split(' ').each do |f|
|
55
|
+
if f.length > 1
|
56
|
+
f0 = f[0, 1]
|
57
|
+
f1 = f[1..-1]
|
58
|
+
r = []
|
59
|
+
|
60
|
+
if f0 == '?'
|
61
|
+
unpacked[0].each { |b| r << b ? 1 : 0 }
|
62
|
+
data += r.pack "C#{f1}"
|
63
|
+
elsif f0 == 'k'
|
64
|
+
unpacked[0].each { |c| r << c.ord }
|
65
|
+
data += r.pack "c#{f1}"
|
66
|
+
else
|
67
|
+
r = unpacked[0]
|
68
|
+
if ['s', 'S', 'l', 'L', 'q', 'Q'].count(f0) > 0
|
69
|
+
data += r.pack "#{f0}<#{f1}"
|
70
|
+
elsif f0 == 'Z'
|
71
|
+
data += [r].pack "#{f0}#{f1}"
|
72
|
+
else
|
73
|
+
data += r.pack "#{f0}#{f1}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
else
|
77
|
+
if f == '?'
|
78
|
+
r = [unpacked[0] ? 1 : 0]
|
79
|
+
data += r.pack 'C'
|
80
|
+
elsif f == 'k'
|
81
|
+
r = [unpacked[0].ord]
|
82
|
+
data += r.pack 'c'
|
83
|
+
else
|
84
|
+
r = [unpacked[0]]
|
85
|
+
if ['s', 'S', 'l', 'L', 'q', 'Q'].count(f) > 0
|
86
|
+
data += r.pack "#{f}<"
|
87
|
+
else
|
88
|
+
data += r.pack f
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
unpacked = unpacked.drop 1
|
94
|
+
end
|
95
|
+
|
96
|
+
data
|
97
|
+
end
|
98
|
+
|
99
|
+
def unpack(data, format)
|
100
|
+
unpacked = []
|
101
|
+
format.split(' ').each do |f|
|
102
|
+
if f.length > 1
|
103
|
+
f0 = f[0, 1]
|
104
|
+
f1 = f[1..-1]
|
105
|
+
u = []
|
106
|
+
|
107
|
+
if f0 == '?'
|
108
|
+
r = data.unpack "C#{f1}a*"
|
109
|
+
data = r[-1]
|
110
|
+
r.delete_at(-1)
|
111
|
+
r.each { |b| u << b != 0 }
|
112
|
+
elsif f0 == 'k'
|
113
|
+
r = data.unpack "c#{f1}a*"
|
114
|
+
data = r[-1]
|
115
|
+
r.delete_at(-1)
|
116
|
+
r.each { |c| u << c.chr }
|
117
|
+
else
|
118
|
+
if ['s', 'S', 'l', 'L', 'q', 'Q'].count(f0) > 0
|
119
|
+
r = data.unpack "#{f}<a*"
|
120
|
+
else
|
121
|
+
r = data.unpack "#{f}a*"
|
122
|
+
end
|
123
|
+
data = r[-1]
|
124
|
+
r.delete_at(-1)
|
125
|
+
r.each { |i| u << i }
|
126
|
+
end
|
127
|
+
|
128
|
+
if u.length == 1
|
129
|
+
u = u[0]
|
130
|
+
end
|
131
|
+
|
132
|
+
unpacked << u
|
133
|
+
else
|
134
|
+
r = []
|
135
|
+
u = nil
|
136
|
+
|
137
|
+
if f == '?'
|
138
|
+
r = data.unpack "Ca*"
|
139
|
+
u = r[0] != 0
|
140
|
+
elsif f == 'k'
|
141
|
+
r = data.unpack "ca*"
|
142
|
+
u = r[0].chr
|
143
|
+
else
|
144
|
+
if ['s', 'q', 'l', 'L', 'S', 'Q'].count(f) > 0
|
145
|
+
r = data.unpack "#{f}<a*"
|
146
|
+
else
|
147
|
+
r = data.unpack "#{f}a*"
|
148
|
+
end
|
149
|
+
u = r[0]
|
150
|
+
end
|
151
|
+
|
152
|
+
data = r[1]
|
153
|
+
unpacked << u
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
unpacked
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_uid_from_data(data)
|
161
|
+
data[0, 4].unpack('L<')[0]
|
162
|
+
end
|
163
|
+
|
164
|
+
def get_length_from_data(data)
|
165
|
+
data[4, 1].unpack('C')[0]
|
166
|
+
end
|
167
|
+
|
168
|
+
def get_function_id_from_data(data)
|
169
|
+
data[5, 1].unpack('C')[0]
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_sequence_number_from_data(data)
|
173
|
+
(data[6, 1].unpack('C')[0] >> 4) & 0x0F
|
174
|
+
end
|
175
|
+
|
176
|
+
def get_error_code_from_data(data)
|
177
|
+
(data[7, 1].unpack('C')[0] >> 6) & 0x03
|
178
|
+
end
|
179
|
+
|
180
|
+
class Device
|
181
|
+
RESPONSE_EXPECTED_INVALID_FUNCTION_ID = 0
|
182
|
+
RESPONSE_EXPECTED_ALWAYS_TRUE = 1 # getter
|
183
|
+
RESPONSE_EXPECTED_ALWAYS_FALSE = 2 # callback
|
184
|
+
RESPONSE_EXPECTED_TRUE = 3 # setter
|
185
|
+
RESPONSE_EXPECTED_FALSE = 4 # setter, default
|
186
|
+
|
187
|
+
attr_accessor :uid
|
188
|
+
attr_accessor :expected_response_function_id
|
189
|
+
attr_accessor :expected_response_sequence_number
|
190
|
+
attr_accessor :callback_formats
|
191
|
+
attr_accessor :registered_callbacks
|
192
|
+
|
193
|
+
# Creates the device object with the unique device ID <tt>uid</tt> and adds
|
194
|
+
# it to the IPConnection <tt>ipcon</tt>.
|
195
|
+
def initialize(uid, ipcon)
|
196
|
+
@uid = Base58.decode uid
|
197
|
+
|
198
|
+
if @uid > 0xFFFFFFFF
|
199
|
+
# convert from 64bit to 32bit
|
200
|
+
value1 = @uid & 0xFFFFFFFF
|
201
|
+
value2 = (@uid >> 32) & 0xFFFFFFFF
|
202
|
+
|
203
|
+
@uid = (value1 & 0x00000FFF)
|
204
|
+
@uid |= (value1 & 0x0F000000) >> 12
|
205
|
+
@uid |= (value2 & 0x0000003F) << 16
|
206
|
+
@uid |= (value2 & 0x000F0000) << 6
|
207
|
+
@uid |= (value2 & 0x3F000000) << 2
|
208
|
+
end
|
209
|
+
|
210
|
+
@api_version = [0, 0, 0]
|
211
|
+
|
212
|
+
@ipcon = ipcon
|
213
|
+
|
214
|
+
@request_mutex = Mutex.new
|
215
|
+
|
216
|
+
@response_expected = Array.new(256, RESPONSE_EXPECTED_INVALID_FUNCTION_ID)
|
217
|
+
@response_expected[IPConnection::FUNCTION_ENUMERATE] = RESPONSE_EXPECTED_ALWAYS_FALSE
|
218
|
+
@response_expected[IPConnection::CALLBACK_ENUMERATE] = RESPONSE_EXPECTED_ALWAYS_FALSE
|
219
|
+
|
220
|
+
@expected_response_function_id = 0
|
221
|
+
@expected_response_sequence_number = 0
|
222
|
+
|
223
|
+
@response_mutex = Mutex.new
|
224
|
+
@response_condition = ConditionVariable.new
|
225
|
+
@response_queue = Queue.new
|
226
|
+
|
227
|
+
@callback_formats = {}
|
228
|
+
@registered_callbacks = {}
|
229
|
+
|
230
|
+
@ipcon.devices[@uid] = self # FIXME: use a weakref here
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns the API version (major, minor, revision) of the bindings for
|
234
|
+
# this device.
|
235
|
+
def get_api_version
|
236
|
+
@api_version
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the response expected flag for the function specified by the
|
240
|
+
# <tt>function_id</tt> parameter. It is <tt>true</tt> if the function is
|
241
|
+
# expected to send a response, <tt>false</tt> otherwise.
|
242
|
+
#
|
243
|
+
# For getter functions this is enabled by default and cannot be disabled,
|
244
|
+
# because those functions will always send a response. For callback
|
245
|
+
# configuration functions it is enabled by default too, but can be
|
246
|
+
# disabled via the set_response_expected function. For setter functions it
|
247
|
+
# is disabled by default and can be enabled.
|
248
|
+
#
|
249
|
+
# Enabling the response expected flag for a setter function allows to
|
250
|
+
# detect timeouts and other error conditions calls of this setter as
|
251
|
+
# well. The device will then send a response for this purpose. If this
|
252
|
+
# flag is disabled for a setter function then no response is send and
|
253
|
+
# errors are silently ignored, because they cannot be detected.
|
254
|
+
def get_response_expected(function_id)
|
255
|
+
if function_id < 0 or function_id > 255
|
256
|
+
raise ArgumentError, "Function ID #{function_id} out of range"
|
257
|
+
end
|
258
|
+
|
259
|
+
flag = @response_expected[function_id]
|
260
|
+
|
261
|
+
if flag == RESPONSE_EXPECTED_INVALID_FUNCTION_ID
|
262
|
+
raise ArgumentError, "Invalid function ID #{function_id}"
|
263
|
+
end
|
264
|
+
|
265
|
+
if flag == RESPONSE_EXPECTED_ALWAYS_TRUE or \
|
266
|
+
flag == RESPONSE_EXPECTED_TRUE
|
267
|
+
true
|
268
|
+
else
|
269
|
+
false
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Changes the response expected flag of the function specified by the
|
274
|
+
# <tt>function_id</tt> parameter. This flag can only be changed for setter
|
275
|
+
# (default value: <tt>false</tt>) and callback configuration functions
|
276
|
+
# (default value: <tt>true</tt>). For getter functions it is always enabled
|
277
|
+
# and callbacks it is always disabled.
|
278
|
+
#
|
279
|
+
# Enabling the response expected flag for a setter function allows to
|
280
|
+
# detect timeouts and other error conditions calls of this setter as
|
281
|
+
# well. The device will then send a response for this purpose. If this
|
282
|
+
# flag is disabled for a setter function then no response is send and
|
283
|
+
# errors are silently ignored, because they cannot be detected.
|
284
|
+
def set_response_expected(function_id, response_expected)
|
285
|
+
if function_id < 0 or function_id > 255
|
286
|
+
raise ArgumentError, "Function ID #{function_id} out of range"
|
287
|
+
end
|
288
|
+
|
289
|
+
flag = @response_expected[function_id]
|
290
|
+
|
291
|
+
if flag == RESPONSE_EXPECTED_INVALID_FUNCTION_ID
|
292
|
+
raise ArgumentError, "Invalid function ID #{function_id}"
|
293
|
+
end
|
294
|
+
|
295
|
+
if flag == RESPONSE_EXPECTED_ALWAYS_TRUE or \
|
296
|
+
flag == RESPONSE_EXPECTED_ALWAYS_FALSE
|
297
|
+
raise ArgumentError, "Response Expected flag cannot be changed for function ID #{function_id}"
|
298
|
+
end
|
299
|
+
|
300
|
+
if response_expected
|
301
|
+
@response_expected[function_id] = RESPONSE_EXPECTED_TRUE
|
302
|
+
else
|
303
|
+
@response_expected[function_id] = RESPONSE_EXPECTED_FALSE
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Changes the response expected flag for all setter and callback
|
308
|
+
# configuration functions of this device at once.
|
309
|
+
def set_response_expected_all(response_expected)
|
310
|
+
if response_expected
|
311
|
+
flag = RESPONSE_EXPECTED_TRUE
|
312
|
+
else
|
313
|
+
flag = RESPONSE_EXPECTED_FALSE
|
314
|
+
end
|
315
|
+
|
316
|
+
for function_id in 0..255
|
317
|
+
if @response_expected[function_id] == RESPONSE_EXPECTED_TRUE or \
|
318
|
+
@response_expected[function_id] == RESPONSE_EXPECTED_FALSE
|
319
|
+
@response_expected[function_id] = flag
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# internal
|
325
|
+
def send_request(function_id, request_data, request_format,
|
326
|
+
response_length, response_format)
|
327
|
+
response = nil
|
328
|
+
|
329
|
+
if request_data.length > 0
|
330
|
+
payload = pack request_data, request_format
|
331
|
+
else
|
332
|
+
payload = ''
|
333
|
+
end
|
334
|
+
|
335
|
+
header, response_expected, sequence_number = \
|
336
|
+
@ipcon.create_packet_header self, 8 + payload.length, function_id
|
337
|
+
request = header + payload
|
338
|
+
|
339
|
+
if response_expected
|
340
|
+
packet = nil
|
341
|
+
|
342
|
+
@request_mutex.synchronize {
|
343
|
+
@expected_response_function_id = function_id
|
344
|
+
@expected_response_sequence_number = sequence_number
|
345
|
+
|
346
|
+
begin
|
347
|
+
@ipcon.send_request request
|
348
|
+
|
349
|
+
while true
|
350
|
+
packet = dequeue_response "Did not receive response in time for function ID #{function_id}"
|
351
|
+
|
352
|
+
if function_id == get_function_id_from_data(packet) and \
|
353
|
+
sequence_number == get_sequence_number_from_data(packet)
|
354
|
+
# ignore old responses that arrived after the timeout expired, but before setting
|
355
|
+
# expected_response_function_id and expected_response_sequence_number back to None
|
356
|
+
break
|
357
|
+
end
|
358
|
+
end
|
359
|
+
ensure
|
360
|
+
@expected_response_function_id = 0
|
361
|
+
@expected_response_sequence_number = 0
|
362
|
+
end
|
363
|
+
}
|
364
|
+
|
365
|
+
error_code = get_error_code_from_data(packet)
|
366
|
+
|
367
|
+
if error_code == 0
|
368
|
+
# no error
|
369
|
+
elsif error_code == 1
|
370
|
+
raise NotSupportedException, "Got invalid parameter for function ID #{function_id}"
|
371
|
+
elsif error_code == 2
|
372
|
+
raise NotSupportedException, "Function ID #{function_id} is not supported"
|
373
|
+
else
|
374
|
+
raise NotSupportedException, "Function ID #{function_id} returned an unknown error"
|
375
|
+
end
|
376
|
+
|
377
|
+
if response_length > 0
|
378
|
+
response = unpack packet[8..-1], response_format
|
379
|
+
|
380
|
+
if response.length == 1
|
381
|
+
response = response[0]
|
382
|
+
end
|
383
|
+
end
|
384
|
+
else
|
385
|
+
@ipcon.send_request request
|
386
|
+
end
|
387
|
+
|
388
|
+
response
|
389
|
+
end
|
390
|
+
|
391
|
+
# internal
|
392
|
+
def enqueue_response(response)
|
393
|
+
@response_mutex.synchronize {
|
394
|
+
@response_queue.push response
|
395
|
+
@response_condition.signal
|
396
|
+
}
|
397
|
+
end
|
398
|
+
|
399
|
+
# internal
|
400
|
+
def dequeue_response(message)
|
401
|
+
response = nil
|
402
|
+
|
403
|
+
@response_mutex.synchronize {
|
404
|
+
@response_condition.wait @response_mutex, @ipcon.timeout
|
405
|
+
|
406
|
+
if @response_queue.empty?
|
407
|
+
raise TimeoutException, message
|
408
|
+
end
|
409
|
+
|
410
|
+
response = @response_queue.pop
|
411
|
+
}
|
412
|
+
|
413
|
+
response
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# internal
|
418
|
+
class CallbackContext
|
419
|
+
attr_accessor :queue
|
420
|
+
attr_accessor :thread
|
421
|
+
attr_accessor :mutex
|
422
|
+
attr_accessor :packet_dispatch_allowed
|
423
|
+
|
424
|
+
def initialize
|
425
|
+
@queue = nil
|
426
|
+
@thread = nil
|
427
|
+
@mutex = nil
|
428
|
+
@packet_dispatch_allowed = false
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
class IPConnection
|
433
|
+
attr_accessor :devices
|
434
|
+
attr_accessor :timeout
|
435
|
+
|
436
|
+
CALLBACK_ENUMERATE = 253
|
437
|
+
CALLBACK_CONNECTED = 0
|
438
|
+
CALLBACK_DISCONNECTED = 1
|
439
|
+
|
440
|
+
# enumeration_type parameter for CALLBACK_ENUMERATE
|
441
|
+
ENUMERATION_TYPE_AVAILABLE = 0
|
442
|
+
ENUMERATION_TYPE_CONNECTED = 1
|
443
|
+
ENUMERATION_TYPE_DISCONNECTED = 2
|
444
|
+
|
445
|
+
# connect_reason parameter for CALLBACK_CONNECTED
|
446
|
+
CONNECT_REASON_REQUEST = 0
|
447
|
+
CONNECT_REASON_AUTO_RECONNECT = 1
|
448
|
+
|
449
|
+
# disconnect_reason parameter for CALLBACK_DISCONNECTED
|
450
|
+
DISCONNECT_REASON_REQUEST = 0
|
451
|
+
DISCONNECT_REASON_ERROR = 1
|
452
|
+
DISCONNECT_REASON_SHUTDOWN = 2
|
453
|
+
|
454
|
+
# returned by get_connection_state
|
455
|
+
CONNECTION_STATE_DISCONNECTED = 0
|
456
|
+
CONNECTION_STATE_CONNECTED = 1
|
457
|
+
CONNECTION_STATE_PENDING = 2 # auto-reconnect in progress
|
458
|
+
|
459
|
+
# Creates an IP Connection object that can be used to enumerate the
|
460
|
+
# available devices. It is also required for the constructor of Bricks
|
461
|
+
# and Bricklets.
|
462
|
+
def initialize
|
463
|
+
@host = nil
|
464
|
+
@port = 0
|
465
|
+
|
466
|
+
@timeout = 2.5
|
467
|
+
|
468
|
+
@auto_reconnect = true
|
469
|
+
@auto_reconnect_allowed = false
|
470
|
+
@auto_reconnect_pending = false
|
471
|
+
|
472
|
+
@next_sequence_number = 0
|
473
|
+
@sequence_number_mutex = Mutex.new
|
474
|
+
|
475
|
+
@devices = {}
|
476
|
+
|
477
|
+
@registered_callbacks = {}
|
478
|
+
|
479
|
+
@socket_mutex = Mutex.new
|
480
|
+
@socket = nil # protected by socket_mutex
|
481
|
+
@socket_id = 0 # protected by socket_mutex
|
482
|
+
|
483
|
+
@receive_flag = false
|
484
|
+
@receive_thread = nil
|
485
|
+
|
486
|
+
@callback = nil
|
487
|
+
|
488
|
+
@disconnect_probe_flag = false
|
489
|
+
@disconnect_probe_queue = nil
|
490
|
+
@disconnect_probe_thread = nil # protected by socket_mutex
|
491
|
+
|
492
|
+
@waiter_queue = Queue.new
|
493
|
+
end
|
494
|
+
|
495
|
+
# Creates a TCP/IP connection to the given <tt>host</tt> and <tt>port</tt>.
|
496
|
+
# The host and port can point to a Brick Daemon or to a WIFI/Ethernet
|
497
|
+
# Extension.
|
498
|
+
#
|
499
|
+
# Devices can only be controlled when the connection was established
|
500
|
+
# successfully.
|
501
|
+
#
|
502
|
+
# Blocks until the connection is established and throws an exception if
|
503
|
+
# there is no Brick Daemon or WIFI/Ethernet Extension listening at the
|
504
|
+
# given host and port.
|
505
|
+
def connect(host, port)
|
506
|
+
@socket_mutex.synchronize {
|
507
|
+
if @socket != nil
|
508
|
+
raise AlreadyConnectedException, "Already connected to #{@host}:#{@port}"
|
509
|
+
end
|
510
|
+
|
511
|
+
@host = host
|
512
|
+
@port = port
|
513
|
+
|
514
|
+
connect_unlocked false
|
515
|
+
}
|
516
|
+
end
|
517
|
+
|
518
|
+
# Disconnects the TCP/IP connection from the Brick Daemon or the
|
519
|
+
# WIFI/Ethernet Extension.
|
520
|
+
def disconnect
|
521
|
+
callback = nil
|
522
|
+
|
523
|
+
@socket_mutex.synchronize {
|
524
|
+
@auto_reconnect_allowed = false
|
525
|
+
|
526
|
+
if @auto_reconnect_pending
|
527
|
+
# Abort pending auto reconnect
|
528
|
+
@auto_reconnect_pending = false
|
529
|
+
else
|
530
|
+
if @socket == nil
|
531
|
+
raise NotConnectedException, 'Not connected'
|
532
|
+
end
|
533
|
+
|
534
|
+
disconnect_unlocked
|
535
|
+
end
|
536
|
+
|
537
|
+
# Destroy callback thread
|
538
|
+
callback = @callback
|
539
|
+
@callback = nil
|
540
|
+
}
|
541
|
+
|
542
|
+
# Do this outside of socket_mutex to allow calling (dis-)connect from
|
543
|
+
# the callbacks while blocking on the join call here
|
544
|
+
callback.queue.push [QUEUE_KIND_META, [CALLBACK_DISCONNECTED,
|
545
|
+
DISCONNECT_REASON_REQUEST, nil]]
|
546
|
+
callback.queue.push [QUEUE_KIND_EXIT, nil]
|
547
|
+
|
548
|
+
if Thread.current != callback.thread
|
549
|
+
callback.thread.join
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
# Can return the following states:
|
554
|
+
#
|
555
|
+
# - CONNECTION_STATE_DISCONNECTED: No connection is established.
|
556
|
+
# - CONNECTION_STATE_CONNECTED: A connection to the Brick Daemon or
|
557
|
+
# the WIFI/Ethernet Extension is established.
|
558
|
+
# - CONNECTION_STATE_PENDING: IP Connection is currently trying to
|
559
|
+
# connect.
|
560
|
+
def get_connection_state
|
561
|
+
if @socket != nil
|
562
|
+
CONNECTION_STATE_CONNECTED
|
563
|
+
elsif @auto_reconnect_pending
|
564
|
+
CONNECTION_STATE_PENDING
|
565
|
+
else
|
566
|
+
CONNECTION_STATE_DISCONNECTED
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# Enables or disables auto-reconnect. If auto-reconnect is enabled,
|
571
|
+
# the IP Connection will try to reconnect to the previously given
|
572
|
+
# host and port, if the connection is lost.
|
573
|
+
#
|
574
|
+
# Default value is *true*.
|
575
|
+
def set_auto_reconnect(auto_reconnect)
|
576
|
+
@auto_reconnect = auto_reconnect
|
577
|
+
|
578
|
+
if not @auto_reconnect
|
579
|
+
# Abort potentially pending auto reconnect
|
580
|
+
@auto_reconnect_allowed = false
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
# Returns <tt>true</tt> if auto-reconnect is enabled, <tt>false</tt>
|
585
|
+
# otherwise.
|
586
|
+
def get_auto_reconnect
|
587
|
+
@auto_reconnect
|
588
|
+
end
|
589
|
+
|
590
|
+
# Sets the timeout in seconds for getters and for setters for which
|
591
|
+
# the response expected flag is activated.
|
592
|
+
#
|
593
|
+
# Default timeout is 2.5.
|
594
|
+
def set_timeout(timeout)
|
595
|
+
@timeout = timeout
|
596
|
+
end
|
597
|
+
|
598
|
+
# Returns the timeout as set by set_timeout.
|
599
|
+
def get_timeout
|
600
|
+
@timeout
|
601
|
+
end
|
602
|
+
|
603
|
+
# Broadcasts an enumerate request. All devices will respond with an
|
604
|
+
# enumerate callback.
|
605
|
+
def enumerate
|
606
|
+
request, _, _ = create_packet_header nil, 8, FUNCTION_ENUMERATE
|
607
|
+
|
608
|
+
send_request request
|
609
|
+
end
|
610
|
+
|
611
|
+
# Stops the current thread until unwait is called.
|
612
|
+
#
|
613
|
+
# This is useful if you rely solely on callbacks for events, if you want
|
614
|
+
# to wait for a specific callback or if the IP Connection was created in
|
615
|
+
# a thread.
|
616
|
+
#
|
617
|
+
# Wait and unwait act in the same way as "acquire" and "release" of a
|
618
|
+
# semaphore.
|
619
|
+
def wait
|
620
|
+
@waiter_queue.pop
|
621
|
+
end
|
622
|
+
|
623
|
+
# Unwaits the thread previously stopped by wait.
|
624
|
+
#
|
625
|
+
# Wait and unwait act in the same way as "acquire" and "release" of a
|
626
|
+
# semaphore.
|
627
|
+
def unwait
|
628
|
+
@waiter_queue.push nil
|
629
|
+
end
|
630
|
+
|
631
|
+
# Registers a callback with ID <tt>id</tt> to the block <tt>block</tt>.
|
632
|
+
def register_callback(id, &block)
|
633
|
+
callback = block
|
634
|
+
@registered_callbacks[id] = callback
|
635
|
+
end
|
636
|
+
|
637
|
+
# internal
|
638
|
+
def get_next_sequence_number
|
639
|
+
@sequence_number_mutex.synchronize {
|
640
|
+
sequence_number = @next_sequence_number + 1
|
641
|
+
@next_sequence_number = sequence_number % 15
|
642
|
+
sequence_number
|
643
|
+
}
|
644
|
+
end
|
645
|
+
|
646
|
+
# internal
|
647
|
+
def create_packet_header(device, length, function_id)
|
648
|
+
uid = 0
|
649
|
+
sequence_number = get_next_sequence_number
|
650
|
+
response_expected = false
|
651
|
+
r_bit = 0
|
652
|
+
|
653
|
+
if device != nil
|
654
|
+
uid = device.uid
|
655
|
+
response_expected = device.get_response_expected function_id
|
656
|
+
end
|
657
|
+
|
658
|
+
if response_expected
|
659
|
+
r_bit = 1
|
660
|
+
end
|
661
|
+
|
662
|
+
sequence_number_and_options = (sequence_number << 4) | (r_bit << 3)
|
663
|
+
header = pack [uid, length, function_id, sequence_number_and_options, 0], 'L C C C C'
|
664
|
+
|
665
|
+
[header, response_expected, sequence_number]
|
666
|
+
end
|
667
|
+
|
668
|
+
def send_request(request)
|
669
|
+
@socket_mutex.synchronize {
|
670
|
+
if @socket == nil
|
671
|
+
raise NotConnectedException, 'Not connected'
|
672
|
+
end
|
673
|
+
|
674
|
+
begin
|
675
|
+
@socket.send request, 0
|
676
|
+
rescue IOError
|
677
|
+
handle_disconnect_by_peer DISCONNECT_REASON_ERROR, @socket_id, true
|
678
|
+
raise NotConnectedException, 'Not connected'
|
679
|
+
rescue Errno::ECONNRESET
|
680
|
+
handle_disconnect_by_peer DISCONNECT_REASON_SHUTDOWN, @socket_id, true
|
681
|
+
raise NotConnectedException, 'Not connected'
|
682
|
+
end
|
683
|
+
|
684
|
+
@disconnect_probe_flag = false
|
685
|
+
}
|
686
|
+
end
|
687
|
+
|
688
|
+
private
|
689
|
+
|
690
|
+
FUNCTION_DISCONNECT_PROBE = 128
|
691
|
+
FUNCTION_ENUMERATE = 254
|
692
|
+
|
693
|
+
QUEUE_KIND_EXIT = 0
|
694
|
+
QUEUE_KIND_META = 1
|
695
|
+
QUEUE_KIND_PACKET = 2
|
696
|
+
|
697
|
+
DISCONNECT_PROBE_INTERVAL = 5
|
698
|
+
|
699
|
+
# internal
|
700
|
+
def connect_unlocked(is_auto_reconnect)
|
701
|
+
# NOTE: Assumes that the socket mutex is locked
|
702
|
+
|
703
|
+
# Create callback queue and thread
|
704
|
+
if @callback == nil
|
705
|
+
@callback = CallbackContext.new
|
706
|
+
@callback.queue = Queue.new
|
707
|
+
@callback.mutex = Mutex.new
|
708
|
+
@callback.packet_dispatch_allowed = false
|
709
|
+
@callback.thread = Thread.new(@callback) do |callback|
|
710
|
+
callback_loop callback
|
711
|
+
end
|
712
|
+
@callback.thread.abort_on_exception = true
|
713
|
+
end
|
714
|
+
|
715
|
+
# Create socket
|
716
|
+
@socket = TCPSocket.new @host, @port
|
717
|
+
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
718
|
+
@socket_id += 1
|
719
|
+
|
720
|
+
# Create disconnect probe thread
|
721
|
+
@disconnect_probe_flag = true
|
722
|
+
@disconnect_probe_queue = Queue.new
|
723
|
+
@disconnect_probe_thread = Thread.new(@disconnect_probe_queue) do |disconnect_probe_queue|
|
724
|
+
disconnect_probe_loop disconnect_probe_queue
|
725
|
+
end
|
726
|
+
@disconnect_probe_thread.abort_on_exception = true
|
727
|
+
|
728
|
+
# Create receive thread
|
729
|
+
@callback.packet_dispatch_allowed = true
|
730
|
+
|
731
|
+
@receive_flag = true
|
732
|
+
@receive_thread = Thread.new(@socket_id) do |socket_id|
|
733
|
+
receive_loop socket_id
|
734
|
+
end
|
735
|
+
@receive_thread.abort_on_exception = true
|
736
|
+
|
737
|
+
# Trigger connected callback
|
738
|
+
if is_auto_reconnect
|
739
|
+
connect_reason = CONNECT_REASON_AUTO_RECONNECT
|
740
|
+
else
|
741
|
+
connect_reason = CONNECT_REASON_REQUEST
|
742
|
+
end
|
743
|
+
|
744
|
+
@auto_reconnect_allowed = false;
|
745
|
+
@auto_reconnect_pending = false;
|
746
|
+
|
747
|
+
@callback.queue.push [QUEUE_KIND_META, [CALLBACK_CONNECTED,
|
748
|
+
connect_reason, nil]]
|
749
|
+
end
|
750
|
+
|
751
|
+
# internal
|
752
|
+
def disconnect_unlocked
|
753
|
+
# NOTE: Assumes that the socket mutex is locked
|
754
|
+
|
755
|
+
# Destroy disconnect probe thread
|
756
|
+
@disconnect_probe_queue.push true
|
757
|
+
@disconnect_probe_thread.join
|
758
|
+
@disconnect_probe_thread = nil
|
759
|
+
|
760
|
+
# Stop dispatching packet callbacks before ending the receive
|
761
|
+
# thread to avoid timeout exceptions due to callback functions
|
762
|
+
# trying to call getters
|
763
|
+
if Thread.current != @callback.thread
|
764
|
+
# FIXME: Cannot lock callback mutex here because this can
|
765
|
+
# deadlock due to an ordering problem with the socket mutex
|
766
|
+
#@callback.mutex.synchronize {
|
767
|
+
@callback.packet_dispatch_allowed = false
|
768
|
+
#}
|
769
|
+
else
|
770
|
+
@callback.packet_dispatch_allowed = false
|
771
|
+
end
|
772
|
+
|
773
|
+
# Destroy receive thread
|
774
|
+
@receive_flag = false
|
775
|
+
|
776
|
+
@socket.shutdown(Socket::SHUT_RDWR)
|
777
|
+
|
778
|
+
if @receive_thread != nil
|
779
|
+
@receive_thread.join
|
780
|
+
@receive_thread = nil
|
781
|
+
end
|
782
|
+
|
783
|
+
# Destroy socket
|
784
|
+
@socket.close
|
785
|
+
@socket = nil
|
786
|
+
end
|
787
|
+
|
788
|
+
# internal
|
789
|
+
def receive_loop(socket_id)
|
790
|
+
pending_data = ''
|
791
|
+
|
792
|
+
while @receive_flag
|
793
|
+
begin
|
794
|
+
result = IO.select [@socket], [], [], 1
|
795
|
+
rescue IOError
|
796
|
+
# FIXME: handle this error?
|
797
|
+
break
|
798
|
+
end
|
799
|
+
|
800
|
+
if result == nil or result[0].length < 1
|
801
|
+
next
|
802
|
+
end
|
803
|
+
|
804
|
+
begin
|
805
|
+
data = @socket.recv 8192
|
806
|
+
rescue IOError
|
807
|
+
handle_disconnect_by_peer DISCONNECT_REASON_ERROR, socket_id, false
|
808
|
+
break
|
809
|
+
rescue Errno::ECONNRESET
|
810
|
+
handle_disconnect_by_peer DISCONNECT_REASON_SHUTDOWN, socket_id, false
|
811
|
+
break
|
812
|
+
end
|
813
|
+
|
814
|
+
if data.length == 0
|
815
|
+
if @receive_flag
|
816
|
+
handle_disconnect_by_peer DISCONNECT_REASON_SHUTDOWN, socket_id, false
|
817
|
+
end
|
818
|
+
break
|
819
|
+
end
|
820
|
+
|
821
|
+
pending_data += data
|
822
|
+
|
823
|
+
while true
|
824
|
+
if pending_data.length < 8
|
825
|
+
# Wait for complete header
|
826
|
+
break
|
827
|
+
end
|
828
|
+
|
829
|
+
length = get_length_from_data pending_data
|
830
|
+
|
831
|
+
if pending_data.length < length
|
832
|
+
# Wait for complete packet
|
833
|
+
break
|
834
|
+
end
|
835
|
+
|
836
|
+
packet = pending_data[0, length]
|
837
|
+
pending_data = pending_data[length..-1]
|
838
|
+
|
839
|
+
handle_response packet
|
840
|
+
end
|
841
|
+
end
|
842
|
+
end
|
843
|
+
|
844
|
+
# internal
|
845
|
+
def dispatch_meta(function_id, parameter, socket_id)
|
846
|
+
if function_id == CALLBACK_CONNECTED
|
847
|
+
if @registered_callbacks.has_key? CALLBACK_CONNECTED
|
848
|
+
@registered_callbacks[CALLBACK_CONNECTED].call parameter
|
849
|
+
end
|
850
|
+
elsif function_id == CALLBACK_DISCONNECTED
|
851
|
+
if parameter != DISCONNECT_REASON_REQUEST
|
852
|
+
# Need to do this here, the receive_loop is not allowed to
|
853
|
+
# hold the socket_mutex because this could cause a deadlock
|
854
|
+
# with a concurrent call to the (dis-)connect function
|
855
|
+
@socket_mutex.synchronize {
|
856
|
+
# Don't close the socket if it got disconnected or
|
857
|
+
# reconnected in the meantime
|
858
|
+
if @socket != nil and @socket_id == socket_id
|
859
|
+
# Destroy disconnect probe thread
|
860
|
+
@disconnect_probe_queue.push true
|
861
|
+
@disconnect_probe_thread.join
|
862
|
+
@disconnect_probe_thread = nil
|
863
|
+
|
864
|
+
# Destroy socket
|
865
|
+
@socket.close
|
866
|
+
@socket = nil
|
867
|
+
end
|
868
|
+
}
|
869
|
+
end
|
870
|
+
|
871
|
+
# FIXME: Wait a moment here, otherwise the next connect
|
872
|
+
# attempt will succeed, even if there is no open server
|
873
|
+
# socket. the first receive will then fail directly
|
874
|
+
sleep 0.1
|
875
|
+
|
876
|
+
if @registered_callbacks.has_key? CALLBACK_DISCONNECTED
|
877
|
+
@registered_callbacks[CALLBACK_DISCONNECTED].call parameter
|
878
|
+
end
|
879
|
+
|
880
|
+
if parameter != DISCONNECT_REASON_REQUEST and @auto_reconnect and @auto_reconnect_allowed
|
881
|
+
@auto_reconnect_pending = true
|
882
|
+
retry_connect = true
|
883
|
+
|
884
|
+
# Block here until reconnect. this is okay, there is no
|
885
|
+
# callback to deliver when there is no connection
|
886
|
+
while retry_connect
|
887
|
+
retry_connect = false
|
888
|
+
|
889
|
+
@socket_mutex.synchronize {
|
890
|
+
if @auto_reconnect_allowed and @socket == nil
|
891
|
+
begin
|
892
|
+
connect_unlocked true
|
893
|
+
rescue
|
894
|
+
retry_connect = true
|
895
|
+
end
|
896
|
+
else
|
897
|
+
@auto_reconnect_pending = false
|
898
|
+
end
|
899
|
+
}
|
900
|
+
|
901
|
+
if retry_connect
|
902
|
+
sleep 0.1
|
903
|
+
end
|
904
|
+
end
|
905
|
+
end
|
906
|
+
end
|
907
|
+
end
|
908
|
+
|
909
|
+
# internal
|
910
|
+
def dispatch_packet(packet)
|
911
|
+
uid = get_uid_from_data packet
|
912
|
+
length = get_length_from_data packet
|
913
|
+
function_id = get_function_id_from_data packet
|
914
|
+
|
915
|
+
if function_id == CALLBACK_ENUMERATE and \
|
916
|
+
@registered_callbacks.has_key? CALLBACK_ENUMERATE
|
917
|
+
payload = unpack packet[8..-1], 'Z8 Z8 k C3 C3 S C'
|
918
|
+
@registered_callbacks[CALLBACK_ENUMERATE].call(*payload)
|
919
|
+
elsif @devices.has_key? uid
|
920
|
+
device = @devices[uid]
|
921
|
+
|
922
|
+
if device.registered_callbacks.has_key? function_id
|
923
|
+
payload = unpack packet[8..-1], device.callback_formats[function_id]
|
924
|
+
device.registered_callbacks[function_id].call(*payload)
|
925
|
+
end
|
926
|
+
end
|
927
|
+
end
|
928
|
+
|
929
|
+
# internal
|
930
|
+
def callback_loop(callback)
|
931
|
+
alive = true
|
932
|
+
|
933
|
+
while alive
|
934
|
+
kind, data = callback.queue.pop
|
935
|
+
|
936
|
+
# FIXME: Cannot lock callback mutex here because this can
|
937
|
+
# deadlock due to an ordering problem with the socket mutex
|
938
|
+
# callback.mutex.synchronize {
|
939
|
+
if kind == QUEUE_KIND_EXIT
|
940
|
+
alive = false
|
941
|
+
elsif kind == QUEUE_KIND_META
|
942
|
+
function_id, parameter, socket_id = data
|
943
|
+
|
944
|
+
dispatch_meta function_id, parameter, socket_id
|
945
|
+
elsif kind == QUEUE_KIND_PACKET
|
946
|
+
# don't dispatch callbacks when the receive thread isn't running
|
947
|
+
if callback.packet_dispatch_allowed
|
948
|
+
dispatch_packet data
|
949
|
+
end
|
950
|
+
end
|
951
|
+
#}
|
952
|
+
end
|
953
|
+
end
|
954
|
+
|
955
|
+
# internal
|
956
|
+
def disconnect_probe_loop(disconnect_probe_queue)
|
957
|
+
request, _, _ = create_packet_header nil, 8, FUNCTION_DISCONNECT_PROBE
|
958
|
+
|
959
|
+
while true
|
960
|
+
begin
|
961
|
+
Timeout::timeout(DISCONNECT_PROBE_INTERVAL) {
|
962
|
+
disconnect_probe_queue.pop
|
963
|
+
}
|
964
|
+
rescue Timeout::Error
|
965
|
+
if @disconnect_probe_flag
|
966
|
+
@socket_mutex.synchronize {
|
967
|
+
begin
|
968
|
+
@socket.send request, 0
|
969
|
+
rescue IOError
|
970
|
+
handle_disconnect_by_peer DISCONNECT_REASON_ERROR, @socket_id, false
|
971
|
+
break
|
972
|
+
rescue Errno::ECONNRESET
|
973
|
+
handle_disconnect_by_peer DISCONNECT_REASON_SHUTDOWN, @socket_id, false
|
974
|
+
break
|
975
|
+
end
|
976
|
+
}
|
977
|
+
else
|
978
|
+
@disconnect_probe_flag = true
|
979
|
+
end
|
980
|
+
next
|
981
|
+
end
|
982
|
+
break
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
# internal
|
987
|
+
def handle_disconnect_by_peer(disconnect_reason, socket_id, disconnect_immediately)
|
988
|
+
# NOTE: assumes that socket_mutex is locked if disconnect_immediately is true
|
989
|
+
|
990
|
+
@auto_reconnect_allowed = true
|
991
|
+
|
992
|
+
if disconnect_immediately
|
993
|
+
disconnect_unlocked
|
994
|
+
end
|
995
|
+
|
996
|
+
@callback.queue.push [QUEUE_KIND_META, [CALLBACK_DISCONNECTED,
|
997
|
+
disconnect_reason, socket_id]]
|
998
|
+
end
|
999
|
+
|
1000
|
+
# internal
|
1001
|
+
def handle_response(packet)
|
1002
|
+
@disconnect_probe_flag = false
|
1003
|
+
|
1004
|
+
uid = get_uid_from_data packet
|
1005
|
+
function_id = get_function_id_from_data packet
|
1006
|
+
sequence_number = get_sequence_number_from_data packet
|
1007
|
+
|
1008
|
+
if sequence_number == 0 and function_id == CALLBACK_ENUMERATE
|
1009
|
+
if @registered_callbacks.has_key? CALLBACK_ENUMERATE
|
1010
|
+
@callback.queue.push [QUEUE_KIND_PACKET, packet]
|
1011
|
+
end
|
1012
|
+
elsif @devices.has_key? uid
|
1013
|
+
device = @devices[uid]
|
1014
|
+
|
1015
|
+
if sequence_number == 0
|
1016
|
+
if device.registered_callbacks.has_key? function_id
|
1017
|
+
@callback.queue.push [QUEUE_KIND_PACKET, packet]
|
1018
|
+
end
|
1019
|
+
elsif device.expected_response_function_id == function_id and \
|
1020
|
+
device.expected_response_sequence_number == sequence_number
|
1021
|
+
device.enqueue_response packet
|
1022
|
+
else
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
end
|
1026
|
+
end
|
1027
|
+
end
|