flic 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/flic.rb +2 -1
  3. data/lib/flic/callbacks.rb +36 -0
  4. data/lib/flic/client.rb +52 -203
  5. data/lib/flic/client/connection_channel.rb +21 -0
  6. data/lib/flic/client/features.rb +15 -0
  7. data/lib/flic/client/features/connection_channel.rb +175 -0
  8. data/lib/flic/client/features/force_disconnect.rb +13 -0
  9. data/lib/flic/client/features/get_button_uuid.rb +43 -0
  10. data/lib/flic/client/features/get_info.rb +52 -0
  11. data/lib/flic/client/features/ping.rb +55 -0
  12. data/lib/flic/client/features/scan.rb +112 -0
  13. data/lib/flic/client/features/scan_wizard.rb +131 -0
  14. data/lib/flic/client/scan_wizard.rb +14 -0
  15. data/lib/flic/client/scanner.rb +12 -0
  16. data/lib/flic/client/server_info.rb +16 -0
  17. data/lib/flic/protocol.rb +1 -0
  18. data/lib/flic/protocol/commands/change_mode_parameters.rb +3 -2
  19. data/lib/flic/protocol/commands/create_connection_channel.rb +3 -2
  20. data/lib/flic/protocol/commands/remove_connection_channel.rb +1 -1
  21. data/lib/flic/{client → protocol}/connection.rb +20 -26
  22. data/lib/flic/protocol/events/button_click_or_hold.rb +1 -1
  23. data/lib/flic/protocol/events/button_single_or_double_click.rb +1 -1
  24. data/lib/flic/protocol/events/button_single_or_double_click_or_hold.rb +1 -1
  25. data/lib/flic/protocol/events/button_up_or_down.rb +1 -1
  26. data/lib/flic/protocol/events/connection_channel_removed.rb +2 -2
  27. data/lib/flic/protocol/events/connection_status_changed.rb +1 -1
  28. data/lib/flic/protocol/events/create_connection_channel_response.rb +1 -1
  29. data/lib/flic/protocol/events/get_info_response.rb +3 -2
  30. data/lib/flic/protocol/primitives.rb +1 -0
  31. data/lib/flic/protocol/primitives/disconnect_time.rb +29 -0
  32. data/lib/flic/simple_client.rb +116 -0
  33. data/lib/flic/version.rb +1 -1
  34. metadata +18 -6
  35. data/lib/flic/event_bus.rb +0 -81
  36. data/lib/flic/event_bus/driver.rb +0 -23
  37. data/lib/flic/event_bus/subscription.rb +0 -66
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ade3fb4aee08cb658059f896f2ba8e3524b7ee76
4
- data.tar.gz: b04d34afa0853fb278f57a58c4569d02d53c02dc
3
+ metadata.gz: 5c7e0c99f004d000f1ed0b0296403fbc8e723a4a
4
+ data.tar.gz: d48a0bb111640992b77d6bac24f3cdd2e70b2b79
5
5
  SHA512:
6
- metadata.gz: 625cdb437414faa416799c2a64b581880aa7847fd4f774cc34e16f3c74f882928fbd7dfcc8f1c8b88f73c6c1bc79becb9a0f5e9f8a7e3e54fa826707bbc23835
7
- data.tar.gz: d0e5ec61e2394840c33232e60cc2db1840df09587198f99b443d28786e22078ae345fae7f66dc7939cd0ceeb0140297dc3db8729849b27acf0fa253375aada44
6
+ metadata.gz: fccb7b55b186dbae110a268d78682ae2cc1213b7fc3f0883f08c1fc330549c44b61e868a4a17c8b859e11688cefcc1fa3834cd34932316da3ac5def7135f7796
7
+ data.tar.gz: 2181c732b2ad6340fc12bd4865de9d2da1dea2e2118b4c09dc2ee9fa5a1d221ee88299609ab2495cb20c665e19d8d3b5618831f3fcaf71be95554ee240bb04fd
@@ -1,7 +1,8 @@
1
1
  require 'flic/version'
2
2
 
3
3
  module Flic
4
+ autoload :Callbacks, 'flic/callbacks'
4
5
  autoload :Client, 'flic/client'
5
- autoload :EventBus, 'flic/event_bus'
6
6
  autoload :Protocol, 'flic/protocol'
7
+ autoload :SimpleClient, 'flic/simple_client'
7
8
  end
@@ -0,0 +1,36 @@
1
+ require 'flic'
2
+
3
+ require 'thread'
4
+
5
+ module Flic
6
+ module Callbacks
7
+ SEMAPHORE = Mutex.new
8
+
9
+ private
10
+
11
+ def define_callbacks(*callback_names)
12
+ callback_names.each do |callback_name|
13
+ semaphore_instance_variable = :"@#{callback_name}_callbacks_semaphore"
14
+ callbacks_instance_variable = :"@#{callback_name}_callbacks"
15
+
16
+ define_method :"#{callback_name}" do |*args, &callback|
17
+ semaphore = SEMAPHORE.synchronize do
18
+ instance_variable_get(semaphore_instance_variable) ||
19
+ instance_variable_set(semaphore_instance_variable, Mutex.new)
20
+ end
21
+
22
+ semaphore.synchronize do
23
+ callbacks = instance_variable_get(callbacks_instance_variable) ||
24
+ instance_variable_set(callbacks_instance_variable, [])
25
+
26
+ if callback
27
+ callbacks << callback
28
+ else
29
+ callbacks.each { |callback| callback.call *args }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,14 +1,29 @@
1
1
  require 'flic'
2
- require 'flic/event_bus'
3
- require 'flic/protocol'
2
+
3
+ require 'thread'
4
+ require 'socket'
4
5
 
5
6
  module Flic
6
7
  class Client
7
- autoload :Connection, 'flic/client/connection'
8
-
9
8
  class Error < StandardError; end
10
9
  class ClientShutdownError < Error; end
11
10
 
11
+ autoload :ConnectionChannel, 'flic/client/connection_channel'
12
+ autoload :Features, 'flic/client/features'
13
+ autoload :ScanWizard, 'flic/client/scan_wizard'
14
+ autoload :Scanner, 'flic/client/scanner'
15
+ autoload :ServerInfo, 'flic/client/server_info'
16
+
17
+ extend Callbacks
18
+
19
+ prepend Features::ConnectionChannel
20
+ prepend Features::ForceDisconnect
21
+ prepend Features::GetButtonUuid
22
+ prepend Features::GetInfo
23
+ prepend Features::Ping
24
+ prepend Features::Scan
25
+ prepend Features::ScanWizard
26
+
12
27
  class << self
13
28
  def open(*args)
14
29
  client = new(*args)
@@ -21,227 +36,61 @@ module Flic
21
36
  end
22
37
  end
23
38
 
24
- attr_reader :connection, :driver
39
+ attr_reader :host, :port, :socket, :connection
25
40
 
26
- def initialize(*connection_args)
27
- @connection = Connection.new(*connection_args)
41
+ define_callbacks :new_button_verified, :bluetooth_controller_state_changed,
42
+ :connections_exhausted, :connection_available
28
43
 
29
- @driver = EventBus::Driver.new do |event_bus|
30
- begin
31
- connection.listen do |event|
32
- event_bus.broadcast(event)
33
- end
34
- rescue Connection::ConnectionClosedError
35
- nil
36
- rescue Protocol::Error => protocol_error
37
- warn protocol_error
38
-
39
- retry
40
- end
41
- end
42
44
 
45
+ def initialize(host = 'localhost', port = 5551)
46
+ @host, @port = host, port
47
+ @handle_next_event_semaphore = Mutex.new
48
+ @socket = TCPSocket.new(host, port)
49
+ @connection = Protocol::Connection.new(socket)
43
50
  yield self if block_given?
44
51
  end
45
52
 
46
- def hostname
47
- connection.hostname
48
- end
49
-
50
- def port
51
- connection.port
52
- end
53
-
54
- def shutdown?
55
- connection.closed?
56
- end
57
-
58
53
  def shutdown
59
54
  connection.close
60
55
  end
61
56
 
62
- def ping ping_id = rand(2**32)
63
- request Protocol::Commands::Ping.new(ping_id: ping_id) do |event|
64
- Protocol::Events::PingResponse === event &&
65
- event.ping_id == ping_id
66
- end
67
-
68
- true
69
- end
70
-
71
- def server_info
72
- request Protocol::Commands::GetInfo, Protocol::Events::GetInfoResponse
73
- end
74
-
75
- def button_uuid(bluetooth_address)
76
- command = Protocol::Commands::GetButtonUuid.new(bluetooth_address: bluetooth_address)
77
-
78
- response = request command do |event|
79
- Protocol::Events::GetButtonUuidResponse === event &&
80
- event.bluetooth_address == command.bluetooth_address
81
- end
82
-
83
- unless response.uuid == Protocol::INVALID_BUTTON_UUID
84
- response.uuid
85
- end
86
- end
87
-
88
- def buttons
89
- server_info.verified_buttons
90
- end
91
-
92
- def disconnect_button(bluetooth_address)
93
- send_command Protocol::Commands::ForceDisconnect.new(bluetooth_address: bluetooth_address)
94
- end
95
-
96
- def connect_button
97
- bluetooth_address = nil
98
-
99
- result = scan_wizard do |button_type, _bluetooth_address|
100
- bluetooth_address = _bluetooth_address if button_type == :public
101
- end
102
-
103
- bluetooth_address if result == :success
104
- end
105
-
106
- def scan(scan_id = rand(2**32))
107
- subscribe do |subscription|
108
- send_command Protocol::Commands::CreateScanner.new(scan_id: scan_id)
109
-
110
- begin
111
- subscription.listen do |event|
112
- if Protocol::Events::AdvertisementPacket === event && event.scan_id == scan_id
113
- yield event.bluetooth_address, event.name, event.rssi, event.is_private, event.is_already_verified
114
- end
115
- end
116
- ensure
117
- send_command Protocol::Commands::RemoveScanner.new(scan_id: scan_id)
118
- end
119
- end
120
- end
121
-
122
- def scan_wizard(scan_wizard_id = rand(2**32))
123
- subscribe do |subscription|
124
- send_command Protocol::Commands::CreateScanWizard.new(scan_wizard_id: scan_wizard_id)
125
-
57
+ def handle_next_event
58
+ @handle_next_event_semaphore.synchronize do
126
59
  begin
127
- bluetooth_address = nil
128
- name = nil
129
-
130
- result = subscription.listen do |event|
131
- case event
132
- when Protocol::Events::ScanWizardFoundPrivateButton
133
- yield :private, nil, nil
134
- when Protocol::Events::ScanWizardFoundPublicButton
135
- bluetooth_address, name = event.bluetooth_address, event.name
136
- when Protocol::Events::ScanWizardButtonConnected
137
- yield :public, bluetooth_address, name
138
- when Protocol::Events::ScanWizardCompleted
139
- break event.scan_wizard_result
140
- end
141
- end
142
- ensure
143
- send_command Protocol::Commands::CancelScanWizard.new(scan_wizard_id: scan_wizard_id) unless result
144
- end
145
- end
146
- end
147
-
148
- def channel(bluetooth_address, latency_mode = :normal, auto_disconnect_time = nil, connection_id = rand(2**32))
149
- auto_disconnect_time = 512 unless auto_disconnect_time # 512 means disabled
150
-
151
- subscribe do |subscription|
152
- send_command Protocol::Commands::CreateConnectionChannel.new(
153
- connection_id: connection_id,
154
- bluetooth_address: bluetooth_address,
155
- latency_mode: latency_mode,
156
- auto_disconnect_time: auto_disconnect_time
157
- )
60
+ handle_event connection.recv_event
61
+ rescue Protocol::Connection::ConnectionClosedError
62
+ shutdown
158
63
 
159
- is_removed = false
160
-
161
- begin
162
- subscription.listen do |event|
163
- case event
164
- when Protocol::Events::ButtonSingleOrDoubleClickOrHold
165
- yield bluetooth_address, event.click_type, event.time_difference
166
- when Protocol::Events::ConnectionChannelRemoved
167
- is_removed = true
168
- break
169
- end
170
- end
171
- ensure
172
- send_command Protocol::Commands::RemoveConnectionChannel.new(connection_id: connection_id) unless is_removed
64
+ raise ClientShutdownError, 'The connection has been closed'
173
65
  end
174
66
  end
175
67
  end
176
68
 
177
- def listen(*bluetooth_addresses)
178
- subscribe do |subscription|
179
- connection_id_bluetooth_address = {}
180
-
181
- begin
182
- bluetooth_addresses.each do |bluetooth_address|
183
- connection_id = rand(2**32)
184
-
185
- send_command Protocol::Commands::CreateConnectionChannel.new(
186
- connection_id: connection_id,
187
- bluetooth_address: bluetooth_address,
188
- latency_mode: :normal,
189
- auto_disconnect_time: 512
190
- )
191
-
192
- connection_id_bluetooth_address[connection_id] = bluetooth_address
193
- end
194
-
195
- subscription.listen do |event|
196
- case event
197
- when Protocol::Events::ButtonSingleOrDoubleClickOrHold
198
- bluetooth_address = connection_id_bluetooth_address[event.connection_id]
199
- yield bluetooth_address, event.click_type, event.time_difference
200
- when Protocol::Events::ConnectionChannelRemoved
201
- bluetooth_address = connection_id_bluetooth_address[event.connection_id]
202
- connection_id_bluetooth_address.delete event.connection_id
203
- raise "connection to #{bluetooth_address} was removed"
204
- end
205
- end
206
- ensure
207
- connection_id_bluetooth_address.each do |connection_id, _|
208
- send_command Protocol::Commands::RemoveConnectionChannel.new(
209
- connection_id: connection_id
210
- )
211
- end
212
- end
213
- end
69
+ def enter_main_loop
70
+ loop { handle_next_event }
214
71
  end
215
72
 
216
73
  private
217
74
 
218
- def event_bus
219
- @event_bus ||= driver.event_bus
220
- end
221
-
222
75
  def send_command(command)
223
- command = command.new if Class === command
224
76
  connection.send_command(command)
225
- rescue Client::Connection::ConnectionClosedError
226
- raise ClientShutdownError
227
- end
228
-
229
- def subscribe
230
- event_bus.subscribe { |subscription| yield subscription }
231
- rescue EventBus::EventBusShutdown
232
- raise ClientShutdownError
233
- end
234
-
235
- def request(command, response_matcher = Proc.new)
236
- subscribe do |subscription|
237
- send_command command
238
-
239
- subscription.listen do |event|
240
- if response_matcher === event
241
- break event
242
- end
243
- end
77
+ rescue Protocol::Connection::ConnectionClosedError
78
+ shutdown
79
+
80
+ raise ClientShutdownError, 'The connection has been closed'
81
+ end
82
+
83
+ def handle_event(event)
84
+ case event
85
+ when Protocol::Events::NewVerifiedButton
86
+ new_button_verified event.bluetooth_address
87
+ when Protocol::Events::BluetoothControllerStateChange
88
+ bluetooth_controller_state_changed event.bluetooth_controller_state
89
+ when Protocol::Events::NoSpaceForNewConnection
90
+ connections_exhausted
91
+ when Protocol::Events::GotSpaceForNewConnection
92
+ connection_available
244
93
  end
245
94
  end
246
95
  end
247
- end
96
+ end
@@ -0,0 +1,21 @@
1
+ require 'flic/client'
2
+
3
+ module Flic
4
+ class Client
5
+ class ConnectionChannel
6
+ extend Callbacks
7
+
8
+ attr_reader :button_bluetooth_address, :latency_mode, :auto_disconnect_time
9
+ attr_accessor :status
10
+
11
+ define_callbacks :added, :removed, :failed_to_add, :status_changed,
12
+ :button_up_or_down, :button_click_or_hold,
13
+ :button_single_click_or_double_click, :button_single_click_or_double_click_or_hold
14
+
15
+ def initialize(button_bluetooth_address, latency_mode, auto_disconnect_time = nil)
16
+ @button_bluetooth_address, @latency_mode, @auto_disconnect_time = button_bluetooth_address, latency_mode, auto_disconnect_time
17
+ @status = :disconnected
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ require 'flic/client'
2
+
3
+ module Flic
4
+ class Client
5
+ module Features
6
+ autoload :ConnectionChannel, 'flic/client/features/connection_channel'
7
+ autoload :ForceDisconnect, 'flic/client/features/force_disconnect'
8
+ autoload :GetButtonUuid, 'flic/client/features/get_button_uuid'
9
+ autoload :GetInfo, 'flic/client/features/get_info'
10
+ autoload :Ping, 'flic/client/features/ping'
11
+ autoload :Scan, 'flic/client/features/scan'
12
+ autoload :ScanWizard, 'flic/client/features/scan_wizard'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,175 @@
1
+ require 'flic/client/features'
2
+
3
+ require 'thread'
4
+
5
+ module Flic
6
+ class Client
7
+ module Features
8
+ module ConnectionChannel
9
+ def initialize(*)
10
+ @connection_channel_id_connection_channel_semaphore = Mutex.new
11
+ @connection_channel_id_connection_channel = {}
12
+
13
+ super
14
+ end
15
+
16
+ def connection_channels
17
+ @connection_channel_id_connection_channel_semaphore.synchronize { @connection_channel_id_connection_channel.values }
18
+ end
19
+
20
+ def add_connection_channel(connection_channel)
21
+ connection_channel_id = _add_connection_channel(connection_channel)
22
+
23
+ if connection_channel_id
24
+ send_command Protocol::Commands::CreateConnectionChannel.new(
25
+ connection_channel_id: connection_channel_id,
26
+ bluetooth_address: connection_channel.button_bluetooth_address,
27
+ latency_mode: connection_channel.latency_mode,
28
+ auto_disconnect_time: connection_channel.auto_disconnect_time
29
+ )
30
+ end
31
+ end
32
+
33
+ def remove_connection_channel(connection_channel)
34
+ connection_channel_id = find_connection_channel_id_by_connection_channel(connection_channel)
35
+
36
+ if connection_channel_id
37
+ send_command Protocol::Commands::RemoveConnectionChannel.new(
38
+ connection_channel_id: connection_channel_id
39
+ )
40
+ end
41
+ end
42
+
43
+ def shutdown(*)
44
+ connection_channels.each do |connection_channel|
45
+ _remove_connection_channel(connection_channel)
46
+ connection_channel.removed self
47
+ end
48
+
49
+ super
50
+ end
51
+
52
+ private
53
+
54
+ def handle_event(event)
55
+ case event
56
+ when Protocol::Events::CreateConnectionChannelResponse
57
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
58
+
59
+ if connection_channel
60
+ if event.error == :no_error
61
+ connection_channel.added self
62
+ else
63
+ _remove_connection_channel(connection_channel)
64
+ connection_channel.failed_to_add self, event.error
65
+ end
66
+ end
67
+
68
+ when Protocol::Events::ConnectionStatusChanged
69
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
70
+
71
+ if connection_channel
72
+ connection_channel.status = event.connection_status
73
+ connection_channel.status_changed event.connection_status
74
+ end
75
+
76
+ when Protocol::Events::ButtonUpOrDown
77
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
78
+
79
+ if connection_channel
80
+ connection_channel.button_up_or_down event.click_type, event.time_difference, event.was_queued
81
+ end
82
+ when Protocol::Events::ButtonClickOrHold
83
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
84
+
85
+ if connection_channel
86
+ connection_channel.button_click_or_hold event.click_type, event.time_difference, event.was_queued
87
+ end
88
+
89
+ when Protocol::Events::ButtonSingleOrDoubleClick
90
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
91
+
92
+ if connection_channel
93
+ connection_channel.button_single_click_or_double_click event.click_type, event.time_difference, event.was_queued
94
+ end
95
+
96
+ when Protocol::Events::ButtonSingleOrDoubleClickOrHold
97
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
98
+
99
+ if connection_channel
100
+ connection_channel.button_single_click_or_double_click_or_hold event.click_type, event.time_difference, event.was_queued
101
+ end
102
+
103
+ when Protocol::Events::ConnectionChannelRemoved
104
+ connection_channel = find_connection_channel_by_connection_channel_id(event.connection_channel_id)
105
+
106
+ if connection_channel
107
+ _remove_connection_channel(connection_channel)
108
+ connection_channel.removed self, event.reason
109
+ end
110
+
111
+ else
112
+ super
113
+ end
114
+ end
115
+
116
+ def find_connection_channel_by_connection_channel_id(needle)
117
+ @connection_channel_id_connection_channel_semaphore.synchronize do
118
+ @connection_channel_id_connection_channel.each do |connection_channel_id, connection_channel|
119
+ return connection_channel if connection_channel_id == needle
120
+ end
121
+ end
122
+
123
+ nil
124
+ end
125
+
126
+ def find_connection_channel_id_by_connection_channel(needle)
127
+ @connection_channel_id_connection_channel_semaphore.synchronize do
128
+ @connection_channel_id_connection_channel.each do |connection_channel_id, connection_channel|
129
+ return connection_channel_id if connection_channel == needle
130
+ end
131
+ end
132
+
133
+ nil
134
+ end
135
+
136
+ def _add_connection_channel(connection_channel)
137
+ connection_channel_id = nil
138
+
139
+ @connection_channel_id_connection_channel_semaphore.synchronize do
140
+ unless @connection_channel_id_connection_channel.values.include?(connection_channel)
141
+ loop do
142
+ connection_channel_id = rand(2**32)
143
+
144
+ break unless @connection_channel_id_connection_channel.has_key?(connection_channel_id)
145
+ end
146
+
147
+ @connection_channel_id_connection_channel[connection_channel_id] = connection_channel
148
+ end
149
+ end
150
+
151
+ connection_channel_id
152
+ end
153
+
154
+ def _remove_connection_channel(connection_channel)
155
+ connection_channel_id = nil
156
+
157
+ @connection_channel_id_connection_channel_semaphore.synchronize do
158
+ if @connection_channel_id_connection_channel.values.include?(connection_channel)
159
+ @connection_channel_id_connection_channel.each do |_connection_channel_id, _connection_channel|
160
+ if connection_channel == _connection_channel
161
+ connection_channel_id = _connection_channel_id
162
+ break
163
+ end
164
+ end
165
+
166
+ @connection_channel_id_connection_channel.delete connection_channel_id if connection_channel_id
167
+ end
168
+ end
169
+
170
+ connection_channel_id
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end