lifx-lan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/.yardopts +3 -0
- data/CHANGES.md +45 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +23 -0
- data/README.md +15 -0
- data/Rakefile +20 -0
- data/bin/lifx-snoop +50 -0
- data/examples/auto-off/auto-off.rb +34 -0
- data/examples/blink/blink.rb +19 -0
- data/examples/identify/identify.rb +69 -0
- data/examples/travis-build-light/build-light.rb +57 -0
- data/lib/bindata_ext/bool.rb +30 -0
- data/lib/bindata_ext/record.rb +11 -0
- data/lib/lifx-lan.rb +27 -0
- data/lib/lifx/lan/client.rb +149 -0
- data/lib/lifx/lan/color.rb +199 -0
- data/lib/lifx/lan/config.rb +17 -0
- data/lib/lifx/lan/firmware.rb +60 -0
- data/lib/lifx/lan/gateway_connection.rb +185 -0
- data/lib/lifx/lan/light.rb +440 -0
- data/lib/lifx/lan/light_collection.rb +111 -0
- data/lib/lifx/lan/light_target.rb +185 -0
- data/lib/lifx/lan/logging.rb +14 -0
- data/lib/lifx/lan/message.rb +168 -0
- data/lib/lifx/lan/network_context.rb +188 -0
- data/lib/lifx/lan/observable.rb +66 -0
- data/lib/lifx/lan/protocol/address.rb +25 -0
- data/lib/lifx/lan/protocol/device.rb +387 -0
- data/lib/lifx/lan/protocol/header.rb +24 -0
- data/lib/lifx/lan/protocol/light.rb +142 -0
- data/lib/lifx/lan/protocol/message.rb +19 -0
- data/lib/lifx/lan/protocol/metadata.rb +23 -0
- data/lib/lifx/lan/protocol/payload.rb +12 -0
- data/lib/lifx/lan/protocol/sensor.rb +31 -0
- data/lib/lifx/lan/protocol/type.rb +204 -0
- data/lib/lifx/lan/protocol/wan.rb +51 -0
- data/lib/lifx/lan/protocol/wifi.rb +102 -0
- data/lib/lifx/lan/protocol_path.rb +85 -0
- data/lib/lifx/lan/required_keyword_arguments.rb +12 -0
- data/lib/lifx/lan/routing_manager.rb +114 -0
- data/lib/lifx/lan/routing_table.rb +48 -0
- data/lib/lifx/lan/seen.rb +25 -0
- data/lib/lifx/lan/site.rb +97 -0
- data/lib/lifx/lan/tag_manager.rb +111 -0
- data/lib/lifx/lan/tag_table.rb +49 -0
- data/lib/lifx/lan/target.rb +24 -0
- data/lib/lifx/lan/thread.rb +13 -0
- data/lib/lifx/lan/timers.rb +29 -0
- data/lib/lifx/lan/transport.rb +46 -0
- data/lib/lifx/lan/transport/tcp.rb +91 -0
- data/lib/lifx/lan/transport/udp.rb +87 -0
- data/lib/lifx/lan/transport_manager.rb +43 -0
- data/lib/lifx/lan/transport_manager/lan.rb +169 -0
- data/lib/lifx/lan/utilities.rb +36 -0
- data/lib/lifx/lan/version.rb +5 -0
- data/lifx-lan.gemspec +26 -0
- data/spec/color_spec.rb +43 -0
- data/spec/gateway_connection_spec.rb +30 -0
- data/spec/integration/client_spec.rb +42 -0
- data/spec/integration/light_spec.rb +56 -0
- data/spec/integration/tags_spec.rb +42 -0
- data/spec/light_collection_spec.rb +37 -0
- data/spec/message_spec.rb +183 -0
- data/spec/protocol_path_spec.rb +109 -0
- data/spec/routing_manager_spec.rb +25 -0
- data/spec/routing_table_spec.rb +23 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/transport/udp_spec.rb +44 -0
- data/spec/transport_spec.rb +14 -0
- metadata +187 -0
data/lib/lifx-lan.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "lifx/lan/version"
|
2
|
+
require "bindata"
|
3
|
+
require "bindata_ext/bool"
|
4
|
+
require "bindata_ext/record"
|
5
|
+
|
6
|
+
require "lifx/lan/required_keyword_arguments"
|
7
|
+
require "lifx/lan/utilities"
|
8
|
+
require "lifx/lan/logging"
|
9
|
+
|
10
|
+
require "lifx/lan/thread"
|
11
|
+
|
12
|
+
require "lifx/lan/protocol/payload"
|
13
|
+
%w(device light sensor wan wifi message).each { |f| require "lifx/lan/protocol/#{f}" }
|
14
|
+
require "lifx/lan/protocol/type"
|
15
|
+
require "lifx/lan/message"
|
16
|
+
require "lifx/lan/transport"
|
17
|
+
|
18
|
+
require "lifx/lan/config"
|
19
|
+
require "lifx/lan/client"
|
20
|
+
|
21
|
+
module LIFX
|
22
|
+
module LAN
|
23
|
+
NULL_SITE_ID = "000000000000"
|
24
|
+
|
25
|
+
class TimeoutError < StandardError; end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
require 'lifx/lan/network_context'
|
5
|
+
require 'lifx/lan/light_collection'
|
6
|
+
|
7
|
+
module LIFX
|
8
|
+
module LAN
|
9
|
+
# {LIFX::LAN::Client} is the top level interface to the library. It mainly maps
|
10
|
+
# methods to the backing {NetworkContext} instance.
|
11
|
+
class Client
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Returns a {Client} set up for accessing devices on the LAN
|
15
|
+
#
|
16
|
+
# @return [Client] A LAN LIFX::Client
|
17
|
+
def lan
|
18
|
+
@lan ||= new(transport_manager: TransportManager::LAN.new)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
extend Forwardable
|
23
|
+
include Utilities
|
24
|
+
include RequiredKeywordArguments
|
25
|
+
|
26
|
+
# Refers to the client's network context.
|
27
|
+
# @return [NetworkContext] Enclosed network context
|
28
|
+
attr_reader :context
|
29
|
+
|
30
|
+
# @param transport_manager: [TransportManager] Specify the {TransportManager}
|
31
|
+
def initialize(transport_manager: required!('transport_manager'))
|
32
|
+
@context = NetworkContext.new(transport_manager: transport_manager)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Default timeout in seconds for discovery
|
36
|
+
DISCOVERY_DEFAULT_TIMEOUT = 10
|
37
|
+
|
38
|
+
# This method tells the {NetworkContext} to look for devices asynchronously.
|
39
|
+
# @return [Client] self
|
40
|
+
def discover
|
41
|
+
@context.discover
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_discovery
|
45
|
+
@context.stop_discovery
|
46
|
+
end
|
47
|
+
|
48
|
+
class DiscoveryTimeout < Timeout::Error; end
|
49
|
+
# This method tells the {NetworkContext} to look for devices, and will block
|
50
|
+
# until there's at least one device.
|
51
|
+
#
|
52
|
+
# @example Wait until at least three lights have been found
|
53
|
+
# client.discover! { |c| c.lights.count >= 3 }
|
54
|
+
#
|
55
|
+
# @param timeout: [Numeric] How long to try to wait for before returning
|
56
|
+
# @param condition_interval: [Numeric] Seconds between evaluating the block
|
57
|
+
# @yield [Client] This block is evaluated every `condition_interval` seconds. If true, method returns. If no block is supplied, it will block until it finds at least one light.
|
58
|
+
# @raise [DiscoveryTimeout] If discovery times out
|
59
|
+
# @return [Client] self
|
60
|
+
def discover!(timeout: DISCOVERY_DEFAULT_TIMEOUT, condition_interval: 0.1, &block)
|
61
|
+
block ||= -> { self.lights.count > 0 }
|
62
|
+
try_until -> { block.arity == 1 ? block.call(self) : block.call },
|
63
|
+
timeout: timeout,
|
64
|
+
timeout_exception: DiscoveryTimeout,
|
65
|
+
condition_interval: condition_interval,
|
66
|
+
action_interval: 1 do
|
67
|
+
discover
|
68
|
+
refresh
|
69
|
+
end
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sends a request to refresh devices and tags.
|
74
|
+
# @return [void]
|
75
|
+
def refresh
|
76
|
+
@context.refresh
|
77
|
+
end
|
78
|
+
|
79
|
+
# This method takes a block consisting of multiple asynchronous color or power changing targets
|
80
|
+
# and it will try to schedule them so they run at the same time.
|
81
|
+
#
|
82
|
+
# You cannot nest `sync` calls, nor call synchronous methods inside a `sync` block.
|
83
|
+
#
|
84
|
+
# Due to messaging rate constraints, the amount of messages determine the delay before
|
85
|
+
# the commands are executed. This method also assumes all the lights have the same time.
|
86
|
+
# @example This example sets all the lights to a random colour at the same time.
|
87
|
+
# client.sync do
|
88
|
+
# client.lights.each do |light|
|
89
|
+
# light.set_color(rand(4) * 90, 1, 1)
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# @note This method is in alpha and might go away. Use tags for better group messaging.
|
94
|
+
# @yield Block of commands to synchronize
|
95
|
+
# @return [Float] Number of seconds until commands are executed
|
96
|
+
def sync(**kwargs, &block)
|
97
|
+
@context.sync(**kwargs, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
# This is the same as {#sync}, except it will block until the commands have been executed.
|
101
|
+
# @see #sync
|
102
|
+
# @return [Float] Number of seconds slept
|
103
|
+
def sync!(&block)
|
104
|
+
sync(&block).tap do |delay|
|
105
|
+
sleep(delay)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [LightCollection] Lights available to the client
|
110
|
+
# @see [NetworkContext#lights]
|
111
|
+
def lights
|
112
|
+
context.lights
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [Array<String>] All tags visible to the client
|
116
|
+
# @see [NetworkContext#tags]
|
117
|
+
def tags
|
118
|
+
context.tags
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Array<String>] Tags that are currently unused by known devices
|
122
|
+
# @see [NetworkContext#unused_tags]
|
123
|
+
def unused_tags
|
124
|
+
context.unused_tags
|
125
|
+
end
|
126
|
+
|
127
|
+
# Purges unused tags from the system.
|
128
|
+
# Should only use when all devices are on the network, otherwise
|
129
|
+
# offline devices using their tags will not be tagged correctly.
|
130
|
+
# @return [Array<String>] Tags that were purged
|
131
|
+
def purge_unused_tags!
|
132
|
+
context.purge_unused_tags!
|
133
|
+
end
|
134
|
+
|
135
|
+
# Blocks until all messages have been sent to the gateways
|
136
|
+
# @param timeout: [Numeric] When specified, flush will wait `timeout:` seconds before throwing `Timeout::Error`
|
137
|
+
# @raise [TimeoutError] if `timeout:` was exceeded while waiting for send queue to flush
|
138
|
+
# @return [void]
|
139
|
+
def flush(timeout: nil)
|
140
|
+
context.flush(timeout: timeout)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Stops everything and cleans up.
|
144
|
+
def stop
|
145
|
+
context.stop
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module LIFX
|
2
|
+
module LAN
|
3
|
+
module Colors
|
4
|
+
DEFAULT_KELVIN = 3500
|
5
|
+
|
6
|
+
{
|
7
|
+
red: 0,
|
8
|
+
orange: 36,
|
9
|
+
yellow: 60,
|
10
|
+
green: 120,
|
11
|
+
cyan: 195,
|
12
|
+
blue: 250,
|
13
|
+
purple: 280,
|
14
|
+
pink: 325
|
15
|
+
}.each do |color, hue|
|
16
|
+
define_method(color) do |saturation: 1.0, brightness: 1.0, kelvin: DEFAULT_KELVIN|
|
17
|
+
Color.new(hue, saturation, brightness, kelvin)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Helper to create a white {Color}
|
22
|
+
# @param brightness: [Float] Valid range: `0..1`
|
23
|
+
# @param kelvin: [Integer] Valid range: `2500..9000`
|
24
|
+
# @return [Color]
|
25
|
+
def white(brightness: 1.0, kelvin: DEFAULT_KELVIN)
|
26
|
+
Color.new(0, 0, brightness, kelvin)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Helper to create a random {Color}
|
30
|
+
def random_color(hue: rand(360), saturation: rand, brightness: rand, kelvin: DEFAULT_KELVIN)
|
31
|
+
Color.new(hue, saturation, brightness, kelvin)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# LIFX::Color represents a color intervally by HSBK (Hue, Saturation, Brightness/Value, Kelvin).
|
36
|
+
# It has methods to construct a LIFX::Color instance from various color representations.
|
37
|
+
class Color < Struct.new(:hue, :saturation, :brightness, :kelvin)
|
38
|
+
extend Colors
|
39
|
+
UINT16_MAX = 65535
|
40
|
+
KELVIN_MIN = 2500
|
41
|
+
KELVIN_MAX = 9000
|
42
|
+
|
43
|
+
class << self
|
44
|
+
# Helper method to create from HSB/HSV
|
45
|
+
# @param hue [Float] Valid range: `0..360`
|
46
|
+
# @param saturation [Float] Valid range: `0..1`
|
47
|
+
# @param brightness [Float] Valid range: `0..1`
|
48
|
+
# @return [Color]
|
49
|
+
def hsb(hue, saturation, brightness)
|
50
|
+
new(hue, saturation, brightness, DEFAULT_KELVIN)
|
51
|
+
end
|
52
|
+
alias_method :hsv, :hsb
|
53
|
+
|
54
|
+
# Helper method to create from HSBK/HSVK
|
55
|
+
# @param hue [Float] Valid range: `0..360`
|
56
|
+
# @param saturation [Float] Valid range: `0..1`
|
57
|
+
# @param brightness [Float] Valid range: `0..1`
|
58
|
+
# @param kelvin [Integer] Valid range: `2500..9000`
|
59
|
+
# @return [Color]
|
60
|
+
def hsbk(hue, saturation, brightness, kelvin)
|
61
|
+
new(hue, saturation, brightness, kelvin)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Helper method to create from HSL
|
65
|
+
# @param hue [Float] Valid range: `0..360`
|
66
|
+
# @param saturation [Float] Valid range: `0..1`
|
67
|
+
# @param luminance [Float] Valid range: `0..1`
|
68
|
+
# @return [Color]
|
69
|
+
def hsl(hue, saturation, luminance)
|
70
|
+
# From: http://ariya.blogspot.com.au/2008/07/converting-between-hsl-and-hsv.html
|
71
|
+
l = luminance * 2
|
72
|
+
saturation *= (l <= 1) ? l : 2 - l
|
73
|
+
brightness = (l + saturation) / 2
|
74
|
+
saturation = (2 * saturation) / (l + saturation)
|
75
|
+
new(hue, saturation, brightness, DEFAULT_KELVIN)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Helper method to create from RGB.
|
79
|
+
# @note RGB is not the recommended way to create colors
|
80
|
+
# @param r [Integer] Red. Valid range: `0..255`
|
81
|
+
# @param g [Integer] Green. Valid range: `0..255`
|
82
|
+
# @param b [Integer] Blue. Valid range: `0..255`
|
83
|
+
# @return [Color]
|
84
|
+
def rgb(r, g, b)
|
85
|
+
r = r / 255.0
|
86
|
+
g = g / 255.0
|
87
|
+
b = b / 255.0
|
88
|
+
|
89
|
+
max = [r, g, b].max
|
90
|
+
min = [r, g, b].min
|
91
|
+
|
92
|
+
h = s = v = max
|
93
|
+
d = max - min
|
94
|
+
s = max.zero? ? 0 : d / max
|
95
|
+
|
96
|
+
if max == min
|
97
|
+
h = 0
|
98
|
+
else
|
99
|
+
case max
|
100
|
+
when r
|
101
|
+
h = (g - b) / d + (g < b ? 6 : 0)
|
102
|
+
when g
|
103
|
+
h = (b - r) / d + 2
|
104
|
+
when b
|
105
|
+
h = (r - g) / d + 4
|
106
|
+
end
|
107
|
+
h = h * 60
|
108
|
+
end
|
109
|
+
|
110
|
+
new(h, s, v, DEFAULT_KELVIN)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates an instance from a {Protocol::Light::Hsbk} struct
|
114
|
+
# @api private
|
115
|
+
# @param hsbk [Protocol::Light::Hsbk]
|
116
|
+
# @return [Color]
|
117
|
+
def from_struct(hsbk)
|
118
|
+
new(
|
119
|
+
(hsbk.hue.to_f / UINT16_MAX) * 360,
|
120
|
+
(hsbk.saturation.to_f / UINT16_MAX),
|
121
|
+
(hsbk.brightness.to_f / UINT16_MAX),
|
122
|
+
hsbk.kelvin
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(hue, saturation, brightness, kelvin)
|
128
|
+
hue = hue % 360
|
129
|
+
super(hue, saturation, brightness, kelvin)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns a new Color with the hue changed while keeping other attributes
|
133
|
+
# @param hue [Float] Hue in degrees. `0..360`
|
134
|
+
# @return [Color]
|
135
|
+
def with_hue(hue)
|
136
|
+
Color.new(hue, saturation, brightness, kelvin)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns a new Color with the saturaiton changed while keeping other attributes
|
140
|
+
# @param saturaiton [Float] Saturation as float. `0..1`
|
141
|
+
# @return [Color]
|
142
|
+
def with_saturation(saturation)
|
143
|
+
Color.new(hue, saturation, brightness, kelvin)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns a new Color with the brightness changed while keeping other attributes
|
147
|
+
# @param brightness [Float] Brightness as float. `0..1`
|
148
|
+
# @return [Color]
|
149
|
+
def with_brightness(brightness)
|
150
|
+
Color.new(hue, saturation, brightness, kelvin)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns a new Color with the kelvin changed while keeping other attributes
|
154
|
+
# @param kelvin [Integer] Kelvin. `2500..9000`
|
155
|
+
# @return [Color]
|
156
|
+
def with_kelvin(kelvin)
|
157
|
+
Color.new(hue, saturation, brightness, kelvin)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns a struct for use by the protocol
|
161
|
+
# @api private
|
162
|
+
# @return [Protocol::Light::Hsbk]
|
163
|
+
def to_hsbk
|
164
|
+
Protocol::Light::Hsbk.new(
|
165
|
+
hue: (hue / 360.0 * UINT16_MAX).to_i,
|
166
|
+
saturation: (saturation * UINT16_MAX).to_i,
|
167
|
+
brightness: (brightness * UINT16_MAX).to_i,
|
168
|
+
kelvin: [KELVIN_MIN, kelvin.to_i, KELVIN_MAX].sort[1]
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns hue, saturation, brightness and kelvin in an array
|
173
|
+
# @return [Array<Float, Float, Float, Integer>]
|
174
|
+
def to_a
|
175
|
+
[hue, saturation, brightness, kelvin]
|
176
|
+
end
|
177
|
+
|
178
|
+
DEFAULT_SIMILAR_THRESHOLD = 0.001 # 0.1% variance
|
179
|
+
# Checks if colours are equal to 0.1% variance
|
180
|
+
# @param other [Color] Color to compare to
|
181
|
+
# @param threshold: [Float] 0..1. Threshold to consider it similar
|
182
|
+
# @return [Boolean]
|
183
|
+
def similar_to?(other, threshold: DEFAULT_SIMILAR_THRESHOLD)
|
184
|
+
return false unless other.is_a?(Color)
|
185
|
+
conditions = []
|
186
|
+
|
187
|
+
conditions << (((hue - other.hue).abs < (threshold * 360)) || begin
|
188
|
+
# FIXME: Surely there's a better way.
|
189
|
+
hues = [hue, other.hue].sort
|
190
|
+
hues[0] += 360
|
191
|
+
(hues[0] - hues[1]).abs < (threshold * 360)
|
192
|
+
end)
|
193
|
+
conditions << ((saturation - other.saturation).abs < threshold)
|
194
|
+
conditions << ((brightness - other.brightness).abs < threshold)
|
195
|
+
conditions.all?
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'configatron/core'
|
2
|
+
require 'logger'
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
Config = Configatron::Store.new
|
6
|
+
|
7
|
+
Config.default_duration = 1
|
8
|
+
Config.message_wait_timeout = 3
|
9
|
+
Config.message_retry_interval = 0.5
|
10
|
+
Config.broadcast_ip = '255.255.255.255'
|
11
|
+
Config.allowed_transports = [:udp, :tcp]
|
12
|
+
Config.log_invalid_messages = true
|
13
|
+
Config.logger = Logger.new(STDERR).tap do |logger|
|
14
|
+
logger.level = Logger::WARN
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
# LIFX::LAN::Firmware handles decoding firmware payloads
|
6
|
+
# @private
|
7
|
+
class Firmware < Struct.new(:build_time, :major, :minor)
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
def initialize(payload)
|
11
|
+
self.build_time = decode_time(payload.build)
|
12
|
+
self.major = (payload.version >> 0x10)
|
13
|
+
self.minor = (payload.version & 0xFF)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#<Firmware version=#{self.major}.#{self.minor}>"
|
18
|
+
end
|
19
|
+
alias_method :inspect, :to_s
|
20
|
+
|
21
|
+
def <=>(obj)
|
22
|
+
case obj
|
23
|
+
when String
|
24
|
+
major, minor = obj.split('.', 2).map(&:to_i)
|
25
|
+
[self.major, self.minor] <=> [major, minor]
|
26
|
+
when Firmware
|
27
|
+
[self.major, self.minor] <=> [obj.major, obj.minor]
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def decode_time(int)
|
37
|
+
if int < 1300000000000000000
|
38
|
+
year = byte(int, 56) + 2000
|
39
|
+
month = bytes(int, 48, 40, 32).map(&:chr).join
|
40
|
+
day = byte(int, 24)
|
41
|
+
hour = byte(int, 16)
|
42
|
+
min = byte(int, 8)
|
43
|
+
sec = byte(int, 0)
|
44
|
+
# Don't want to pull in DateTime just for DateTime.new
|
45
|
+
Time.parse("%s %d %04d, %02d:%02d:%02d" % [month, day, year, hour, min, sec])
|
46
|
+
else
|
47
|
+
Time.at(int / 1000000000)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def byte(n, pos)
|
52
|
+
0xFF & (n >> pos)
|
53
|
+
end
|
54
|
+
|
55
|
+
def bytes(n, *range)
|
56
|
+
range.map {|r| byte(n, r)}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|