lifx 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +71 -13
  6. data/Rakefile +12 -0
  7. data/bin/lifx-console +15 -0
  8. data/bin/lifx-snoop +50 -0
  9. data/examples/auto-off/Gemfile +3 -0
  10. data/examples/auto-off/auto-off.rb +35 -0
  11. data/examples/identify/Gemfile +3 -0
  12. data/examples/identify/identify.rb +70 -0
  13. data/examples/travis-build-light/Gemfile +4 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +29 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx/client.rb +136 -0
  18. data/lib/lifx/color.rb +190 -0
  19. data/lib/lifx/config.rb +12 -0
  20. data/lib/lifx/firmware.rb +55 -0
  21. data/lib/lifx/gateway_connection.rb +177 -0
  22. data/lib/lifx/light.rb +406 -0
  23. data/lib/lifx/light_collection.rb +105 -0
  24. data/lib/lifx/light_target.rb +189 -0
  25. data/lib/lifx/logging.rb +11 -0
  26. data/lib/lifx/message.rb +166 -0
  27. data/lib/lifx/network_context.rb +200 -0
  28. data/lib/lifx/observable.rb +46 -0
  29. data/lib/lifx/protocol/address.rb +21 -0
  30. data/lib/lifx/protocol/device.rb +225 -0
  31. data/lib/lifx/protocol/header.rb +24 -0
  32. data/lib/lifx/protocol/light.rb +110 -0
  33. data/lib/lifx/protocol/message.rb +17 -0
  34. data/lib/lifx/protocol/metadata.rb +21 -0
  35. data/lib/lifx/protocol/payload.rb +7 -0
  36. data/lib/lifx/protocol/sensor.rb +29 -0
  37. data/lib/lifx/protocol/type.rb +134 -0
  38. data/lib/lifx/protocol/wan.rb +50 -0
  39. data/lib/lifx/protocol/wifi.rb +76 -0
  40. data/lib/lifx/protocol_path.rb +84 -0
  41. data/lib/lifx/routing_manager.rb +110 -0
  42. data/lib/lifx/routing_table.rb +33 -0
  43. data/lib/lifx/seen.rb +15 -0
  44. data/lib/lifx/site.rb +89 -0
  45. data/lib/lifx/tag_manager.rb +105 -0
  46. data/lib/lifx/tag_table.rb +47 -0
  47. data/lib/lifx/target.rb +23 -0
  48. data/lib/lifx/timers.rb +18 -0
  49. data/lib/lifx/transport/tcp.rb +81 -0
  50. data/lib/lifx/transport/udp.rb +67 -0
  51. data/lib/lifx/transport.rb +41 -0
  52. data/lib/lifx/transport_manager/lan.rb +140 -0
  53. data/lib/lifx/transport_manager.rb +34 -0
  54. data/lib/lifx/utilities.rb +33 -0
  55. data/lib/lifx/version.rb +1 -1
  56. data/lib/lifx.rb +15 -1
  57. data/lifx.gemspec +11 -7
  58. data/spec/color_spec.rb +45 -0
  59. data/spec/gateway_connection_spec.rb +32 -0
  60. data/spec/integration/client_spec.rb +40 -0
  61. data/spec/integration/light_spec.rb +43 -0
  62. data/spec/integration/tags_spec.rb +31 -0
  63. data/spec/message_spec.rb +163 -0
  64. data/spec/protocol_path_spec.rb +109 -0
  65. data/spec/routing_manager_spec.rb +22 -0
  66. data/spec/spec_helper.rb +52 -0
  67. data/spec/transport/udp_spec.rb +38 -0
  68. data/spec/transport_spec.rb +14 -0
  69. metadata +143 -26
data/lib/lifx/site.rb ADDED
@@ -0,0 +1,89 @@
1
+ require 'lifx/seen'
2
+ require 'lifx/timers'
3
+ require 'lifx/observable'
4
+ require 'lifx/gateway_connection'
5
+
6
+ module LIFX
7
+ # @api private
8
+ class Site
9
+ include Seen
10
+ include Timers
11
+ include Logging
12
+ include Observable
13
+
14
+ attr_reader :id, :gateways, :tag_manager
15
+
16
+ def initialize(id:)
17
+ @id = id
18
+ @gateways = {}
19
+ @gateways_mutex = Mutex.new
20
+ @threads = []
21
+ @threads << initialize_timer_thread
22
+ initialize_stale_gateway_check
23
+ end
24
+
25
+ def write(message)
26
+ message.path.site_id = id
27
+ @gateways.values.each do |gateway|
28
+ gateway.write(message)
29
+ end
30
+ end
31
+
32
+ def handle_message(message, ip, transport)
33
+ logger.debug("<- #{self} #{transport}: #{message}")
34
+ payload = message.payload
35
+ case payload
36
+ when Protocol::Device::StatePanGateway
37
+ @gateways_mutex.synchronize do
38
+ @gateways[message.device_id] ||= GatewayConnection.new
39
+ @gateways[message.device_id].handle_message(message, ip, transport)
40
+ @gateways[message.device_id].add_observer(self) do |**args|
41
+ notify_observers(**args)
42
+ end
43
+ end
44
+ end
45
+ seen!
46
+ end
47
+
48
+ def flush(**options)
49
+ @gateways.values.map do |gateway|
50
+ Thread.new do
51
+ gateway.flush(**options)
52
+ end
53
+ end.each(&:join)
54
+ end
55
+
56
+ def to_s
57
+ %Q{#<LIFX::Site id=#{id}>}
58
+ end
59
+ alias_method :inspect, :to_s
60
+
61
+ def stop
62
+ @threads.each do |thread|
63
+ Thread.kill(thread)
64
+ end
65
+ @gateways.values.each do |gateway|
66
+ gateway.close
67
+ end
68
+ end
69
+
70
+
71
+ protected
72
+
73
+ STALE_GATEWAY_CHECK_INTERVAL = 10
74
+ def initialize_stale_gateway_check
75
+ timers.every(STALE_GATEWAY_CHECK_INTERVAL) do
76
+ @gateways_mutex.synchronize do
77
+ stale_gateways = @gateways.select do |k, v|
78
+ !v.connected?
79
+ end
80
+ stale_gateways.each do |id, _|
81
+ logger.info("#{self}: Dropping stale gateway id #{id}")
82
+ @gateways.delete(id)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,105 @@
1
+ module LIFX
2
+ # @api private
3
+ class TagManager
4
+ # TagManager handles discovery of tags, resolving tags to [site_id, tags_field] pairs,
5
+ # creating, setting and removing tags.
6
+
7
+ # Stores site <-> [tag_name, tag_id]
8
+ include Utilities
9
+ include Logging
10
+
11
+ attr_reader :context
12
+
13
+ class TagLimitReached < StandardError; end
14
+
15
+ def initialize(context:, tag_table:)
16
+ @context = context
17
+ @tag_table = tag_table
18
+ end
19
+
20
+ def create_tag(label:, site_id:)
21
+ id = next_unused_id_on_site_id(site_id)
22
+ raise TagLimitReached if id.nil?
23
+ # Add the entry for the tag we're about to create to prevent a case where
24
+ # we don't receive a StateTagLabels before another tag gets created
25
+ @tag_table.update_table(tag_id: id, label: label, site_id: site_id)
26
+ context.send_message(target: Target.new(site_id: site_id),
27
+ payload: Protocol::Device::SetTagLabels.new(tags: id_to_tags_field(id), label: label))
28
+ end
29
+
30
+ def add_tag_to_device(tag:, device:)
31
+ tag_entry = entry_with(label: tag, site_id: device.site_id)
32
+ if !tag_entry
33
+ create_tag(label: tag, site_id: device.site_id)
34
+ tag_entry = entry_with(label: tag, site_id: device.site_id)
35
+ end
36
+
37
+ device_tags_field = device.tags_field
38
+ device_tags_field |= id_to_tags_field(tag_entry.tag_id)
39
+ device.send_message!(Protocol::Device::SetTags.new(tags: device_tags_field), wait_for: Protocol::Device::StateTags) do
40
+ device.tags.include?(tag)
41
+ end
42
+ end
43
+
44
+ def remove_tag_from_device(tag:, device:)
45
+ tag_entry = entry_with(label: tag, site_id: device.site_id)
46
+ return if !tag_entry
47
+
48
+ device_tags_field = device.tags_field
49
+ device_tags_field &= ~id_to_tags_field(tag_entry.tag_id)
50
+ device.send_message!(Protocol::Device::SetTags.new(tags: device_tags_field), wait_for: Protocol::Device::StateTags) do
51
+ !device.tags.include?(tag)
52
+ end
53
+ end
54
+
55
+ def tags
56
+ @tag_table.tags
57
+ end
58
+
59
+ def unused_tags
60
+ @tag_table.tags.select do |tag|
61
+ context.lights.with_tag(tag).empty?
62
+ end
63
+ end
64
+
65
+ # This will clear out tags that currently do not resolve to any devices.
66
+ # If used when devices that are tagged with a tag that is not attached to an
67
+ # active device, it will effectively untag them when they're back on.
68
+ def purge_unused_tags!
69
+ unused_tags.each do |tag|
70
+ logger.info("Purging tag '#{tag}'")
71
+ entries_with(label: tag).each do |entry|
72
+ payload = Protocol::Device::SetTagLabels.new(tags: id_to_tags_field(entry.tag_id), label: '')
73
+ context.send_message(target: Target.new(site_id: entry.site_id),
74
+ payload: payload,
75
+ acknowledge: true)
76
+ end
77
+ end
78
+ Timeout.timeout(5) do
79
+ while !unused_tags.empty?
80
+ sleep 0.1
81
+ end
82
+ end
83
+ end
84
+
85
+ protected
86
+
87
+ VALID_TAG_IDS = (0...64).to_a.freeze
88
+
89
+ def entry_with(**args)
90
+ entries_with(**args).first
91
+ end
92
+
93
+ def entries_with(**args)
94
+ @tag_table.entries_with(**args)
95
+ end
96
+
97
+ def id_to_tags_field(id)
98
+ 2 ** id
99
+ end
100
+
101
+ def next_unused_id_on_site_id(site_id)
102
+ (VALID_TAG_IDS - entries_with(site_id: site_id).map(&:tag_id)).first
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,47 @@
1
+ module LIFX
2
+ # @api private
3
+ class TagTable
4
+ class Entry < Struct.new(:tag_id, :label, :site_id); end
5
+
6
+ def initialize(entries: {})
7
+ @entries = Hash.new { |h, k| h[k] = {} }
8
+ entries.each do |k, v|
9
+ @entries[k] = v
10
+ end
11
+ end
12
+
13
+ def entries_with(tag_id: nil, site_id: nil, label: nil)
14
+ entries.select do |entry|
15
+ ret = []
16
+ ret << (entry.tag_id == tag_id) if tag_id
17
+ ret << (entry.site_id == site_id) if site_id
18
+ ret << (entry.label == label) if label
19
+ ret.all?
20
+ end
21
+ end
22
+
23
+ def entry_with(**args)
24
+ entries_with(**args).first
25
+ end
26
+
27
+ def update_table(tag_id:, label:, site_id:)
28
+ entry = @entries[site_id][tag_id] ||= Entry.new(tag_id, label, site_id)
29
+ entry.label = label
30
+ end
31
+
32
+ def delete_entries_with(tag_id: nil, site_id: nil, label: nil)
33
+ matching_entries = entries_with(tag_id: tag_id, site_id: site_id, label: label)
34
+ matching_entries.each do |entry|
35
+ @entries[entry.site_id].delete(entry.tag_id)
36
+ end
37
+ end
38
+
39
+ def tags
40
+ entries.map(&:label).uniq
41
+ end
42
+
43
+ def entries
44
+ @entries.values.map(&:values).flatten
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module LIFX
2
+ # @api private
3
+
4
+ class Target
5
+ # Target is a high-level abstraction for the target of a Message
6
+
7
+ attr_reader :site_id, :device_id, :tag, :broadcast
8
+ def initialize(device_id: nil, site_id: nil, tag: nil, broadcast: nil)
9
+ @site_id = site_id
10
+ @device_id = device_id
11
+ @tag = tag
12
+ @broadcast = broadcast
13
+ end
14
+
15
+ def broadcast?
16
+ !!broadcast
17
+ end
18
+
19
+ def tag?
20
+ !!tag
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ require 'timers'
2
+ module LIFX
3
+ module Timers
4
+ protected
5
+ def initialize_timer_thread
6
+ timers.after(1) {} # Just so timers.wait doesn't complain when there's no timer
7
+ Thread.new do
8
+ loop do
9
+ timers.wait
10
+ end
11
+ end
12
+ end
13
+
14
+ def timers
15
+ @timers ||= ::Timers.new
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,81 @@
1
+ require 'socket'
2
+
3
+ module LIFX
4
+ class Transport
5
+ # @api private
6
+ class TCP < Transport
7
+ include Logging
8
+
9
+ def initialize(*args)
10
+ super
11
+ connect
12
+ end
13
+
14
+ def connected?
15
+ @socket && !@socket.closed?
16
+ end
17
+
18
+ CONNECT_TIMEOUT = 3
19
+ def connect
20
+ Timeout.timeout(CONNECT_TIMEOUT) do
21
+ @socket = TCPSocket.new(host, port) # Performs the connection
22
+ end
23
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 1024)
24
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
25
+ logger.info("#{self}: Connected.")
26
+ rescue => ex
27
+ logger.warn("#{self}: Exception occured in #connect - #{ex}")
28
+ logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
29
+ @socket = nil
30
+ end
31
+
32
+ def close
33
+ return if !@socket
34
+ Thread.kill(@listener) if @listener
35
+ @listener = nil
36
+ @socket.close if !@socket.closed?
37
+ @socket = nil
38
+ end
39
+
40
+ HEADER_SIZE = 8
41
+ def listen
42
+ return if @listener
43
+ @listener = Thread.new do
44
+ while @socket do
45
+ begin
46
+ header_data = @socket.recv(HEADER_SIZE, Socket::MSG_PEEK)
47
+ header = Protocol::Header.read(header_data)
48
+ size = header.msg_size
49
+ data = @socket.recv(size)
50
+ message = Message.unpack(data)
51
+
52
+ notify_observers(message: message, ip: host, transport: self)
53
+ rescue Message::UnpackError
54
+ if !@ignore_unpackable_messages
55
+ logger.warn("#{self}: Exception occured - #{ex}")
56
+ end
57
+ rescue => ex
58
+ logger.warn("#{self}: Exception occured in #listen - #{ex}")
59
+ logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
60
+ close
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ SEND_TIMEOUT = 2
67
+ def write(message)
68
+ data = message.pack
69
+ Timeout.timeout(SEND_TIMEOUT) do
70
+ @socket.write(data)
71
+ end
72
+ true
73
+ rescue => ex
74
+ logger.warn("#{self}: Exception in #write: #{ex}")
75
+ logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
76
+ close
77
+ false
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,67 @@
1
+ require 'socket'
2
+ module LIFX
3
+ class Transport
4
+ # @api private
5
+ class UDP < Transport
6
+ def initialize(*args)
7
+ super
8
+ @socket = create_socket
9
+ end
10
+
11
+ def connected?
12
+ !!@socket
13
+ end
14
+
15
+ def write(message)
16
+ data = message.pack
17
+ @socket.send(data, 0, host, port)
18
+ true
19
+ rescue => ex
20
+ logger.warn("#{self}: Error on #write: #{ex}")
21
+ logger.debug("#{self}: Backtrace: #{ex.backtrace.join("\n")}")
22
+ close
23
+ false
24
+ end
25
+
26
+ def listen(ip: self.host, port: self.port)
27
+ if @listener
28
+ raise "Socket already being listened to"
29
+ end
30
+
31
+ Thread.abort_on_exception = true
32
+ @listener = Thread.new do
33
+ reader = UDPSocket.new
34
+ reader.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
35
+ reader.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
36
+ reader.bind(ip, port)
37
+ loop do
38
+ begin
39
+ bytes, (_, _, ip, _) = reader.recvfrom(128)
40
+ message = Message.unpack(bytes)
41
+ notify_observers(message: message, ip: ip, transport: self)
42
+ rescue Message::UnpackError
43
+ if !@ignore_unpackable_messages
44
+ logger.warn("#{self}: Unrecognised bytes: #{bytes.bytes.map { |b| '%02x ' % b }.join}")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def close
52
+ Thread.kill(@listener) if @listener
53
+ return if !@socket
54
+ @socket.close
55
+ @socket = nil
56
+ end
57
+
58
+ protected
59
+
60
+ def create_socket
61
+ UDPSocket.new.tap do |socket|
62
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ require 'lifx/observable'
2
+
3
+ module LIFX
4
+ # @api private
5
+ class Transport
6
+ include Logging
7
+ include Observable
8
+
9
+ attr_reader :host, :port
10
+
11
+ def initialize(host, port, ignore_unpackable_messages: true)
12
+ @host = host
13
+ @port = port
14
+ @ignore_unpackable_messages = ignore_unpackable_messages
15
+ end
16
+
17
+ def listen
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def write(message)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def close
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def to_s
30
+ %Q{#<#{self.class.name} #{host}:#{port}>}
31
+ end
32
+ alias_method :inspect, :to_s
33
+
34
+ def observer_callback_definition
35
+ -> (message:, ip: nil, transport: nil) {}
36
+ end
37
+ end
38
+ end
39
+
40
+ require 'lifx/transport/udp'
41
+ require 'lifx/transport/tcp'
@@ -0,0 +1,140 @@
1
+ require 'lifx/site'
2
+
3
+ module LIFX
4
+ module TransportManager
5
+ class LAN < Base
6
+ def initialize(bind_ip: '0.0.0.0', send_ip: '255.255.255.255', port: 56700, peer_port: 56750)
7
+ super
8
+ @bind_ip = bind_ip
9
+ @send_ip = send_ip
10
+ @port = port
11
+ @peer_port = peer_port
12
+
13
+ @sites = {}
14
+ initialize_transports
15
+ end
16
+
17
+ def flush(**options)
18
+ @sites.values.map do |site|
19
+ Thread.new do
20
+ site.flush(**options)
21
+ end
22
+ end.each(&:join)
23
+ end
24
+
25
+ DISCOVERY_INTERVAL_WHEN_NO_SITES_FOUND = 1 # seconds
26
+ DISCOVERY_INTERVAL = 15 # seconds
27
+ def discover
28
+ stop_discovery
29
+ Thread.abort_on_exception = true
30
+ @discovery_thread = Thread.new do
31
+ @last_request_seen = Time.at(0)
32
+ message = Message.new(path: ProtocolPath.new(tagged: true), payload: Protocol::Device::GetPanGateway.new)
33
+ logger.info("Discovering gateways on #{@bind_ip}:#{@port}")
34
+ loop do
35
+ interval = @sites.empty? ?
36
+ DISCOVERY_INTERVAL_WHEN_NO_SITES_FOUND :
37
+ DISCOVERY_INTERVAL
38
+ if Time.now - @last_request_seen > interval
39
+ write(message)
40
+ end
41
+ sleep(interval / 2.0)
42
+ end
43
+ end
44
+ end
45
+
46
+ def stop_discovery
47
+ Thread.kill(@discovery_thread) if @discovery_thread
48
+ end
49
+
50
+ def stop
51
+ stop_discovery
52
+ @transport.close
53
+ @sites.values.each do |site|
54
+ site.stop
55
+ end
56
+ end
57
+
58
+ def write(message)
59
+ return unless on_network?
60
+ if message.path.all_sites?
61
+ broadcast(message)
62
+ else
63
+ site = @sites[message.path.site_id]
64
+ if site
65
+ site.write(message)
66
+ else
67
+ broadcast(message)
68
+ end
69
+ end
70
+ broadcast_to_peers(message)
71
+ end
72
+
73
+ def on_network?
74
+ Socket.getifaddrs.any? { |ifaddr| ifaddr.broadaddr }
75
+ end
76
+
77
+ def broadcast(message)
78
+ if !@transport.connected?
79
+ create_broadcast_transport
80
+ end
81
+ @transport.write(message)
82
+ end
83
+
84
+ def broadcast_to_peers(message)
85
+ if !@peer_transport.connected?
86
+ create_peer_transport
87
+ end
88
+ @peer_transport.write(message)
89
+ end
90
+
91
+ def sites
92
+ @sites.dup
93
+ end
94
+
95
+ def gateways
96
+ @sites.values.map(&:gateways)
97
+ end
98
+
99
+ protected
100
+
101
+ def initialize_transports
102
+ create_broadcast_transport
103
+ create_peer_transport
104
+ end
105
+
106
+ def create_broadcast_transport
107
+ @transport = Transport::UDP.new(@send_ip, @port)
108
+ @transport.add_observer(self) do |message:, ip:, transport:|
109
+ handle_broadcast_message(message, ip, @transport)
110
+ notify_observers(message: message, ip: ip, transport: transport)
111
+ end
112
+ @transport.listen(ip: @bind_ip)
113
+ end
114
+
115
+ def create_peer_transport
116
+ @peer_transport = Transport::UDP.new('255.255.255.255', @peer_port)
117
+ @peer_transport.add_observer(self) do |message:, ip:, transport:|
118
+ notify_observers(message: message, ip: ip, transport: transport)
119
+ end
120
+ @peer_transport.listen(ip: @bind_ip)
121
+ end
122
+
123
+ def handle_broadcast_message(message, ip, transport)
124
+ payload = message.payload
125
+ case payload
126
+ when Protocol::Device::StatePanGateway
127
+ if !@sites.has_key?(message.path.site_id)
128
+ @sites[message.path.site_id] = Site.new(id: message.path.site_id)
129
+ @sites[message.path.site_id].add_observer(self) do |**args|
130
+ notify_observers(**args)
131
+ end
132
+ end
133
+ @sites[message.path.site_id].handle_message(message, ip, transport)
134
+ when Protocol::Device::GetPanGateway
135
+ @last_request_seen = Time.now
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,34 @@
1
+ require 'lifx/observable'
2
+
3
+ module LIFX
4
+ # @api private
5
+ module TransportManager
6
+ class Base
7
+ include Logging
8
+ include Observable
9
+
10
+ def initialize(**args)
11
+ end
12
+
13
+ def discover
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def write(message)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def flush(**options)
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def stop
26
+ raise NotImplementedError
27
+ end
28
+
29
+
30
+ end
31
+ end
32
+ end
33
+
34
+ require 'lifx/transport_manager/lan'
@@ -0,0 +1,33 @@
1
+ module LIFX
2
+ module Utilities
3
+ def try_until(condition_proc, timeout_exception: Timeout::Error,
4
+ timeout: 3,
5
+ condition_interval: 0.1,
6
+ action_interval: 0.5,
7
+ signal: nil, &action_block)
8
+ Timeout.timeout(timeout) do
9
+ m = Mutex.new
10
+ time = 0
11
+ while !condition_proc.call
12
+ if Time.now.to_f - time > action_interval
13
+ time = Time.now.to_f
14
+ action_block.call
15
+ end
16
+ if signal
17
+ m.synchronize do
18
+ signal.wait(m, condition_interval)
19
+ end
20
+ else
21
+ sleep(condition_interval)
22
+ end
23
+ end
24
+ end
25
+ rescue Timeout::Error
26
+ raise timeout_exception if timeout_exception
27
+ end
28
+
29
+ def tag_ids_from_field(tags_field)
30
+ (0...64).to_a.select { |t| (tags_field & (2 ** t)) > 0 }
31
+ end
32
+ end
33
+ end
data/lib/lifx/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module LIFX
2
- VERSION = "0.0.1"
2
+ VERSION = "0.4.0"
3
3
  end