usblamp 0.1.1

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.
data/MANIFEST ADDED
@@ -0,0 +1,24 @@
1
+ .gitignore
2
+ .rubocop.yml
3
+ .travis.yml
4
+ Gemfile
5
+ LICENSE.md
6
+ MANIFEST
7
+ README.md
8
+ Rakefile
9
+ bin/usblamp
10
+ lib/usblamp.rb
11
+ lib/usblamp/cli.rb
12
+ lib/usblamp/cli/blink.rb
13
+ lib/usblamp/cli/fadein.rb
14
+ lib/usblamp/color.rb
15
+ lib/usblamp/controller.rb
16
+ lib/usblamp/version.rb
17
+ spec/spec_helper.rb
18
+ spec/usblamp/cli_spec.rb
19
+ spec/usblamp/color_spec.rb
20
+ spec/usblamp/controller_spec.rb
21
+ task/manifest.rake
22
+ task/rspec.rake
23
+ task/rubocop.rake
24
+ usblamp.gemspec
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ #WebMail Notifier with Ruby (Dream Cheeky)
2
+
3
+ [![Build Status](https://travis-ci.org/PierreRambaud/usblamp.png?branch=master)](https://travis-ci.org/PierreRambaud/usblamp)
4
+
5
+ Ruby script to power the Dreamcheeky USB webmail notifier gadget. <http://www.dreamcheeky.com/webmail-notifier>
6
+
7
+ ##Requirements
8
+
9
+ - Ruby 1.9.2 or newer
10
+
11
+ ##Installation
12
+
13
+ Assuming RubyGems isn't down you can install the Gem as following:
14
+
15
+ ```
16
+ $ gem install usblamp
17
+ ```
18
+
19
+ ##Usage
20
+
21
+ ```
22
+ Usage: usblamp [COMMAND] [OPTIONS]
23
+
24
+ Options:
25
+
26
+ -v, --version Shows the current version
27
+ -r, --red Red
28
+ -g, --green Green
29
+ -b, --blue Blue
30
+ -c, --color Color
31
+ -h, --help Display this help message.
32
+
33
+ Available commands:
34
+
35
+ fadein Fade in effect
36
+ blink Blink effect
37
+ ```
38
+
39
+ ##Troubleshooting
40
+
41
+ Should be run as root unless the necessary udev rules are set.
42
+ Create the file `/etc/udev/rules.d/42-usblamp.rules`
43
+ And add this content by replacing `got` by your username:
44
+ ```
45
+ SUBSYSTEM !="usb_device", ACTION !="add", GOTO="datalogger_rules_end"
46
+ SYSFS{idVendor} =="1d34", SYSFS{idProduct} =="0004", SYMLINK+="datalogger"
47
+ MODE="0666", OWNER="got", GROUP="root"
48
+ LABEL="datalogger_rules_end"
49
+ ```
50
+
51
+ ## Running tests
52
+
53
+ Install dependencies:
54
+
55
+ `$ bundle install`
56
+
57
+ To run tests:
58
+ `$ bundle exec rake`
59
+
60
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'bundler/gem_tasks'
3
+
4
+ GEMSPEC = Gem::Specification.load('usblamp.gemspec')
5
+
6
+ Dir['./task/*.rake'].each do |task|
7
+ import(task)
8
+ end
9
+
10
+ task default: [:rubocop, :spec]
data/bin/usblamp ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ require File.expand_path('../../lib/usblamp', __FILE__)
5
+ Usblamp::CLI.slop.parse
@@ -0,0 +1,20 @@
1
+ # -*- coding: utf-8 -*-
2
+ Usblamp::CLI.slop.command 'blink' do
3
+ banner 'Usage: usblamp blink [OPTIONS]'
4
+ description 'Blink effect'
5
+ separator "\nOptions:\n"
6
+
7
+ Usblamp::CLI.slop.options.each do |c|
8
+ next if c.long == 'version'
9
+ options << c
10
+ end
11
+
12
+ on :t=, :times=, 'Times', as: Integer, default: 2
13
+
14
+ run do |opts, _args|
15
+ lamp = Usblamp::Controller.new
16
+ lamp.open
17
+ lamp.switch_off
18
+ lamp.blink(opts[:t], lamp.parse(opts))
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # -*- coding: utf-8 -*-
2
+ Usblamp::CLI.slop.command 'fadein' do
3
+ banner 'Usage: usblamp fadein [OPTIONS]'
4
+ description 'Fade in effect'
5
+ separator "\nOptions:\n"
6
+
7
+ Usblamp::CLI.slop.options.each do |c|
8
+ next if c.long == 'version'
9
+ options << c
10
+ end
11
+
12
+ on :d=, :delay=, 'Delay', as: Integer, default: 2
13
+
14
+ run do |opts, _args|
15
+ lamp = Usblamp::Controller.new
16
+ lamp.open
17
+ lamp.switch_off
18
+ lamp.fade_in(opts[:d], lamp.parse(opts))
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Usblamp
3
+ # CLI mode
4
+ module CLI
5
+ ##
6
+ # Hash containing the default Slop options.
7
+ #
8
+ # @return [Hash]
9
+ #
10
+ SLOP_OPTIONS = {
11
+ strict: true,
12
+ help: true,
13
+ banner: 'Usage: usblamp [COMMAND] [OPTIONS]'
14
+ }
15
+
16
+ ##
17
+ # @return [Slop]
18
+ #
19
+ def self.slop
20
+ @slop ||= default_slop
21
+ end
22
+
23
+ ##
24
+ # @return [Slop]
25
+ #
26
+ def self.default_slop
27
+ Slop.new(SLOP_OPTIONS.dup) do
28
+ separator "\nOptions:\n"
29
+
30
+ on :v, :version, 'Shows the current version' do
31
+ puts CLI.version_information
32
+ end
33
+
34
+ on :r=, :red=, 'Red', as: Integer
35
+ on :g=, :green=, 'Green', as: Integer
36
+ on :b=, :blue=, 'Blue', as: Integer
37
+ on :c=, :color=, 'Color'
38
+
39
+ run do |opts, _args|
40
+ lamp = Controller.new
41
+ lamp.open
42
+ lamp.switch_off
43
+ lamp.send_color(lamp.parse(opts))
44
+ end
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Returns a String containing some platform/version related information.
50
+ #
51
+ # @return [String]
52
+ #
53
+ def self.version_information
54
+ "usblamp v#{VERSION} on #{RUBY_DESCRIPTION}"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Usblamp module
3
+ module Usblamp
4
+ # Color class
5
+ class Color
6
+ attr_accessor :red, :green, :blue
7
+ MAX_VALUE = 0x40
8
+
9
+ def initialize(red = nil, green = nil, blue = nil)
10
+ set(red, green, blue)
11
+ end
12
+
13
+ def set(red, green = nil, blue = nil)
14
+ if red.is_a?(String) && green.nil? && blue.nil?
15
+ if ['#', '_'].include?(red[0])
16
+ return define_from_hex(red)
17
+ else
18
+ return define_from_string(red)
19
+ end
20
+ end
21
+
22
+ red = 0 if red.nil?
23
+ green = 0 if green.nil?
24
+ blue = 0 if blue.nil?
25
+
26
+ @red = define_value(red)
27
+ @green = define_value(green)
28
+ @blue = define_value(blue)
29
+ end
30
+
31
+ private
32
+
33
+ def define_value(integer)
34
+ [[0, integer.to_i].max, MAX_VALUE].min
35
+ end
36
+
37
+ def define_from_hex(string)
38
+ string = string.gsub(/[#_]/, '')
39
+ fail ArgumentError, 'Wrong length for hexadecimal string' unless
40
+ [3, 6].include?(string.length.to_i)
41
+
42
+ return set(string[0..1].to_i(16),
43
+ string[2..3].to_i(16),
44
+ string[4..5].to_i(16)
45
+ ) if string.length == 6
46
+
47
+ set((string[0] * 2).to_i(16),
48
+ (string[1] * 2).to_i(16),
49
+ (string[2] * 2).to_i(16)
50
+ )
51
+ end
52
+
53
+ def define_from_string(string)
54
+ return set(MAX_VALUE, 0, 0) if string == 'red'
55
+ return set(0, MAX_VALUE, 0) if string == 'green'
56
+ return set(0, 0, MAX_VALUE) if string == 'blue'
57
+ return set(MAX_VALUE, MAX_VALUE, MAX_VALUE) if string == 'white'
58
+ return set(MAX_VALUE, 0, MAX_VALUE) if string == 'magenta'
59
+ return set(0, MAX_VALUE, MAX_VALUE) if string == 'cyan'
60
+ return set(MAX_VALUE, MAX_VALUE, 0) if string == 'yellow'
61
+ return set(MAX_VALUE / 2, MAX_VALUE / 2, MAX_VALUE / 2) if
62
+ string == 'purple'
63
+
64
+ set(0, 0, 0)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,110 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Usblamp module
3
+ module Usblamp
4
+ # Controller class
5
+ class Controller
6
+ attr_accessor :device, :color
7
+
8
+ VENDOR_ID = 0x1d34
9
+ PRODUCT_ID = 0x0004
10
+ INIT_PACKET1 = [0x1F, 0x01, 0x29, 0x00, 0xB8, 0x54, 0x2C, 0x03]
11
+ INIT_PACKET2 = [0x00, 0x01, 0x29, 0x00, 0xB8, 0x54, 0x2C, 0x04]
12
+
13
+ def initialize
14
+ @device = nil
15
+ @color = nil
16
+ end
17
+
18
+ def open
19
+ usb = LIBUSB::Context.new
20
+ device = usb.devices(idVendor: VENDOR_ID, idProduct: PRODUCT_ID).first
21
+
22
+ fail 'Could no find a supported device.' if device.nil?
23
+ @device = device.open
24
+ @device.detach_kernel_driver(0) if @device.kernel_driver_active?(0)
25
+ @device.claim_interface(0)
26
+ send(INIT_PACKET1)
27
+ send(INIT_PACKET2)
28
+ end
29
+
30
+ def send(data)
31
+ return true if @device
32
+ .control_transfer(
33
+ bmRequestType: LIBUSB::REQUEST_TYPE_CLASS |
34
+ LIBUSB::RECIPIENT_INTERFACE,
35
+ bRequest: LIBUSB::REQUEST_SET_CONFIGURATION,
36
+ wValue: 0x81,
37
+ wIndex: LIBUSB::ENDPOINT_OUT,
38
+ dataOut: data.map(&:chr).reduce(:+),
39
+ timeout: 10) == 8
40
+
41
+ false
42
+ end
43
+
44
+ def send_color(color)
45
+ fail TypeError, 'Parameter must be an instance of Color' unless
46
+ color.is_a?(Color)
47
+
48
+ @color = color
49
+ data = [color.red,
50
+ color.green,
51
+ color.blue,
52
+ 0x00,
53
+ 0x00,
54
+ 0x00,
55
+ 0x00,
56
+ 0x05]
57
+ send(data)
58
+ end
59
+
60
+ def switch_off
61
+ send_color(Color.new('black'))
62
+ end
63
+
64
+ def parse(args)
65
+ color = Color.new(args[:c]) unless args[:c].nil?
66
+ color = Color.new(args[:r], args[:g], args[:b]) if args[:c].nil?
67
+ color
68
+ end
69
+
70
+ def fade_in(delay, new_color)
71
+ delay = delay.to_i
72
+ color = Color.new
73
+ max_value = [new_color.red, new_color.green, new_color.blue].max
74
+ (0..max_value).each do |i|
75
+ sleep((delay.to_f * 1000.to_f / max_value.to_f + 1.to_f) / 1000.to_f)
76
+ color.red = transition(i,
77
+ @color.red,
78
+ new_color.red,
79
+ max_value)
80
+ color.green = transition(i,
81
+ @color.green,
82
+ new_color.green,
83
+ max_value)
84
+ color.blue = transition(i,
85
+ @color.blue,
86
+ new_color.blue,
87
+ max_value)
88
+ send_color(color)
89
+ end
90
+ end
91
+
92
+ def blink(times, new_color)
93
+ (1..times).each do
94
+ send_color(new_color)
95
+ sleep 0.5
96
+ switch_off
97
+ sleep 0.5
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def transition(index, start_point, end_point, maximum)
104
+ (((start_point.to_f + (end_point.to_f - start_point.to_f)) *
105
+ (index.to_f + 1.to_f)
106
+ ) / maximum.to_f
107
+ ).to_i
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Gemirro Version
3
+ module Usblamp
4
+ VERSION = '0.1.1'
5
+ end
data/lib/usblamp.rb ADDED
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'slop'
3
+ require 'libusb'
4
+
5
+ unless $LOAD_PATH.include?(File.expand_path('../', __FILE__))
6
+ $LOAD_PATH.unshift(File.expand_path('../', __FILE__))
7
+ end
8
+
9
+ require 'usblamp/color'
10
+ require 'usblamp/controller'
11
+ require 'usblamp/version'
12
+
13
+ require 'usblamp/cli'
14
+ require 'usblamp/cli/fadein'
15
+ require 'usblamp/cli/blink'
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'simplecov'
5
+
6
+ SimpleCov.start do
7
+ add_filter '/spec/'
8
+ end
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'usblamp/cli'
4
+ require 'slop'
5
+
6
+ # Usblamp tests
7
+ module Usblamp
8
+ # CLI tests
9
+ module CLI
10
+ describe 'CLI' do
11
+ it 'should return options' do
12
+ options = CLI.slop
13
+ expect(options).to be_a(::Slop)
14
+ expect(options.config[:strict]).to be_truthy
15
+ expect(options.config[:banner])
16
+ .to eq('Usage: usblamp [COMMAND] [OPTIONS]')
17
+ expect(options.to_s)
18
+ .to match(/-v, --version(\s+)Shows the current version/)
19
+ expect(options.to_s)
20
+ .to match(/-h, --help(\s+)Display this help message./)
21
+
22
+ version = options.fetch_option(:v)
23
+ expect(version.short).to eq('v')
24
+ expect(version.long).to eq('version')
25
+ expect { version.call }.to output(/usblamp v.* on ruby/).to_stdout
26
+ end
27
+
28
+ it 'should retrieve version information' do
29
+ expect(CLI.version_information).to eq(
30
+ "usblamp v#{VERSION} on #{RUBY_DESCRIPTION}"
31
+ )
32
+ end
33
+
34
+ it 'should change color' do
35
+ controller = double(open: true,
36
+ switch_off: true,
37
+ parse: { c: 'blue' },
38
+ send_color: true)
39
+ allow(Controller).to receive(:new).and_return(controller)
40
+ expect(CLI.slop.parse(c: 'blue')).to eq(c: 'blue')
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,90 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'usblamp/color'
4
+
5
+ # Usblamp module
6
+ module Usblamp
7
+ describe 'Color' do
8
+ before(:each) do
9
+ @color = Color.new
10
+ end
11
+
12
+ it 'should test initialize with color' do
13
+ color = Color.new(10, 11, 12)
14
+ expect(color.red).to eq(10)
15
+ expect(color.green).to eq(11)
16
+ expect(color.blue).to eq(12)
17
+ end
18
+
19
+ it 'should test const' do
20
+ expect(Color::MAX_VALUE).to eq(0x40)
21
+ end
22
+
23
+ it 'should set' do
24
+ @color.set(10, 11, 12)
25
+ expect(@color.red).to eq(10)
26
+ expect(@color.green).to eq(11)
27
+ expect(@color.blue).to eq(12)
28
+ end
29
+
30
+ it 'should set from hex' do
31
+ @color.set('#112233')
32
+ expect(@color.red).to eq(17)
33
+ expect(@color.green).to eq(34)
34
+ expect(@color.blue).to eq(51)
35
+ @color.set('_112233')
36
+ expect(@color.red).to eq(17)
37
+ expect(@color.green).to eq(34)
38
+ expect(@color.blue).to eq(51)
39
+ @color.set('#123')
40
+ expect(@color.red).to eq(17)
41
+ expect(@color.green).to eq(34)
42
+ expect(@color.blue).to eq(51)
43
+ end
44
+
45
+ it 'should set from string' do
46
+ @color.set('red')
47
+ expect(@color.red).to eq(64)
48
+ expect(@color.green).to eq(0)
49
+ expect(@color.blue).to eq(0)
50
+ @color.set('green')
51
+ expect(@color.red).to eq(0)
52
+ expect(@color.green).to eq(64)
53
+ expect(@color.blue).to eq(0)
54
+ @color.set('blue')
55
+ expect(@color.red).to eq(0)
56
+ expect(@color.green).to eq(0)
57
+ expect(@color.blue).to eq(64)
58
+ @color.set('white')
59
+ expect(@color.red).to eq(64)
60
+ expect(@color.green).to eq(64)
61
+ expect(@color.blue).to eq(64)
62
+ @color.set('magenta')
63
+ expect(@color.red).to eq(64)
64
+ expect(@color.green).to eq(0)
65
+ expect(@color.blue).to eq(64)
66
+ @color.set('cyan')
67
+ expect(@color.red).to eq(0)
68
+ expect(@color.green).to eq(64)
69
+ expect(@color.blue).to eq(64)
70
+ @color.set('yellow')
71
+ expect(@color.red).to eq(64)
72
+ expect(@color.green).to eq(64)
73
+ expect(@color.blue).to eq(0)
74
+ @color.set('purple')
75
+ expect(@color.red).to eq(32)
76
+ expect(@color.green).to eq(32)
77
+ expect(@color.blue).to eq(32)
78
+ @color.set('nothing')
79
+ expect(@color.red).to eq(0)
80
+ expect(@color.green).to eq(0)
81
+ expect(@color.blue).to eq(0)
82
+ end
83
+
84
+ it 'should raise error when undefined color' do
85
+ expect { @color.set('#FF') }.to raise_error ArgumentError
86
+ expect { @color.set('#FFFF') }.to raise_error ArgumentError
87
+ expect { @color.set('#FFFFF') }.to raise_error ArgumentError
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,152 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'libusb'
4
+ require 'usblamp/controller'
5
+
6
+ # Usblamp module
7
+ module Usblamp
8
+ describe 'Controller' do
9
+ before(:each) do
10
+ @controller = Controller.new
11
+ end
12
+
13
+ it 'should test init' do
14
+ expect(@controller.device).to be(nil)
15
+ expect(@controller.color).to be(nil)
16
+ end
17
+
18
+ it 'should test const' do
19
+ expect(Controller::VENDOR_ID).to eq(0x1d34)
20
+ expect(Controller::PRODUCT_ID).to eq(0x0004)
21
+ expect(Controller::INIT_PACKET1)
22
+ .to eq([31, 1, 41, 0, 184, 84, 44, 3])
23
+ expect(Controller::INIT_PACKET2)
24
+ .to eq([0, 1, 41, 0, 184, 84, 44, 4])
25
+ expect(@controller.device).to eq(nil)
26
+ end
27
+
28
+ it 'should raise when no supported devices found' do
29
+ device = double(devices: [])
30
+ allow(LIBUSB::Context).to receive(:new).and_return(device)
31
+ expect { @controller.open }.to raise_error(RuntimeError)
32
+ end
33
+
34
+ it 'should open device' do
35
+ handle = spy(open: true,
36
+ kernel_driver_active?: true,
37
+ detach_kernel_driver: true,
38
+ control_transfer: 8)
39
+ device = double(devices: [double(open: handle)])
40
+ allow(LIBUSB::Context).to receive(:new).and_return(device)
41
+ expect(@controller.open).to be_truthy
42
+ expect(handle).to have_received(:claim_interface).with(0)
43
+ expect(handle).to have_received(:control_transfer)
44
+ .with(bmRequestType: 33,
45
+ bRequest: 9,
46
+ wValue: 129,
47
+ wIndex: 0,
48
+ dataOut: [31, 1, 41, 0, 184, 84, 44, 3].map(&:chr).reduce(:+),
49
+ timeout: 10)
50
+ expect(handle).to have_received(:control_transfer)
51
+ .with(bmRequestType: 33,
52
+ bRequest: 9,
53
+ wValue: 129,
54
+ wIndex: 0,
55
+ dataOut: [0, 1, 41, 0, 184, 84, 44, 4].map(&:chr).reduce(:+),
56
+ timeout: 10)
57
+ end
58
+
59
+ it 'should fail send transfer' do
60
+ device = double(control_transfer: 5)
61
+ @controller.device = device
62
+ expect(@controller.send([0])).to be_falsy
63
+ end
64
+
65
+ it 'should test send_color parameter' do
66
+ expect { @controller.send_color('fake') }
67
+ .to raise_error(TypeError, 'Parameter must be an instance of Color')
68
+ end
69
+
70
+ it 'should send color' do
71
+ device = spy(control_transfer: 8)
72
+ @controller.device = device
73
+ expect(@controller.send_color(Color.new('blue'))).to be_truthy
74
+ expect(device).to have_received(:control_transfer)
75
+ .with(bmRequestType: 33,
76
+ bRequest: 9,
77
+ wValue: 129,
78
+ wIndex: 0,
79
+ dataOut: [0, 0, 64, 0, 0, 0, 0, 5].map(&:chr).reduce(:+),
80
+ timeout: 10)
81
+ end
82
+
83
+ it 'should switch off' do
84
+ device = spy(control_transfer: 8)
85
+ @controller.device = device
86
+ expect(@controller.switch_off).to be_truthy
87
+ expect(device).to have_received(:control_transfer)
88
+ .with(bmRequestType: 33,
89
+ bRequest: 9,
90
+ wValue: 129,
91
+ wIndex: 0,
92
+ dataOut: "\x00\x00\x00\x00\x00\x00\x00\x05",
93
+ timeout: 10)
94
+ end
95
+
96
+ it 'should parse args to create color' do
97
+ args = { c: 'red' }
98
+ result = @controller.parse(args)
99
+ expect(result).to be_a(Color)
100
+ expect(result.red).to eq(64)
101
+ expect(result.green).to eq(0)
102
+ expect(result.blue).to eq(0)
103
+
104
+ args = { r: 10, g: 10, b: 10 }
105
+ result = @controller.parse(args)
106
+ expect(result).to be_a(Color)
107
+ expect(result.red).to eq(10)
108
+ expect(result.green).to eq(10)
109
+ expect(result.blue).to eq(10)
110
+ end
111
+
112
+ it 'should fade in' do
113
+ device = spy(control_transfer: 8)
114
+ @controller.color = Color.new('black')
115
+ @controller.device = device
116
+ allow(@controller).to receive(:sleep).and_return(true)
117
+ expect(@controller.fade_in(2, Color.new('blue'))).to be_truthy
118
+ (1..64).each do |i|
119
+ expect(device).to have_received(:control_transfer)
120
+ .with(bmRequestType: 33,
121
+ bRequest: 9,
122
+ wValue: 129,
123
+ wIndex: 0,
124
+ dataOut: [0, 0, i, 0, 0, 0, 0, 5].map(&:chr).reduce(:+),
125
+ timeout: 10)
126
+ end
127
+ end
128
+
129
+ it 'should blink' do
130
+ device = spy(control_transfer: 8)
131
+ @controller.device = device
132
+ allow(@controller).to receive(:sleep).and_return(true)
133
+ expect(@controller.blink(2, Color.new('blue'))).to be_truthy
134
+ (1..2).each do
135
+ expect(device).to have_received(:control_transfer).twice
136
+ .with(bmRequestType: 33,
137
+ bRequest: 9,
138
+ wValue: 129,
139
+ wIndex: 0,
140
+ dataOut: [0, 0, 64, 0, 0, 0, 0, 5].map(&:chr).reduce(:+),
141
+ timeout: 10)
142
+ expect(device).to have_received(:control_transfer).twice
143
+ .with(bmRequestType: 33,
144
+ bRequest: 9,
145
+ wValue: 129,
146
+ wIndex: 0,
147
+ dataOut: [0, 0, 0, 0, 0, 0, 0, 5].map(&:chr).reduce(:+),
148
+ timeout: 10)
149
+ end
150
+ end
151
+ end
152
+ end