lifx 0.0.1 → 0.4.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 +4 -4
- data/.yardopts +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +71 -13
- data/Rakefile +12 -0
- data/bin/lifx-console +15 -0
- data/bin/lifx-snoop +50 -0
- data/examples/auto-off/Gemfile +3 -0
- data/examples/auto-off/auto-off.rb +35 -0
- data/examples/identify/Gemfile +3 -0
- data/examples/identify/identify.rb +70 -0
- data/examples/travis-build-light/Gemfile +4 -0
- data/examples/travis-build-light/build-light.rb +57 -0
- data/lib/bindata_ext/bool.rb +29 -0
- data/lib/bindata_ext/record.rb +11 -0
- data/lib/lifx/client.rb +136 -0
- data/lib/lifx/color.rb +190 -0
- data/lib/lifx/config.rb +12 -0
- data/lib/lifx/firmware.rb +55 -0
- data/lib/lifx/gateway_connection.rb +177 -0
- data/lib/lifx/light.rb +406 -0
- data/lib/lifx/light_collection.rb +105 -0
- data/lib/lifx/light_target.rb +189 -0
- data/lib/lifx/logging.rb +11 -0
- data/lib/lifx/message.rb +166 -0
- data/lib/lifx/network_context.rb +200 -0
- data/lib/lifx/observable.rb +46 -0
- data/lib/lifx/protocol/address.rb +21 -0
- data/lib/lifx/protocol/device.rb +225 -0
- data/lib/lifx/protocol/header.rb +24 -0
- data/lib/lifx/protocol/light.rb +110 -0
- data/lib/lifx/protocol/message.rb +17 -0
- data/lib/lifx/protocol/metadata.rb +21 -0
- data/lib/lifx/protocol/payload.rb +7 -0
- data/lib/lifx/protocol/sensor.rb +29 -0
- data/lib/lifx/protocol/type.rb +134 -0
- data/lib/lifx/protocol/wan.rb +50 -0
- data/lib/lifx/protocol/wifi.rb +76 -0
- data/lib/lifx/protocol_path.rb +84 -0
- data/lib/lifx/routing_manager.rb +110 -0
- data/lib/lifx/routing_table.rb +33 -0
- data/lib/lifx/seen.rb +15 -0
- data/lib/lifx/site.rb +89 -0
- data/lib/lifx/tag_manager.rb +105 -0
- data/lib/lifx/tag_table.rb +47 -0
- data/lib/lifx/target.rb +23 -0
- data/lib/lifx/timers.rb +18 -0
- data/lib/lifx/transport/tcp.rb +81 -0
- data/lib/lifx/transport/udp.rb +67 -0
- data/lib/lifx/transport.rb +41 -0
- data/lib/lifx/transport_manager/lan.rb +140 -0
- data/lib/lifx/transport_manager.rb +34 -0
- data/lib/lifx/utilities.rb +33 -0
- data/lib/lifx/version.rb +1 -1
- data/lib/lifx.rb +15 -1
- data/lifx.gemspec +11 -7
- data/spec/color_spec.rb +45 -0
- data/spec/gateway_connection_spec.rb +32 -0
- data/spec/integration/client_spec.rb +40 -0
- data/spec/integration/light_spec.rb +43 -0
- data/spec/integration/tags_spec.rb +31 -0
- data/spec/message_spec.rb +163 -0
- data/spec/protocol_path_spec.rb +109 -0
- data/spec/routing_manager_spec.rb +22 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/transport/udp_spec.rb +38 -0
- data/spec/transport_spec.rb +14 -0
- metadata +143 -26
    
        data/lib/lifx/color.rb
    ADDED
    
    | @@ -0,0 +1,190 @@ | |
| 1 | 
            +
            module LIFX
         | 
| 2 | 
            +
              module Colors
         | 
| 3 | 
            +
                DEFAULT_KELVIN = 3500
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                {
         | 
| 6 | 
            +
                  red: 0,
         | 
| 7 | 
            +
                  orange: 36,
         | 
| 8 | 
            +
                  yellow: 60,
         | 
| 9 | 
            +
                  green: 120,
         | 
| 10 | 
            +
                  cyan: 195,
         | 
| 11 | 
            +
                  blue: 250,
         | 
| 12 | 
            +
                  purple: 280,
         | 
| 13 | 
            +
                  pink: 325
         | 
| 14 | 
            +
                }.each do |color, hue|
         | 
| 15 | 
            +
                  define_method(color) do |saturation: 1.0, brightness: 1.0, kelvin: DEFAULT_KELVIN|
         | 
| 16 | 
            +
                    Color.new(hue, saturation, brightness, kelvin)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Helper to create a white {Color}
         | 
| 21 | 
            +
                # @param brightness: [Float] Valid range: `0..1`
         | 
| 22 | 
            +
                # @param kelvin: [Integer] Valid range: `2500..10000`
         | 
| 23 | 
            +
                # @return [Color]
         | 
| 24 | 
            +
                def white(brightness: 1.0, kelvin: DEFAULT_KELVIN)
         | 
| 25 | 
            +
                  Color.new(0, 0, brightness, kelvin)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                # Helper to create a random {Color}
         | 
| 29 | 
            +
                def random_color(hue: rand(360), saturation: rand, brightness: rand, kelvin: DEFAULT_KELVIN)
         | 
| 30 | 
            +
                  Color.new(hue, saturation, brightness, kelvin)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              # LIFX::Color represents a color intervally by HSBK (Hue, Saturation, Brightness/Value, Kelvin).
         | 
| 35 | 
            +
              # It has methods to construct a LIFX::Color instance from various color representations.
         | 
| 36 | 
            +
              class Color < Struct.new(:hue, :saturation, :brightness, :kelvin)
         | 
| 37 | 
            +
                extend Colors
         | 
| 38 | 
            +
                UINT16_MAX = 65535
         | 
| 39 | 
            +
                KELVIN_MIN = 2500
         | 
| 40 | 
            +
                KELVIN_MAX = 10000
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                class << self
         | 
| 43 | 
            +
                  # Helper method to create from HSB/HSV
         | 
| 44 | 
            +
                  # @param hue [Float] Valid range: `0..360`
         | 
| 45 | 
            +
                  # @param saturation [Float] Valid range: `0..1`
         | 
| 46 | 
            +
                  # @param brightness [Float] Valid range: `0..1`
         | 
| 47 | 
            +
                  # @return [Color]
         | 
| 48 | 
            +
                  def hsb(hue, saturation, brightness)
         | 
| 49 | 
            +
                    new(hue, saturation, brightness, DEFAULT_KELVIN)
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  alias_method :hsv, :hsb
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  # Helper method to create from HSBK/HSVK
         | 
| 54 | 
            +
                  # @param hue [Float] Valid range: `0..360`
         | 
| 55 | 
            +
                  # @param saturation [Float] Valid range: `0..1`
         | 
| 56 | 
            +
                  # @param brightness [Float] Valid range: `0..1`
         | 
| 57 | 
            +
                  # @param kelvin [Integer] Valid range: `2500..10000`
         | 
| 58 | 
            +
                  # @return [Color]
         | 
| 59 | 
            +
                  def hsbk(hue, saturation, brightness, kelvin)
         | 
| 60 | 
            +
                    new(hue, saturation, brightness, kelvin)
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  # Helper method to create from HSL
         | 
| 64 | 
            +
                  # @param hue [Float] Valid range: `0..360`
         | 
| 65 | 
            +
                  # @param saturation [Float] Valid range: `0..1`
         | 
| 66 | 
            +
                  # @param luminance [Float] Valid range: `0..1`
         | 
| 67 | 
            +
                  # @return [Color]
         | 
| 68 | 
            +
                  def hsl(hue, saturation, luminance)
         | 
| 69 | 
            +
                    # From: http://ariya.blogspot.com.au/2008/07/converting-between-hsl-and-hsv.html
         | 
| 70 | 
            +
                    l = luminance * 2
         | 
| 71 | 
            +
                    saturation *= (l <= 1) ? l : 2 - l
         | 
| 72 | 
            +
                    brightness = (l + saturation) / 2
         | 
| 73 | 
            +
                    saturation = (2 * saturation) / (l + saturation)
         | 
| 74 | 
            +
                    new(hue, saturation, brightness, DEFAULT_KELVIN)
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  # Helper method to create from RGB.
         | 
| 78 | 
            +
                  # @note RGB is not the recommended way to create colors
         | 
| 79 | 
            +
                  # @param r [Integer] Red. Valid range: `0..255`
         | 
| 80 | 
            +
                  # @param g [Integer] Green. Valid range: `0..255`
         | 
| 81 | 
            +
                  # @param b [Integer] Blue. Valid range: `0..255`
         | 
| 82 | 
            +
                  # @return [Color]
         | 
| 83 | 
            +
                  def rgb(r, g, b)
         | 
| 84 | 
            +
                    r = r / 255.0
         | 
| 85 | 
            +
                    g = g / 255.0
         | 
| 86 | 
            +
                    b = b / 255.0
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    max = [r, g, b].max
         | 
| 89 | 
            +
                    min = [r, g, b].min
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    h = s = v = max
         | 
| 92 | 
            +
                    d = max - min
         | 
| 93 | 
            +
                    s = max.zero? ? 0 : d / max
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    if max == min
         | 
| 96 | 
            +
                      h = 0
         | 
| 97 | 
            +
                    else
         | 
| 98 | 
            +
                      case max
         | 
| 99 | 
            +
                      when r
         | 
| 100 | 
            +
                        h = (g - b) / d + (g < b ? 6 : 0)
         | 
| 101 | 
            +
                      when g
         | 
| 102 | 
            +
                        h = (b - r) / d + 2
         | 
| 103 | 
            +
                      when b
         | 
| 104 | 
            +
                        h = (r - g) / d + 4
         | 
| 105 | 
            +
                      end
         | 
| 106 | 
            +
                      h = h * 60
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    new(h, s, v, DEFAULT_KELVIN)
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  # Creates an instance from a {Protocol::Light::Hsbk} struct
         | 
| 113 | 
            +
                  # @api private
         | 
| 114 | 
            +
                  # @param hsbk [Protocol::Light::Hsbk]
         | 
| 115 | 
            +
                  # @return [Color]
         | 
| 116 | 
            +
                  def from_struct(hsbk)
         | 
| 117 | 
            +
                    new(
         | 
| 118 | 
            +
                      (hsbk.hue.to_f / UINT16_MAX) * 360,
         | 
| 119 | 
            +
                      (hsbk.saturation.to_f / UINT16_MAX),
         | 
| 120 | 
            +
                      (hsbk.brightness.to_f / UINT16_MAX),
         | 
| 121 | 
            +
                      hsbk.kelvin
         | 
| 122 | 
            +
                    )
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def initialize(hue, saturation, brightness, kelvin)
         | 
| 127 | 
            +
                  hue = hue % 360
         | 
| 128 | 
            +
                  super(hue, saturation, brightness, kelvin)
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                # Returns a new Color with the hue changed while keeping other attributes
         | 
| 132 | 
            +
                # @param hue [Float] Hue in degrees. `0..360`
         | 
| 133 | 
            +
                # @return [Color]
         | 
| 134 | 
            +
                def with_hue(hue)
         | 
| 135 | 
            +
                  Color.new(hue, saturation, brightness, kelvin)
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                # Returns a new Color with the saturaiton changed while keeping other attributes
         | 
| 139 | 
            +
                # @param saturaiton [Float] Saturation as float. `0..1`
         | 
| 140 | 
            +
                # @return [Color]
         | 
| 141 | 
            +
                def with_saturation(saturation)
         | 
| 142 | 
            +
                  Color.new(hue, saturation, brightness, kelvin)
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                # Returns a new Color with the brightness changed while keeping other attributes
         | 
| 146 | 
            +
                # @param brightness [Float] Brightness as float. `0..1`
         | 
| 147 | 
            +
                # @return [Color]
         | 
| 148 | 
            +
                def with_brightness(brightness)
         | 
| 149 | 
            +
                  Color.new(hue, saturation, brightness, kelvin)
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                # Returns a new Color with the kelvin changed while keeping other attributes
         | 
| 153 | 
            +
                # @param kelvin [Integer] Kelvin. `2500..10000`
         | 
| 154 | 
            +
                # @return [Color]
         | 
| 155 | 
            +
                def with_kelvin(kelvin)
         | 
| 156 | 
            +
                  Color.new(hue, saturation, brightness, kelvin)
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                # Returns a struct for use by the protocol
         | 
| 160 | 
            +
                # @api private
         | 
| 161 | 
            +
                # @return [Protocol::Light::Hsbk]
         | 
| 162 | 
            +
                def to_hsbk
         | 
| 163 | 
            +
                  Protocol::Light::Hsbk.new(
         | 
| 164 | 
            +
                    hue: (hue / 360.0 * UINT16_MAX).to_i,
         | 
| 165 | 
            +
                    saturation: (saturation * UINT16_MAX).to_i,
         | 
| 166 | 
            +
                    brightness: (brightness * UINT16_MAX).to_i,
         | 
| 167 | 
            +
                    kelvin: [KELVIN_MIN, kelvin.to_i, KELVIN_MAX].sort[1]
         | 
| 168 | 
            +
                  )
         | 
| 169 | 
            +
                end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                # Returns hue, saturation, brightness and kelvin in an array
         | 
| 172 | 
            +
                # @return [Array<Float, Float, Float, Integer>]
         | 
| 173 | 
            +
                def to_a
         | 
| 174 | 
            +
                  [hue, saturation, brightness, kelvin]
         | 
| 175 | 
            +
                end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                EQUALITY_THRESHOLD = 0.001 # 0.1% variance
         | 
| 178 | 
            +
                # Checks if colours are equal to 0.1% variance
         | 
| 179 | 
            +
                # @param other [Color] Color to compare to
         | 
| 180 | 
            +
                # @return [Boolean]
         | 
| 181 | 
            +
                def ==(other)
         | 
| 182 | 
            +
                  return false unless other.is_a?(Color)
         | 
| 183 | 
            +
                  conditions = []
         | 
| 184 | 
            +
                  conditions << ((hue - other.hue).abs < (EQUALITY_THRESHOLD * 360)) 
         | 
| 185 | 
            +
                  conditions << ((saturation - other.saturation).abs < EQUALITY_THRESHOLD)
         | 
| 186 | 
            +
                  conditions << ((brightness - other.brightness).abs < EQUALITY_THRESHOLD)
         | 
| 187 | 
            +
                  conditions.all?
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
              end
         | 
| 190 | 
            +
            end
         | 
    
        data/lib/lifx/config.rb
    ADDED
    
    | @@ -0,0 +1,12 @@ | |
| 1 | 
            +
            require 'configatron/core'
         | 
| 2 | 
            +
            require 'yell'
         | 
| 3 | 
            +
            module LIFX
         | 
| 4 | 
            +
              Config = Configatron::Store.new
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              Config.default_duration = 1
         | 
| 7 | 
            +
              Config.allowed_transports = [:udp, :tcp]
         | 
| 8 | 
            +
              Config.logger = Yell.new do |logger|
         | 
| 9 | 
            +
                logger.level = 'gte.warn'
         | 
| 10 | 
            +
                logger.adapter STDERR, format: '%d [%5L] %p/%t : %m'
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
            end
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            module LIFX
         | 
| 2 | 
            +
              # LIFX::Firmware handles decoding firmware payloads
         | 
| 3 | 
            +
              class Firmware < Struct.new(:build_time, :major, :minor)
         | 
| 4 | 
            +
                include Comparable
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def initialize(payload)
         | 
| 7 | 
            +
                  self.build_time = decode_time(payload.build)
         | 
| 8 | 
            +
                  self.major  = (payload.version >> 0x10)
         | 
| 9 | 
            +
                  self.minor  = (payload.version &  0xFF)
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def to_s
         | 
| 13 | 
            +
                  "#<Firmware version=#{self.major}.#{self.minor}>"
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
                alias_method :inspect, :to_s
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def <=>(obj)
         | 
| 18 | 
            +
                  case obj
         | 
| 19 | 
            +
                  when String
         | 
| 20 | 
            +
                    major, minor = obj.split('.', 2).map(&:to_i)
         | 
| 21 | 
            +
                    [self.major, self.minor] <=> [major, minor]
         | 
| 22 | 
            +
                  when Firmware
         | 
| 23 | 
            +
                    [self.major, self.minor] <=> [obj.major, obj.minor]
         | 
| 24 | 
            +
                  else
         | 
| 25 | 
            +
                    nil
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
             | 
| 30 | 
            +
                protected
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def decode_time(int)
         | 
| 33 | 
            +
                  if int < 1300000000000000000
         | 
| 34 | 
            +
                    year   = byte(int, 56) + 2000
         | 
| 35 | 
            +
                    month  = bytes(int, 48, 40, 32).map(&:chr).join
         | 
| 36 | 
            +
                    day    = byte(int, 24)
         | 
| 37 | 
            +
                    hour   = byte(int, 16)
         | 
| 38 | 
            +
                    min    = byte(int,  8)
         | 
| 39 | 
            +
                    sec    = byte(int,  0)
         | 
| 40 | 
            +
                    # Don't want to pull in DateTime just for DateTime.new
         | 
| 41 | 
            +
                    Time.parse("%s %d %04d, %02d:%02d:%02d" % [month, day, year, hour, min, sec])
         | 
| 42 | 
            +
                  else
         | 
| 43 | 
            +
                    Time.at(int / 1000000000)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def byte(n, pos)
         | 
| 48 | 
            +
                  0xFF & (n >> pos)
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def bytes(n, *range)
         | 
| 52 | 
            +
                  range.map {|r| byte(n, r)}
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,177 @@ | |
| 1 | 
            +
            require 'lifx/observable'
         | 
| 2 | 
            +
            require 'lifx/timers'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module LIFX
         | 
| 5 | 
            +
              # @api private
         | 
| 6 | 
            +
              class GatewayConnection
         | 
| 7 | 
            +
                # GatewayConnection handles the UDP and TCP connections to the gateway
         | 
| 8 | 
            +
                # A GatewayConnection is created when a new device sends a StatePanGateway
         | 
| 9 | 
            +
                include Timers
         | 
| 10 | 
            +
                include Logging
         | 
| 11 | 
            +
                include Observable
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                MAX_TCP_ATTEMPTS = 3
         | 
| 14 | 
            +
                def initialize
         | 
| 15 | 
            +
                  @threads = []
         | 
| 16 | 
            +
                  @tcp_attempts = 0
         | 
| 17 | 
            +
                  @threads << initialize_write_queue
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def handle_message(message, ip, transport)
         | 
| 21 | 
            +
                  payload = message.payload
         | 
| 22 | 
            +
                  case payload
         | 
| 23 | 
            +
                  when Protocol::Device::StatePanGateway
         | 
| 24 | 
            +
                    if use_udp? && !udp_connected? && payload.service == Protocol::Device::Service::UDP
         | 
| 25 | 
            +
                      # UDP transport here is only for sending directly to bulb
         | 
| 26 | 
            +
                      # We receive responses via UDP transport listening to broadcast in Network
         | 
| 27 | 
            +
                      connect_udp(ip, payload.port.to_i)
         | 
| 28 | 
            +
                    elsif use_tcp? && !tcp_connected? && payload.service == Protocol::Device::Service::TCP && (port = payload.port.snapshot) > 0
         | 
| 29 | 
            +
                      connect_tcp(ip, port)
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  else
         | 
| 32 | 
            +
                    logger.error("#{self}: Unhandled message: #{message}")
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def use_udp?
         | 
| 37 | 
            +
                  Config.allowed_transports.include?(:udp)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def use_tcp?
         | 
| 41 | 
            +
                  Config.allowed_transports.include?(:tcp)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def udp_connected?
         | 
| 45 | 
            +
                  @udp_transport && @udp_transport.connected?
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def tcp_connected?
         | 
| 49 | 
            +
                  @tcp_transport && @tcp_transport.connected?
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def connected?
         | 
| 53 | 
            +
                  udp_connected? || tcp_connected?
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def connect_udp(ip, port)
         | 
| 57 | 
            +
                  @udp_transport = Transport::UDP.new(ip, port)
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def connect_tcp(ip, port)
         | 
| 61 | 
            +
                  if @tcp_attempts > MAX_TCP_ATTEMPTS
         | 
| 62 | 
            +
                    logger.info("#{self}: Ignoring TCP service of #{ip}:#{port} due to too many failed attempts.")
         | 
| 63 | 
            +
                    return
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                  @tcp_attempts += 1
         | 
| 66 | 
            +
                  logger.info("#{self}: Establishing connection to #{ip}:#{port}")
         | 
| 67 | 
            +
                  @tcp_transport = Transport::TCP.new(ip, port)
         | 
| 68 | 
            +
                  @tcp_transport.add_observer(self) do |message:, ip:, transport:|
         | 
| 69 | 
            +
                    notify_observers(message: message, ip: ip, transport: @tcp_transport)
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                  @tcp_transport.listen
         | 
| 72 | 
            +
                  at_exit do
         | 
| 73 | 
            +
                    @tcp_transport.close if @tcp_transport
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def write(message)
         | 
| 78 | 
            +
                  @queue.push(message)
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def close
         | 
| 82 | 
            +
                  @threads.each { |thr| Thread.kill(thr) }
         | 
| 83 | 
            +
                  [@tcp_transport, @udp_transport].compact.each(&:close)
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def flush(timeout: nil)
         | 
| 87 | 
            +
                  proc = lambda do
         | 
| 88 | 
            +
                    while !@queue.empty?
         | 
| 89 | 
            +
                      sleep 0.05
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
                  if timeout
         | 
| 93 | 
            +
                    Timeout.timeout(timeout) do
         | 
| 94 | 
            +
                      proc.call
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
                  else
         | 
| 97 | 
            +
                    proc.call
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def to_s
         | 
| 102 | 
            +
                  "#<LIFX::GatewayConnection tcp=#{@tcp_transport} tcp_attempts=#{@tcp_attempts} udp=#{@udp_transport}>"
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
                alias_method :inspect, :to_s
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                def set_message_rate(rate)
         | 
| 107 | 
            +
                  @message_rate = rate
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
                protected
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                MAXIMUM_QUEUE_LENGTH   = 10
         | 
| 112 | 
            +
                DEFAULT_MESSAGE_RATE = 5
         | 
| 113 | 
            +
                def message_rate
         | 
| 114 | 
            +
                  @message_rate || DEFAULT_MESSAGE_RATE
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def initialize_write_queue
         | 
| 118 | 
            +
                  @queue = SizedQueue.new(MAXIMUM_QUEUE_LENGTH)
         | 
| 119 | 
            +
                  @last_write = Time.now
         | 
| 120 | 
            +
                  Thread.abort_on_exception = true
         | 
| 121 | 
            +
                  Thread.new do
         | 
| 122 | 
            +
                    loop do
         | 
| 123 | 
            +
                      if !connected?
         | 
| 124 | 
            +
                        sleep 0.1
         | 
| 125 | 
            +
                        next
         | 
| 126 | 
            +
                      end
         | 
| 127 | 
            +
                      delay = [(1.0 / message_rate) - (Time.now - @last_write), 0].max
         | 
| 128 | 
            +
                      logger.debug("#{self}: Sleeping for #{delay}")
         | 
| 129 | 
            +
                      sleep(delay)
         | 
| 130 | 
            +
                      message = @queue.pop
         | 
| 131 | 
            +
                      if !message.is_a?(Message)
         | 
| 132 | 
            +
                        raise ArgumentError.new("Unexpected object in message queue: #{message.inspect}")
         | 
| 133 | 
            +
                      end
         | 
| 134 | 
            +
                      if !actually_write(message)
         | 
| 135 | 
            +
                        logger.error("#{self}: Couldn't write, pushing back onto queue.")
         | 
| 136 | 
            +
                        @queue << message
         | 
| 137 | 
            +
                      end
         | 
| 138 | 
            +
                      @last_write = Time.now
         | 
| 139 | 
            +
                    end
         | 
| 140 | 
            +
                  end
         | 
| 141 | 
            +
                end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                def check_connections
         | 
| 144 | 
            +
                  if @tcp_transport && !tcp_connected?
         | 
| 145 | 
            +
                    @tcp_transport = nil
         | 
| 146 | 
            +
                    logger.info("#{self}: TCP connection dropped, clearing.")
         | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                  if @udp_transport && !udp_connected?
         | 
| 150 | 
            +
                    @udp_transport = nil
         | 
| 151 | 
            +
                    logger.info("#{self}: UDP connection dropped, clearing.")
         | 
| 152 | 
            +
                  end
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def actually_write(message)
         | 
| 156 | 
            +
                  check_connections
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                  # TODO: Support force sending over UDP
         | 
| 159 | 
            +
                  if tcp_connected?
         | 
| 160 | 
            +
                    if @tcp_transport.write(message)
         | 
| 161 | 
            +
                      logger.debug("-> #{self} #{@tcp_transport}: #{message}")
         | 
| 162 | 
            +
                      return true
         | 
| 163 | 
            +
                    end
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  if udp_connected?
         | 
| 167 | 
            +
                    if @udp_transport.write(message)
         | 
| 168 | 
            +
                      logger.debug("-> #{self} #{@tcp_transport}: #{message}")
         | 
| 169 | 
            +
                      return true
         | 
| 170 | 
            +
                    end
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                  false
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
              end
         | 
| 177 | 
            +
            end
         |