flic 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +51 -49
  3. data/Rakefile +6 -1
  4. data/flic.gemspec +1 -0
  5. data/lib/flic.rb +1 -0
  6. data/lib/flic/blocker.rb +59 -0
  7. data/lib/flic/client.rb +12 -7
  8. data/lib/flic/client/scan_wizard.rb +4 -0
  9. data/lib/flic/protocol.rb +20 -6
  10. data/lib/flic/protocol/commands.rb +7 -0
  11. data/lib/flic/protocol/commands/cancel_scan_wizard.rb +1 -3
  12. data/lib/flic/protocol/commands/change_mode_parameters.rb +1 -3
  13. data/lib/flic/protocol/commands/command.rb +10 -4
  14. data/lib/flic/protocol/commands/create_connection_channel.rb +1 -3
  15. data/lib/flic/protocol/commands/create_scan_wizard.rb +1 -3
  16. data/lib/flic/protocol/commands/create_scanner.rb +1 -3
  17. data/lib/flic/protocol/commands/force_disconnect.rb +0 -2
  18. data/lib/flic/protocol/commands/get_button_uuid.rb +0 -2
  19. data/lib/flic/protocol/commands/get_info.rb +0 -1
  20. data/lib/flic/protocol/commands/ping.rb +1 -3
  21. data/lib/flic/protocol/commands/remove_connection_channel.rb +1 -3
  22. data/lib/flic/protocol/commands/remove_scanner.rb +1 -3
  23. data/lib/flic/protocol/connection.rb +22 -19
  24. data/lib/flic/protocol/events.rb +9 -2
  25. data/lib/flic/protocol/events/advertisement_packet.rb +2 -4
  26. data/lib/flic/protocol/events/bluetooth_controller_state_change.rb +0 -2
  27. data/lib/flic/protocol/events/button_click_or_hold.rb +2 -4
  28. data/lib/flic/protocol/events/button_single_or_double_click.rb +2 -4
  29. data/lib/flic/protocol/events/button_single_or_double_click_or_hold.rb +2 -4
  30. data/lib/flic/protocol/events/button_up_or_down.rb +2 -4
  31. data/lib/flic/protocol/events/connection_channel_removed.rb +1 -3
  32. data/lib/flic/protocol/events/connection_status_changed.rb +1 -3
  33. data/lib/flic/protocol/events/create_connection_channel_response.rb +1 -3
  34. data/lib/flic/protocol/events/event.rb +11 -5
  35. data/lib/flic/protocol/events/get_button_uuid_response.rb +0 -2
  36. data/lib/flic/protocol/events/get_info_response.rb +4 -6
  37. data/lib/flic/protocol/events/got_space_for_new_connection.rb +1 -3
  38. data/lib/flic/protocol/events/new_verified_button.rb +0 -2
  39. data/lib/flic/protocol/events/no_space_for_new_connection.rb +1 -3
  40. data/lib/flic/protocol/events/ping_response.rb +1 -3
  41. data/lib/flic/protocol/events/scan_wizard_button_connected.rb +1 -3
  42. data/lib/flic/protocol/events/scan_wizard_completed.rb +1 -3
  43. data/lib/flic/protocol/events/scan_wizard_found_private_button.rb +1 -3
  44. data/lib/flic/protocol/events/scan_wizard_found_public_button.rb +1 -3
  45. data/lib/flic/protocol/packet_header.rb +2 -2
  46. data/lib/flic/protocol/primitives.rb +1 -0
  47. data/lib/flic/protocol/primitives/bluetooth_address.rb +2 -1
  48. data/lib/flic/protocol/primitives/bluetooth_address_type.rb +3 -0
  49. data/lib/flic/protocol/primitives/bluetooth_controller_state.rb +7 -3
  50. data/lib/flic/protocol/primitives/boolean.rb +1 -0
  51. data/lib/flic/protocol/primitives/click_type.rb +13 -6
  52. data/lib/flic/protocol/primitives/connection_status.rb +7 -4
  53. data/lib/flic/protocol/primitives/create_connection_channel_error.rb +4 -2
  54. data/lib/flic/protocol/primitives/device_name.rb +2 -1
  55. data/lib/flic/protocol/primitives/disconnect_reason.rb +8 -4
  56. data/lib/flic/protocol/primitives/disconnect_time.rb +8 -9
  57. data/lib/flic/protocol/primitives/enum.rb +12 -1
  58. data/lib/flic/protocol/primitives/latency_mode.rb +7 -3
  59. data/lib/flic/protocol/primitives/removed_reason.rb +14 -7
  60. data/lib/flic/protocol/primitives/scan_wizard_result.rb +15 -8
  61. data/lib/flic/protocol/primitives/uuid.rb +13 -3
  62. data/lib/flic/simple_client.rb +116 -78
  63. data/lib/flic/version.rb +1 -1
  64. metadata +17 -2
@@ -6,9 +6,7 @@ module Flic
6
6
  module Protocol
7
7
  module Events
8
8
  class ScanWizardCompleted < Event
9
- endian :little
10
-
11
- uint32 :scan_wizard_id
9
+ uint32le :scan_wizard_id
12
10
  scan_wizard_result :scan_wizard_result
13
11
  end
14
12
  end
@@ -5,9 +5,7 @@ module Flic
5
5
  module Protocol
6
6
  module Events
7
7
  class ScanWizardFoundPrivateButton < Event
8
- endian :little
9
-
10
- uint32 :scan_wizard_id
8
+ uint32le :scan_wizard_id
11
9
  end
12
10
  end
13
11
  end
@@ -7,9 +7,7 @@ module Flic
7
7
  module Protocol
8
8
  module Events
9
9
  class ScanWizardFoundPublicButton < Event
10
- endian :little
11
-
12
- uint32 :scan_wizard_id
10
+ uint32le :scan_wizard_id
13
11
 
14
12
  bluetooth_address :bluetooth_address
15
13
  device_name :name
@@ -4,9 +4,9 @@ require 'bindata'
4
4
 
5
5
  module Flic
6
6
  module Protocol
7
+ # Every packet starts with a packet header that includes the length of the remaining packet
7
8
  class PacketHeader < BinData::Record
8
- endian :little
9
- uint16 :byte_length
9
+ uint16le :byte_length
10
10
  end
11
11
  end
12
12
  end
@@ -2,6 +2,7 @@ require 'flic/protocol'
2
2
 
3
3
  module Flic
4
4
  module Protocol
5
+ # A namespace module for all of the primitive classes
5
6
  module Primitives
6
7
  autoload :BluetoothAddress, 'flic/protocol/primitives/bluetooth_address'
7
8
  autoload :BluetoothAddressType, 'flic/protocol/primitives/bluetooth_address_type'
@@ -6,11 +6,12 @@ require 'scanf'
6
6
  module Flic
7
7
  module Protocol
8
8
  module Primitives
9
+ # A bluetooth address (bdaddr_t) is encoded in little endan, 6 bytes in total. When such an address is written as a string, it is normally written in big endian, where each byte is encoded in hex and colon as separator for each byte. For example, the address 08:09:0a:0b:0c:0d is encoded as the bytes 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08.
9
10
  class BluetoothAddress < BinData::Primitive
10
11
  PRINTF_FORMAT_STRING = '%.2X:%.2X:%.2X:%.2X:%.2X:%.2X'.freeze
11
12
  SCANF_FORMAT_STRING = '%X:%X:%X:%X:%X:%X'.freeze
12
13
 
13
- array :little_endian_octets, type: :uint8, initial_length: 6
14
+ array :little_endian_octets, type: :uint8le, initial_length: 6
14
15
 
15
16
  def get
16
17
  sprintf(PRINTF_FORMAT_STRING, *big_endian_octets)
@@ -4,6 +4,9 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # The server can be configured to either use the burnt-in public address stored inside the bluetooth controller, or to use a custom random static address. This custom address is a good idea if you want to be able to use your database with bonding information with a different bluetooth controller.
8
+ # [:public_bluetooth_address_type] burnt-in public address
9
+ # [:random_bluetooth_address_type] another address
7
10
  class BluetoothAddressType < Enum
8
11
  option :public_bluetooth_address_type
9
12
  option :random_bluetooth_address_type
@@ -4,10 +4,14 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # The server software detects when the bluetooth controller is removed or is made unavailable. It will then repeatedly retry to re-established a connection to the same bluetooth controller.
8
+ # [:detached] The server software has lost the HCI socket to the bluetooth controller and is trying to reconnect.
9
+ # [:resetting] The server software has just got connected to the HCI socket and initiated a reset of the bluetooth controller.
10
+ # [:attached] The bluetooth controller has done initialization and is up and running.
7
11
  class BluetoothControllerState < Enum
8
- option :detached # The server software has lost the HCI socket to the bluetooth controller and is trying to reconnect.
9
- option :resetting # The server software has just got connected to the HCI socket and initiated a reset of the bluetooth controller.
10
- option :attached # The bluetooth controller has done initialization and is up and running.
12
+ option :detached
13
+ option :resetting
14
+ option :attached
11
15
  end
12
16
  end
13
17
  end
@@ -3,6 +3,7 @@ require 'flic/protocol/primitives'
3
3
  module Flic
4
4
  module Protocol
5
5
  module Primitives
6
+ # True or false encoded as a byte where 0x00 is false and all other values are true (however, 0x01 is preferred)
6
7
  class Boolean < BinData::Primitive
7
8
  uint8 :byte
8
9
 
@@ -4,13 +4,20 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # The type of click registered by a button
8
+ # [:button_down] The button was pressed.
9
+ # [:button_up] The button was released.
10
+ # [:button_click] The button was clicked, and was held for at most 1 second between press and release.
11
+ # [:button_single_click] The button was clicked once.
12
+ # [:button_double_click] The button was clicked twice. The time between the first and second press must be at most 0.5 seconds.
13
+ # [:button_hold] The button was held for at least 1 second.
7
14
  class ClickType < Enum
8
- option :button_down # The button was pressed.
9
- option :button_up # The button was released.
10
- option :button_click # The button was clicked, and was held for at most 1 second between press and release.
11
- option :button_single_click # The button was clicked once.
12
- option :button_double_click # The button was clicked twice. The time between the first and second press must be at most 0.5 seconds.
13
- option :button_hold # The button was held for at least 1 second.
15
+ option :button_down
16
+ option :button_up
17
+ option :button_click
18
+ option :button_single_click
19
+ option :button_double_click
20
+ option :button_hold
14
21
  end
15
22
  end
16
23
  end
@@ -4,10 +4,13 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
- class ConnectionStatus < Enum
8
- option :disconnected # Not currently an established connection, but will connect as soon as the button is pressed and it is in range as long as the connection channel hasn't been removed (and unless maximum number of concurrent connections has been reached or the bluetooth controller has been detached).
9
- option :connected # The physical bluetooth connection has just been established and the server and the button are currently verifying each other. As soon as this is done, it will switch to the ready status.
10
- option :ready # The verification is done and button events may now arrive.
7
+ # [:disconnected] Not currently an established connection, but will connect as soon as the button is pressed and it is in range as long as the connection channel hasn't been removed (and unless maximum number of concurrent connections has been reached or the bluetooth controller has been detached).
8
+ # [:connected] The physical bluetooth connection has just been established and the server and the button are currently verifying each other. As soon as this is done, it will switch to the ready status.
9
+ # [:ready] The verification is done and button events may now arrive.
10
+ class ConnectionStatus < Enum
11
+ option :disconnected
12
+ option :connected
13
+ option :ready
11
14
  end
12
15
  end
13
16
  end
@@ -4,9 +4,11 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # :no_error - There were space in the bluetooth controller's white list to accept a physical pending connection for this button
8
+ # :maximum_pending_connections_reached - There were no space left in the bluetooth controller to allow a new pending connection
7
9
  class CreateConnectionChannelError < Enum
8
- option :no_error # There were space in the bluetooth controller's white list to accept a physical pending connection for this button
9
- option :maximum_pending_connections_reached # There were no space left in the bluetooth controller to allow a new pending connection
10
+ option :no_error
11
+ option :maximum_pending_connections_reached
10
12
  end
11
13
  end
12
14
  end
@@ -5,11 +5,12 @@ require 'bindata'
5
5
  module Flic
6
6
  module Protocol
7
7
  module Primitives
8
+ # The name of a device (up to 16 character string)
8
9
  class DeviceName < BinData::Primitive
9
10
  BYTE_LENGTH = 16
10
11
 
11
12
  uint8 :byte_length
12
- array :bytes, type: :int8, initial_length: BYTE_LENGTH
13
+ array :bytes, type: :int8le, initial_length: BYTE_LENGTH
13
14
 
14
15
  def get
15
16
  ''.tap do |string|
@@ -4,11 +4,15 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # [:unspecified] Unknown reason
8
+ # [:connection_establishment_failed] The bluetooth controller established a connection, but the Flic button didn't answer in time.
9
+ # [:timed_out] The connection to the Flic button was lost due to either being out of range or some radio communication problems.
10
+ # [:bonding_keys_mismatch] The server and the Flic button for some reason don't agree on the previously established bonding keys.
7
11
  class DisconnectReason < Enum
8
- option :unspecified # Unknown reason
9
- option :connection_establishment_failed # The bluetooth controller established a connection, but the Flic button didn't answer in time.
10
- option :timed_out # The connection to the Flic button was lost due to either being out of range or some radio communication problems.
11
- option :bonding_keys_mismatch # The server and the Flic button for some reason don't agree on the previously established bonding keys.
12
+ option :unspecified
13
+ option :connection_establishment_failed
14
+ option :timed_out
15
+ option :bonding_keys_mismatch
12
16
  end
13
17
  end
14
18
  end
@@ -3,13 +3,12 @@ require 'flic/protocol/primitives'
3
3
  module Flic
4
4
  module Protocol
5
5
  module Primitives
6
+ # Time in seconds after the Flic button may disconnect after the latest press or release. The button will reconnect automatically when it is later pressed again and deliver its enqueued events. Valid values are 0 - 511.
6
7
  class DisconnectTime < BinData::Primitive
7
- endian :little
8
-
9
- uint16 :time, initial_value: 512
8
+ uint16le :time, initial_value: 511
10
9
 
11
10
  def get
12
- if time == 512
11
+ if time == 511
13
12
  nil
14
13
  else
15
14
  time
@@ -17,12 +16,12 @@ module Flic
17
16
  end
18
17
 
19
18
  def set(value)
20
- if value == 512
21
- raise RangeError, '512 is a special value that cannot be used for disconnect_time'
22
- elsif value
23
- self.time = value
19
+ if value == nil
20
+ self.time = 511
21
+ elsif value >= 511
22
+ raise RangeError, 'disconnect_time must be less than 511 seconds (or nil for never)'
24
23
  else
25
- self.time = 512
24
+ self.time = value
26
25
  end
27
26
  end
28
27
  end
@@ -5,32 +5,39 @@ require 'bindata'
5
5
  module Flic
6
6
  module Protocol
7
7
  module Primitives
8
+ # An abstract class for 1 byte enums
8
9
  class Enum < BinData::Primitive
9
10
  class Error < StandardError; end
10
11
  class InvalidOptionError < Error; end
11
12
  class InvalidOctetError < Error; end
12
13
 
13
14
  class << self
15
+ # @return [Hash] a map of options to byte values
14
16
  def option_octet
15
17
  @option_octet ||= {}
16
18
  end
17
19
 
20
+ # @return [Hash] a map of byte values to options
18
21
  def octet_option
19
22
  @octet_option ||= {}
20
23
  end
21
24
 
25
+ # @return [Array] the valid options for this enum
22
26
  def options
23
27
  option_octet.keys
24
28
  end
25
29
 
30
+ # @return [Array] the valid byte values for this enum
26
31
  def octets
27
32
  octet_option.keys
28
33
  end
29
34
 
35
+ # @return [Integer] the byte value for the option with the largest byte value
30
36
  def max_octet
31
37
  octets.max
32
38
  end
33
39
 
40
+ # @return [Integer] the next available byte value (starting at 0x00)
34
41
  def next_available_octet
35
42
  if max_octet
36
43
  1 + max_octet
@@ -41,18 +48,22 @@ module Flic
41
48
 
42
49
  private
43
50
 
51
+ # Associates an option with a byte value of the enum
52
+ # @param option [Symbol]
53
+ # @param octet [Integer] (defaults to the next available byte value)
44
54
  def option(option, octet = next_available_octet)
45
55
  option_octet[option] = octet
46
56
  octet_option[octet] = option
47
57
  end
48
58
 
59
+ # Associates a byte value of an enum with an option
49
60
  def octet(octet, option)
50
61
  octet_option[octet] = option
51
62
  option_octet[option] = octet
52
63
  end
53
64
  end
54
65
 
55
- uint8 :octet
66
+ uint8le :octet
56
67
 
57
68
  def get
58
69
  if octet_option.has_key?(octet)
@@ -4,10 +4,14 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # This specifies the accepted latency mode for the corresponding connection channel. The physical bluetooth connection will use the lowest mode set by any connection channel. The battery usage for the Flic button is normally about the same for all modes if the connection is stable. However lower modes will have higher battery usage if the connection is unstable. Lower modes also consumes more power for the client, which is normally not a problem since most computers run on wall power or have large batteries.
8
+ # [:normal] Up to 100 ms latency.
9
+ # [:low] Up to 17.5 ms latency.
10
+ # [:high] Up to 275 ms latency.
7
11
  class LatencyMode < Enum
8
- option :normal # Up to 100 ms latency.
9
- option :low # Up to 17.5 ms latency.
10
- option :high # Up to 275 ms latency.
12
+ option :normal
13
+ option :low
14
+ option :high
11
15
  end
12
16
  end
13
17
  end
@@ -4,14 +4,21 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
+ # [:removed_by_this_client] The connection channel was removed by this client.
8
+ # [:force_disconnected_by_this_client] The connection channel was removed due to a force disconnect by this client.
9
+ # [:force_disconnected_by_other_client] Another client force disconnected the button used in this connection channel.
10
+ # [:button_is_private] The button is not in public mode. Hold it down for 7 seconds while not trying to establish a connection, then try to reconnect by creating a new connection channel.
11
+ # [:verify_timeout] After the connection was established, the bonding procedure didn't complete in time.
12
+ # [:internet_backend_error] The internet request to the Flic backend failed.
13
+ # [:invalid_data] According to the Flic backend, this Flic button supplied invalid identity data.
7
14
  class RemovedReason < Enum
8
- option :removed_by_this_client # The connection channel was removed by this client.
9
- option :force_disconnected_by_this_client # The connection channel was removed due to a force disconnect by this client.
10
- option :force_disconnected_by_other_client # Another client force disconnected the button used in this connection channel.
11
- option :button_is_private # The button is not in public mode. Hold it down for 7 seconds while not trying to establish a connection, then try to reconnect by creating a new connection channel.
12
- option :verify_timeout # After the connection was established, the bonding procedure didn't complete in time.
13
- option :internet_backend_error # The internet request to the Flic backend failed.
14
- option :invalid_data # According to the Flic backend, this Flic button supplied invalid identity data.
15
+ option :removed_by_this_client
16
+ option :force_disconnected_by_this_client
17
+ option :force_disconnected_by_other_client
18
+ option :button_is_private
19
+ option :verify_timeout
20
+ option :internet_backend_error
21
+ option :invalid_data
15
22
  end
16
23
  end
17
24
  end
@@ -4,14 +4,21 @@ require 'flic/protocol/primitives/enum'
4
4
  module Flic
5
5
  module Protocol
6
6
  module Primitives
7
- class ScanWizardResult < Enum
8
- option :success # Indicates that a button was successfully paired and verified. You may now create a connection channel to that button.
9
- option :cancelled_by_user # A CmdCancelScanWizard was sent.
10
- option :timeout # The scan wizard did not make any progress for some time. Current timeouts are 20 seconds for finding any button, 20 seconds for finding a public button (in case of a private button was found), 10 seconds for connecting the button, 30 seconds for pairing and verifying the button.
11
- option :button_private # First the button was advertising public status, but after connecting it reports private. Probably it switched from public to private just when the connection attempt was started.
12
- option :bluetooth_unavailable # The bluetooth controller is not attached.
13
- option :internet_backend_error # The internet request to the Flic backend failed.
14
- option :invalid_data # According to the Flic backend, this Flic button supplied invalid identity data.
7
+ # [:success] Indicates that a button was successfully paired and verified. You may now create a connection channel to that button.
8
+ # [:cancelled_by_user] A CmdCancelScanWizard was sent.
9
+ # [:timeout] The scan wizard did not make any progress for some time. Current timeouts are 20 seconds for finding any button, 20 seconds for finding a public button (in case of a private button was found), 10 seconds for connecting the button, 30 seconds for pairing and verifying the button.
10
+ # [:button_private] First the button was advertising public status, but after connecting it reports private. Probably it switched from public to private just when the connection attempt was started.
11
+ # [:bluetooth_unavailable] The bluetooth controller is not attached.
12
+ # [:internet_backend_error] The internet request to the Flic backend failed.
13
+ # [:invalid_data] According to the Flic backend, this Flic button supplied invalid identity data.
14
+ class ScanWizardResult < Enum
15
+ option :success
16
+ option :cancelled_by_user
17
+ option :timeout
18
+ option :button_private
19
+ option :bluetooth_unavailable
20
+ option :internet_backend_error
21
+ option :invalid_data
15
22
  end
16
23
  end
17
24
  end
@@ -6,18 +6,28 @@ require 'scanf'
6
6
  module Flic
7
7
  module Protocol
8
8
  module Primitives
9
+ # The uuid of a button (nil is represented as all zeros)
9
10
  class Uuid < BinData::Primitive
11
+ NULL_UUID_OCTETS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].freeze
10
12
  PRINTF_FORMAT_STRING = '%.2X%.2X%.2X%.2X-%.2X%.2X-%.2X%.2X-%.2X%.2X-%.2X%.2X%.2X%.2X%.2X%.2X'.freeze
11
13
  SCANF_FORMAT_STRING = '%X%X%X%X-%X%X-%X%X-%X%X-%X%X%X%X%X%X'.freeze
12
14
 
13
- array :octets, type: :uint8, initial_length: 16
15
+ array :octets, type: :uint8le, initial_length: 16
14
16
 
15
17
  def get
16
- sprintf(PRINTF_FORMAT_STRING, *octets)
18
+ if NULL_UUID_OCTETS == octets
19
+ nil
20
+ else
21
+ sprintf(PRINTF_FORMAT_STRING, *octets)
22
+ end
17
23
  end
18
24
 
19
25
  def set(value)
20
- self.octets = value.scanf(SCANF_FORMAT_STRING)
26
+ if value
27
+ self.octets = value.scanf(SCANF_FORMAT_STRING)
28
+ else
29
+ self.octets = NULL_UUID_OCTETS
30
+ end
21
31
  end
22
32
  end
23
33
  end
@@ -5,121 +5,159 @@ require 'thread'
5
5
  module Flic
6
6
  class SimpleClient
7
7
  class Error < StandardError; end
8
- class ConnectionChannelRemoved; end
9
-
10
- attr_reader :client
8
+ class Shutdown < Error; end
9
+ class ConnectionChannelRemoved < Error; end
10
+ class ButtonIsPrivateError < Error; end
11
11
 
12
- def initialize(*client_args)
13
- @semaphore = Mutex.new
14
- @client = Client.new(*client_args)
12
+ attr_reader :host, :port, :thread
13
+
14
+ def initialize(host = 'localhost', port = 5551)
15
+ @host, @port = host, port
16
+
17
+ @blocker = Blocker.new
18
+ @client = Client.new(host, port)
19
+
20
+ @listen_queues_semaphore = Mutex.new
21
+ @listen_queues = []
22
+
23
+ @thread = Thread.new do
24
+ begin
25
+ @client.enter_main_loop
26
+ rescue Client::Shutdown
27
+ nil
28
+ ensure
29
+ shutdown
30
+ end
31
+ end
32
+
33
+ @is_shutdown = false
34
+ end
35
+
36
+ def shutdown?
37
+ @is_shutdown
15
38
  end
16
39
 
17
40
  def shutdown
18
- @semaphore.synchronize do
19
- client.shutdown
41
+ @listen_queues_semaphore.synchronize do
42
+ unless @listen_queues.frozen?
43
+ @listen_queues.each { |queue| queue << :shutdown }.clear
44
+ @listen_queues.freeze
45
+ end
20
46
  end
47
+
48
+ @blocker.unblock_all! Shutdown, 'The client has shutdown'
49
+
50
+ @client.shutdown
51
+
52
+ @thread.join unless Thread.current == @thread
53
+
54
+ @is_shutdown = true
21
55
  end
22
56
 
23
57
  def buttons
24
- @semaphore.synchronize do
25
- server_info = process_events_until do |callback|
26
- client.get_info(&callback)
58
+ @blocker.block_until_callback do |callback|
59
+ @client.get_info do |server_info|
60
+ callback.call server_info.verified_buttons_bluetooth_addresses
27
61
  end
28
-
29
- server_info.verified_buttons_bluetooth_addresses
30
62
  end
63
+ rescue Client::Shutdown
64
+ raise Shutdown, 'The client has shutdown'
31
65
  end
32
66
 
33
67
  def connect_button
34
- @semaphore.synchronize do
35
- scan_wizard = Client::ScanWizard.new
68
+ scan_wizard = Client::ScanWizard.new
69
+ saw_only_private_button = false
36
70
 
37
- begin
38
- process_events_until do |callback|
39
- scan_wizard.removed do |result, bluetooth_address, *|
40
- if result == :success
41
- callback.call(bluetooth_address)
42
- else
43
- callback.call(nil)
44
- end
45
- end
46
-
47
- client.add_scan_wizard(scan_wizard)
71
+ begin
72
+ @blocker.block_until_callback do |callback|
73
+ scan_wizard.found_private_button do
74
+ saw_only_private_button = true
48
75
  end
49
- ensure
50
- client.remove_scan_wizard(scan_wizard)
76
+
77
+ scan_wizard.found_public_button do
78
+ saw_only_private_button = false
79
+ end
80
+
81
+ scan_wizard.removed do
82
+ callback.call
83
+ end
84
+
85
+ @client.add_scan_wizard(scan_wizard)
51
86
  end
87
+ ensure
88
+ @client.remove_scan_wizard(scan_wizard)
52
89
  end
90
+
91
+ if scan_wizard.successful?
92
+ scan_wizard.button_bluetooth_address
93
+ elsif saw_only_private_button
94
+ raise ButtonIsPrivateError, 'A button was found, but it is private. Press and hold the button for 7 seconds to make it public and try again.'
95
+ end
96
+ rescue Client::Shutdown
97
+ raise Shutdown, 'The client has shutdown'
53
98
  end
54
99
 
55
100
  def disconnect_button(button_bluetooth_address)
56
- @semaphore.synchronize do
57
- client.force_disconnect(button_bluetooth_address)
58
- end
101
+ @client.force_disconnect(button_bluetooth_address)
102
+ rescue Client::Shutdown
103
+ raise Shutdown, 'The client has shutdown'
59
104
  end
60
105
 
61
- def listen(latency_mode, *button_bluetooth_addresses)
62
- @semaphore.synchronize do
63
- connection_channels = []
64
- button_events = []
65
- broken = false
106
+ def listen(button_bluetooth_address_or_latency_mode, *button_bluetooth_addresses)
107
+ if Symbol === button_bluetooth_address_or_latency_mode
108
+ latency_mode = button_bluetooth_address_or_latency_mode
109
+ else
110
+ latency_mode = :normal
111
+ button_bluetooth_addresses.unshift button_bluetooth_address_or_latency_mode
112
+ end
66
113
 
67
- begin
68
- button_bluetooth_addresses.each do |button_bluetooth_addresses|
69
- connection_channel = Client::ConnectionChannel.new(button_bluetooth_addresses, latency_mode)
114
+ connection_channels = []
115
+ queue = Queue.new
70
116
 
71
- connection_channel.button_up_or_down do |click_type, latency|
72
- button_events << [button_bluetooth_addresses, click_type, latency]
73
- end
117
+ @listen_queues_semaphore.synchronize { @listen_queues << queue }
74
118
 
75
- connection_channel.button_single_click_or_double_click_or_hold do |click_type, latency|
76
- button_events << [button_bluetooth_addresses, click_type, latency]
77
- end
119
+ begin
120
+ button_bluetooth_addresses.each do |button_bluetooth_addresses|
121
+ connection_channel = Client::ConnectionChannel.new(button_bluetooth_addresses, latency_mode)
78
122
 
79
- connection_channel.removed do
80
- broken = true
81
- end
123
+ connection_channel.button_up_or_down do |click_type, latency|
124
+ queue << [:button_interaction, button_bluetooth_addresses, click_type, latency]
125
+ end
82
126
 
83
- connection_channels << connection_channel
127
+ connection_channel.button_single_click_or_double_click_or_hold do |click_type, latency|
128
+ queue << [:button_interaction, button_bluetooth_addresses, click_type, latency]
129
+ end
84
130
 
85
- client.add_connection_channel connection_channel
131
+ connection_channel.removed do
132
+ queue << [:connection_channel_removed, connection_channel]
86
133
  end
87
134
 
88
- loop do
89
- client.handle_next_event while !broken && button_events.empty?
135
+ connection_channels << connection_channel
90
136
 
91
- button_events.each do |button_event|
92
- yield *button_event
93
- end
137
+ @client.add_connection_channel connection_channel
138
+ end
94
139
 
95
- button_events.clear
140
+ loop do
141
+ event_type, *params = queue.pop
96
142
 
97
- raise ConnectionChannelRemoved, 'A connection channel was removed' if broken
98
- end
99
- ensure
100
- connection_channels.each do |connection_channel|
101
- client.remove_connection_channel connection_channel
143
+ case event_type
144
+ when :button_interaction
145
+ yield *params
146
+ when :connection_channel_removed
147
+ raise ConnectionChannelRemoved, 'A connection channel was removed'
148
+ when :shutdown
149
+ raise Shutdown, 'The client has shutdown'
102
150
  end
103
151
  end
104
- end
105
- end
106
-
107
- private
108
-
109
- def process_events_until
110
- done = false
111
- result = nil
152
+ ensure
153
+ connection_channels.each do |connection_channel|
154
+ @client.remove_connection_channel connection_channel
155
+ end
112
156
 
113
- callback = proc do |_result|
114
- done = true
115
- result = _result
157
+ @listen_queues_semaphore.synchronize { @listen_queues.delete queue unless @listen_queues.frozen? }
116
158
  end
117
-
118
- yield callback
119
-
120
- client.handle_next_event until done
121
-
122
- result
159
+ rescue Client::Shutdown
160
+ raise Shutdown, 'The client has shutdown'
123
161
  end
124
162
  end
125
163
  end