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.
Files changed (35) hide show
  1. data/lib/tinkerforge.rb +5 -0
  2. data/lib/tinkerforge/brick_dc.rb +359 -0
  3. data/lib/tinkerforge/brick_imu.rb +512 -0
  4. data/lib/tinkerforge/brick_master.rb +1120 -0
  5. data/lib/tinkerforge/brick_servo.rb +475 -0
  6. data/lib/tinkerforge/brick_stepper.rb +556 -0
  7. data/lib/tinkerforge/bricklet_ambient_light.rb +246 -0
  8. data/lib/tinkerforge/bricklet_analog_in.rb +273 -0
  9. data/lib/tinkerforge/bricklet_analog_out.rb +90 -0
  10. data/lib/tinkerforge/bricklet_barometer.rb +313 -0
  11. data/lib/tinkerforge/bricklet_current12.rb +274 -0
  12. data/lib/tinkerforge/bricklet_current25.rb +274 -0
  13. data/lib/tinkerforge/bricklet_distance_ir.rb +274 -0
  14. data/lib/tinkerforge/bricklet_dual_relay.rb +127 -0
  15. data/lib/tinkerforge/bricklet_gps.rb +301 -0
  16. data/lib/tinkerforge/bricklet_humidity.rb +245 -0
  17. data/lib/tinkerforge/bricklet_industrial_digital_in_4.rb +165 -0
  18. data/lib/tinkerforge/bricklet_industrial_digital_out_4.rb +177 -0
  19. data/lib/tinkerforge/bricklet_industrial_quad_relay.rb +177 -0
  20. data/lib/tinkerforge/bricklet_io16.rb +237 -0
  21. data/lib/tinkerforge/bricklet_io4.rb +236 -0
  22. data/lib/tinkerforge/bricklet_joystick.rb +274 -0
  23. data/lib/tinkerforge/bricklet_lcd_16x2.rb +175 -0
  24. data/lib/tinkerforge/bricklet_lcd_20x4.rb +231 -0
  25. data/lib/tinkerforge/bricklet_linear_poti.rb +241 -0
  26. data/lib/tinkerforge/bricklet_piezo_buzzer.rb +84 -0
  27. data/lib/tinkerforge/bricklet_ptc.rb +277 -0
  28. data/lib/tinkerforge/bricklet_rotary_poti.rb +241 -0
  29. data/lib/tinkerforge/bricklet_temperature.rb +188 -0
  30. data/lib/tinkerforge/bricklet_temperature_ir.rb +275 -0
  31. data/lib/tinkerforge/bricklet_voltage.rb +241 -0
  32. data/lib/tinkerforge/bricklet_voltage_current.rb +386 -0
  33. data/lib/tinkerforge/ip_connection.rb +1027 -0
  34. data/lib/tinkerforge/version.rb +4 -0
  35. 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