tinkerforge 2.0.7

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