flic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +30 -0
  8. data/Rakefile +12 -0
  9. data/flic.gemspec +25 -0
  10. data/lib/flic.rb +7 -0
  11. data/lib/flic/client.rb +247 -0
  12. data/lib/flic/client/connection.rb +87 -0
  13. data/lib/flic/event_bus.rb +81 -0
  14. data/lib/flic/event_bus/driver.rb +23 -0
  15. data/lib/flic/event_bus/subscription.rb +66 -0
  16. data/lib/flic/protocol.rb +64 -0
  17. data/lib/flic/protocol/commands.rb +44 -0
  18. data/lib/flic/protocol/commands/cancel_scan_wizard.rb +15 -0
  19. data/lib/flic/protocol/commands/change_mode_parameters.rb +17 -0
  20. data/lib/flic/protocol/commands/command.rb +21 -0
  21. data/lib/flic/protocol/commands/create_connection_channel.rb +20 -0
  22. data/lib/flic/protocol/commands/create_scan_wizard.rb +15 -0
  23. data/lib/flic/protocol/commands/create_scanner.rb +14 -0
  24. data/lib/flic/protocol/commands/force_disconnect.rb +15 -0
  25. data/lib/flic/protocol/commands/get_button_uuid.rb +15 -0
  26. data/lib/flic/protocol/commands/get_info.rb +12 -0
  27. data/lib/flic/protocol/commands/ping.rb +14 -0
  28. data/lib/flic/protocol/commands/remove_connection_channel.rb +14 -0
  29. data/lib/flic/protocol/commands/remove_scanner.rb +14 -0
  30. data/lib/flic/protocol/events.rb +60 -0
  31. data/lib/flic/protocol/events/advertisement_packet.rb +25 -0
  32. data/lib/flic/protocol/events/bluetooth_controller_state_change.rb +15 -0
  33. data/lib/flic/protocol/events/button_click_or_hold.rb +19 -0
  34. data/lib/flic/protocol/events/button_single_or_double_click.rb +19 -0
  35. data/lib/flic/protocol/events/button_single_or_double_click_or_hold.rb +19 -0
  36. data/lib/flic/protocol/events/button_up_or_down.rb +19 -0
  37. data/lib/flic/protocol/events/connection_channel_removed.rb +16 -0
  38. data/lib/flic/protocol/events/connection_status_changed.rb +19 -0
  39. data/lib/flic/protocol/events/create_connection_channel_response.rb +18 -0
  40. data/lib/flic/protocol/events/event.rb +21 -0
  41. data/lib/flic/protocol/events/get_button_uuid_response.rb +17 -0
  42. data/lib/flic/protocol/events/get_info_response.rb +27 -0
  43. data/lib/flic/protocol/events/got_space_for_new_connection.rb +14 -0
  44. data/lib/flic/protocol/events/new_verified_button.rb +15 -0
  45. data/lib/flic/protocol/events/no_space_for_new_connection.rb +14 -0
  46. data/lib/flic/protocol/events/ping_response.rb +14 -0
  47. data/lib/flic/protocol/events/scan_wizard_button_connected.rb +14 -0
  48. data/lib/flic/protocol/events/scan_wizard_completed.rb +16 -0
  49. data/lib/flic/protocol/events/scan_wizard_found_private_button.rb +14 -0
  50. data/lib/flic/protocol/events/scan_wizard_found_public_button.rb +19 -0
  51. data/lib/flic/protocol/packet_header.rb +12 -0
  52. data/lib/flic/protocol/primitives.rb +22 -0
  53. data/lib/flic/protocol/primitives/bluetooth_address.rb +35 -0
  54. data/lib/flic/protocol/primitives/bluetooth_address_type.rb +13 -0
  55. data/lib/flic/protocol/primitives/bluetooth_controller_state.rb +14 -0
  56. data/lib/flic/protocol/primitives/boolean.rb +19 -0
  57. data/lib/flic/protocol/primitives/click_type.rb +17 -0
  58. data/lib/flic/protocol/primitives/connection_status.rb +14 -0
  59. data/lib/flic/protocol/primitives/create_connection_channel_error.rb +13 -0
  60. data/lib/flic/protocol/primitives/device_name.rb +44 -0
  61. data/lib/flic/protocol/primitives/disconnect_reason.rb +15 -0
  62. data/lib/flic/protocol/primitives/enum.rb +85 -0
  63. data/lib/flic/protocol/primitives/latency_mode.rb +14 -0
  64. data/lib/flic/protocol/primitives/removed_reason.rb +18 -0
  65. data/lib/flic/protocol/primitives/scan_wizard_result.rb +18 -0
  66. data/lib/flic/protocol/primitives/uuid.rb +23 -0
  67. data/lib/flic/version.rb +3 -0
  68. metadata +180 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ade3fb4aee08cb658059f896f2ba8e3524b7ee76
4
+ data.tar.gz: b04d34afa0853fb278f57a58c4569d02d53c02dc
5
+ SHA512:
6
+ metadata.gz: 625cdb437414faa416799c2a64b581880aa7847fd4f774cc34e16f3c74f882928fbd7dfcc8f1c8b88f73c6c1bc79becb9a0f5e9f8a7e3e54fa826707bbc23835
7
+ data.tar.gz: d0e5ec61e2394840c33232e60cc2db1840df09587198f99b443d28786e22078ae345fae7f66dc7939cd0ceeb0140297dc3db8729849b27acf0fa253375aada44
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in flic.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Flic
2
+
3
+ Flic is a Ruby implementation of the [Fliclib](https://github.com/50ButtonsEach/fliclib-linux-hci/blob/master/ProtocolDocumentation.md).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'flic'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install flic
20
+
21
+
22
+ ## Contributing
23
+
24
+ Bug reports and pull requests are welcome on GitHub at https://github.com/anarchocurious/flic.
25
+
26
+
27
+ ## License
28
+
29
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
30
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
6
+ task :test => :spec
7
+
8
+ task :console do
9
+ require 'flic'
10
+ require 'pry'
11
+ Pry.start
12
+ end
data/flic.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'flic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'flic'
8
+ spec.version = Flic::VERSION
9
+ spec.authors = ['Alec Larsen']
10
+ spec.email = ['aleclarsen42@gmail.com']
11
+
12
+ spec.summary = %q{A Ruby implementation of the (Fliclib)[https://github.com/50ButtonsEach/fliclib-linux-hci/blob/master/ProtocolDocumentation.md]}
13
+ spec.homepage = 'https://github.com/anarchocurious/flic'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.require_paths = ['lib']
18
+
19
+ spec.add_runtime_dependency 'bindata', '~> 2.3'
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.12'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
+ spec.add_development_dependency 'pry'
25
+ end
data/lib/flic.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'flic/version'
2
+
3
+ module Flic
4
+ autoload :Client, 'flic/client'
5
+ autoload :EventBus, 'flic/event_bus'
6
+ autoload :Protocol, 'flic/protocol'
7
+ end
@@ -0,0 +1,247 @@
1
+ require 'flic'
2
+ require 'flic/event_bus'
3
+ require 'flic/protocol'
4
+
5
+ module Flic
6
+ class Client
7
+ autoload :Connection, 'flic/client/connection'
8
+
9
+ class Error < StandardError; end
10
+ class ClientShutdownError < Error; end
11
+
12
+ class << self
13
+ def open(*args)
14
+ client = new(*args)
15
+
16
+ begin
17
+ yield client
18
+ ensure
19
+ client.shutdown
20
+ end
21
+ end
22
+ end
23
+
24
+ attr_reader :connection, :driver
25
+
26
+ def initialize(*connection_args)
27
+ @connection = Connection.new(*connection_args)
28
+
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
+
43
+ yield self if block_given?
44
+ end
45
+
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
+ def shutdown
59
+ connection.close
60
+ end
61
+
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
+
126
+ 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
+ )
158
+
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
173
+ end
174
+ end
175
+ end
176
+
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
214
+ end
215
+
216
+ private
217
+
218
+ def event_bus
219
+ @event_bus ||= driver.event_bus
220
+ end
221
+
222
+ def send_command(command)
223
+ command = command.new if Class === command
224
+ 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
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,87 @@
1
+ require 'flic'
2
+ require 'flic/client'
3
+ require 'flic/protocol'
4
+
5
+ require 'socket'
6
+
7
+ module Flic
8
+ class Client
9
+ class Connection
10
+ class Error < StandardError; end
11
+ class ConnectionClosedError < Error; end
12
+
13
+ class << self
14
+ def open(*args)
15
+ client = new(*args)
16
+
17
+ begin
18
+ yield client
19
+ ensure
20
+ client.close
21
+ end
22
+ end
23
+ end
24
+
25
+ attr_reader :hostname, :port
26
+
27
+ def initialize(hostname = 'localhost', port = 5551, *additional_socket_args)
28
+ @hostname, @port = hostname, port
29
+ @socket = TCPSocket.new(hostname, port, *additional_socket_args)
30
+ @read_semaphore = Mutex.new
31
+ @write_semaphore = Mutex.new
32
+ end
33
+
34
+ def send_command(command)
35
+ send_packet Protocol.serialize_command(command)
36
+ end
37
+
38
+ def recv_event
39
+ Protocol.parse_event(recv_packet)
40
+ end
41
+
42
+ def listen
43
+ loop { yield recv_event }
44
+ end
45
+
46
+ def closed?
47
+ @socket.closed?
48
+ end
49
+
50
+ def close
51
+ @socket.close
52
+ end
53
+
54
+ private
55
+
56
+ def send_packet(payload)
57
+ @write_semaphore.synchronize do
58
+ packet_header = Protocol::PacketHeader.new(byte_length: payload.bytesize)
59
+ @socket.write(packet_header.to_binary_s)
60
+ @socket.write(payload)
61
+ end
62
+ rescue IOError
63
+ if closed?
64
+ raise ConnectionClosedError
65
+ else
66
+ raise
67
+ end
68
+ end
69
+
70
+ def recv_packet
71
+ @read_semaphore.synchronize do
72
+ packet_header = Protocol::PacketHeader.new
73
+ packet_header_bytes = @socket.read packet_header.num_bytes
74
+ packet_header.read(packet_header_bytes)
75
+
76
+ @socket.read(packet_header.byte_length)
77
+ end
78
+ rescue IOError
79
+ if closed?
80
+ raise ConnectionClosedError
81
+ else
82
+ raise
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end