hacklet 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,3 +2,5 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
+ before_install:
6
+ - sudo apt-get install libftdi1
@@ -1,4 +1,18 @@
1
1
 
2
+ 0.6.0 / 2013-06-18
3
+ ==================
4
+
5
+ * Add Cross-platform support by switching to libftdi-ruby from serialport.
6
+ * API Change: Hacklet::Dongle.open is now used to create a session
7
+ instead of #open_session.
8
+ * Refactor dongle communication functions into SerialConnection for
9
+ clarity.
10
+
11
+ 0.5.1 / 2013-06-02
12
+ ==================
13
+
14
+ * Bugfix: Properly sets the time on the device after commissioning.
15
+
2
16
  0.5.0 / 2013-05-30
3
17
  ==================
4
18
 
data/README.md CHANGED
@@ -2,15 +2,18 @@
2
2
 
3
3
  A library, written in Ruby, for controlling the [Modlet][modlet] (smart) outlet.
4
4
 
5
- If you haven't heard of the Modlet before, it's a smart outlet cover
6
- which allows you to convert any outlet in your house into a smart
7
- outlet. This means that you can control whether a plug is on or off and
8
- you can also determine how much energy it's using with a sampling
9
- frequency of 10 seconds.
5
+ If you haven't heard of the Modlet before, it's an outlet cover which
6
+ allows you to convert any standard US outlet into a smart outlet. This
7
+ means that you can control whether each socket is on or off as well as
8
+ determine how much energy it's using with a sampling frequency of 10
9
+ seconds.
10
10
 
11
11
  There are alot of other similar products but this is the first one that
12
12
  I've seen that [costs $50][amazon] and includes control as well as
13
- monitoring of the both sockets independently.
13
+ monitoring of both sockets independently.
14
+
15
+ Checkout the companion project [hacklet-remote] if you're interested in
16
+ controlling your modlet with [IFTTT].
14
17
 
15
18
  ## Why
16
19
 
@@ -21,17 +24,45 @@ available on all platforms (Linux) and it's pretty heavyweight for what
21
24
  it does.
22
25
 
23
26
  The goal of this project is provide all the same functionality of the
24
- bundled client but do it in a lightweight manner, to provide total
25
- control and availability of the data.
27
+ bundled client but in a lightweight manner without dependencies on
28
+ external services.
26
29
 
27
- ## Getting Started
30
+ ## Installation
31
+
32
+ ### Mac
28
33
 
29
34
  ```shell
35
+ brew install libftdi
30
36
  gem install hacklet
37
+ ```
38
+
39
+ ### Linux (Ubuntu/Debian)
40
+
41
+ ```shell
42
+ apt-get install libftdi1
43
+ gem install hacklet
44
+ echo 'ATTRS{idVendor}=="0403", ATTRS{idProduct}=="8c81", SUBSYSTEMS=="usb", ACTION=="add", MODE="0660", GROUP="plugdev"' | sudo tee /etc/udev/rules.d/99-thinkeco.rules
45
+ ```
31
46
 
32
- # Load the USB-to-serial kernel driver, this only works with Linux currently
33
- sudo modprobe ftdi_sio vendor=0x0403 product=0x8c81
47
+ ### Windows
34
48
 
49
+ **Note**: These directions haven't been confirmed. If you can confirm
50
+ them or improve them, please create an [issue][issues] or send a pull request.
51
+
52
+ 1. Install [7zip] in order to extract Zadiag.
53
+ 1. Download Zadig for [XP][zadiag-xp] or [later][zadiag-later].
54
+ 1. Extract Zadig and run it.
55
+ 1. Click `Options` -> `List all Devices` to populate the device list.
56
+ 1. Open the device dropdown and look for the "Thinkeco" device and select it.
57
+ 1. Select the libusbK driver and press the `Replace Driver` button.
58
+ 1. Download and unzip [libftdi][libftdi-win]
59
+ 1. Run `gem install hacklet`
60
+
61
+ If something isn't clear here, take a look at [libFTDI under Windows][libftdi-win-post] as this was summarized from there.
62
+
63
+ ## Getting Started
64
+
65
+ ```shell
35
66
  # Add the device to the network, keep a copy of the network ids (ie 0xDEED)
36
67
  hacklet commission
37
68
 
@@ -48,24 +79,37 @@ hacklet on -n 0xDEED -s 0
48
79
  ## Contributing
49
80
 
50
81
  All contributions are welcome (bug reports, bug fixes, documentation or
51
- new features)! If you're looking for something to do check the [issue]
52
- list and see if there's something already there. If you've got a new
53
- idea, feel free to create an issue for discussion.
82
+ new features)! All discussion happens using [issues] so if you are
83
+ interested in contributing:
84
+
85
+ * Search to make sure an issue doesn't already exist.
86
+ * If it doesn't, create a new issue and describe your proposal.
87
+
88
+ If you're interested in following the status of the project, simply
89
+ "watch" the repository on Github and you'll receive notices about all of
90
+ the new issues.
54
91
 
55
- To get a general understanding of how things work, checkout the
56
- [developer wiki]
92
+ If your curious about how the hardware works or the specifics of the
93
+ protocol checkout out the [developer wiki].
57
94
 
58
- ### Getting Started
95
+ ### Contribution Workflow
59
96
 
60
- * Checkout the repository
97
+ * Fork the repository
61
98
  * Install dependencies `bundle install`
62
- * Create a feature branch `git checkout -b short-name`
99
+ * Create a feature branch `git checkout -b short-descriptive-name`
63
100
  * Run tests `bundle exec rake`
64
101
  * Write your feature (and tests)
65
102
  * Run tests `bundle exec rake`
66
103
  * Create a pull request
67
104
 
68
105
  [modlet]: http://themodlet.com
69
- [amazon]: http://www.amazon.com/ThinkEco-TE1010-Modlet-Starter-White/dp/B00AAT43OA/
70
- [issue]: https://github.com/mcolyer/hacklet/issues
106
+ [amazon]: http://www.amazon.com/gp/product/B00AAT43OA/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=B00AAT43OA&linkCode=as2&tag=matcol-20
107
+ [issues]: https://github.com/mcolyer/hacklet/issues
71
108
  [developer wiki]: https://github.com/mcolyer/hacklet/wiki
109
+ [hacklet-remote]: https://github.com/mcolyer/hacklet-remote/
110
+ [IFTTT]: http://ifttt.com
111
+ [7zip]: http://www.7-zip.org/
112
+ [libftdi-win]: http://code.google.com/p/picusb/downloads/detail?name=libftdi1-1.0_devkit_mingw32_17Feb2013.zip
113
+ [zadiag-xp]: http://sourceforge.net/projects/libwdi/files/zadig/zadig_xp_v2.0.1.160.7z/download
114
+ [zadiag-later]: http://sourceforge.net/projects/libwdi/files/zadig/zadig_v2.0.1.160.7z/download
115
+ [libftdi-win-post]: http://embedded-funk.blogspot.com/2013/03/libftdi-under-windows.html
@@ -8,6 +8,7 @@ require 'logger'
8
8
 
9
9
  logger = Logger.new(STDOUT)
10
10
  logger.level = Logger::INFO
11
- dongle = Hacklet::Dongle.new(logger)
12
11
 
13
- Hacklet::Command.run(dongle, ARGV)
12
+ Hacklet::Dongle.open(logger) do |dongle|
13
+ Hacklet::Command.run(dongle, ARGV)
14
+ end
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency "serialport", "~>1.1.0"
21
+ gem.add_dependency "libftdi-ruby", "~>0.0.20"
22
22
  gem.add_dependency "bindata", "~>1.5.0"
23
23
  gem.add_dependency "slop", "~>3.4.0"
24
24
  gem.add_development_dependency "rake"
@@ -1,4 +1,5 @@
1
1
  require "hacklet/version"
2
2
  require "hacklet/messages"
3
+ require "hacklet/serial_connection"
3
4
  require "hacklet/dongle"
4
5
  require "hacklet/command"
@@ -16,11 +16,9 @@ module Hacklet
16
16
  network_id = opts[:network][2..-1].to_i(16)
17
17
  socket_id = opts[:socket].to_i
18
18
 
19
- dongle.open_session do |session|
20
- session.lock_network
21
- session.select_network(network_id)
22
- session.switch(network_id, socket_id, true)
23
- end
19
+ dongle.lock_network
20
+ dongle.select_network(network_id)
21
+ dongle.switch(network_id, socket_id, true)
24
22
  end
25
23
  end
26
24
 
@@ -35,11 +33,9 @@ module Hacklet
35
33
  network_id = opts[:network][2..-1].to_i(16)
36
34
  socket_id = opts[:socket].to_i
37
35
 
38
- dongle.open_session do |session|
39
- session.lock_network
40
- session.select_network(network_id)
41
- session.switch(network_id, socket_id, false)
42
- end
36
+ dongle.lock_network
37
+ dongle.select_network(network_id)
38
+ dongle.switch(network_id, socket_id, false)
43
39
  end
44
40
  end
45
41
 
@@ -54,11 +50,9 @@ module Hacklet
54
50
  network_id = opts[:network][2..-1].to_i(16)
55
51
  socket_id = opts[:socket].to_i
56
52
 
57
- dongle.open_session do |session|
58
- session.lock_network
59
- session.select_network(network_id)
60
- session.request_samples(network_id, socket_id)
61
- end
53
+ dongle.lock_network
54
+ dongle.select_network(network_id)
55
+ dongle.request_samples(network_id, socket_id)
62
56
  end
63
57
  end
64
58
 
@@ -68,9 +62,7 @@ module Hacklet
68
62
  end
69
63
 
70
64
  run do |opts, args|
71
- dongle.open_session do |session|
72
- session.commission
73
- end
65
+ dongle.commission
74
66
  end
75
67
  end
76
68
 
@@ -1,4 +1,3 @@
1
- require 'serialport'
2
1
  require 'logger'
3
2
  require 'timeout'
4
3
 
@@ -6,46 +5,44 @@ module Hacklet
6
5
  class Dongle
7
6
  attr_reader :logger
8
7
 
9
- # logger - Optionally takes a Logger instance, the default is to log to
10
- # STDOUT
11
- def initialize(logger=Logger.new(STDOUT))
12
- @logger = logger
13
- end
14
-
15
- # Public: Initializes a session so the client can request data.
8
+ # Public: Initializes a dongle instance and yields it.
16
9
  #
17
- # port - Optional string for configuring the serial port device.
10
+ # logger - The Logger instance to log to, defaults to STDOUT.
18
11
  #
19
12
  # Returns nothing.
20
- def open_session(port='/dev/ttyUSB0')
21
- @serial = open_serial_port(port)
13
+ def self.open(logger=Logger.new(STDOUT))
14
+ serial = SerialConnection.new(logger)
15
+ dongle = Dongle.new(serial, logger)
22
16
  begin
23
- @logger.info("Booting")
24
- boot
25
- boot_confirm
26
- @logger.info("Booting complete")
27
- yield self
17
+ dongle.send(:boot)
18
+ dongle.send(:boot_confirm)
19
+ yield dongle
28
20
  ensure
29
- @serial.close
21
+ serial.close
30
22
  end
31
23
  end
32
24
 
25
+ # serial - Serial connection to use with the dongle.
26
+ # logger - The Logger instance to log to.
27
+ def initialize(serial, logger)
28
+ @serial = serial
29
+ @logger = logger
30
+ end
31
+
33
32
  # Public: Listens for new devices on the network.
34
33
  #
35
34
  # This must be executed within an open session.
36
35
  #
37
36
  # Returns nothing.
38
37
  def commission
39
- require_session
40
-
41
38
  response = nil
42
39
  begin
43
40
  unlock_network
44
41
  Timeout.timeout(30) do
45
42
  @logger.info("Listening for devices ...")
46
43
  loop do
47
- buffer = receive(4)
48
- buffer += receive(buffer.bytes.to_a[3]+1)
44
+ buffer = @serial.receive(4)
45
+ buffer += @serial.receive(buffer.bytes.to_a[3]+1)
49
46
  if buffer.bytes.to_a[1] == 0xa0
50
47
  response = BroadcastResponse.read(buffer)
51
48
  @logger.info("Found device 0x%x on network 0x%x" % [response.device_id, response.network_id])
@@ -70,10 +67,8 @@ module Hacklet
70
67
  #
71
68
  # Returns nothing.
72
69
  def select_network(network_id)
73
- require_session
74
-
75
- transmit(HandshakeRequest.new(:network_id => network_id))
76
- HandshakeResponse.read(receive(6))
70
+ @serial.transmit(HandshakeRequest.new(:network_id => network_id))
71
+ HandshakeResponse.read(@serial.receive(6))
77
72
  end
78
73
 
79
74
  # Public: Request stored samples.
@@ -84,14 +79,12 @@ module Hacklet
84
79
  # TODO: This needs to return a more usable set of data.
85
80
  # Returns the SamplesResponse.
86
81
  def request_samples(network_id, channel_id)
87
- require_session
88
-
89
82
  @logger.info("Requesting samples")
90
- transmit(SamplesRequest.new(:network_id => network_id, :channel_id => channel_id))
91
- AckResponse.read(receive(6))
92
- buffer = receive(4)
83
+ @serial.transmit(SamplesRequest.new(:network_id => network_id, :channel_id => channel_id))
84
+ AckResponse.read(@serial.receive(6))
85
+ buffer = @serial.receive(4)
93
86
  remaining_bytes = buffer.bytes.to_a[3] + 1
94
- buffer += receive(remaining_bytes)
87
+ buffer += @serial.receive(remaining_bytes)
95
88
  response = SamplesResponse.read(buffer)
96
89
 
97
90
  response.converted_samples.each do |time, wattage|
@@ -110,8 +103,6 @@ module Hacklet
110
103
  #
111
104
  # Returns the SwitchResponse.
112
105
  def switch(network_id, channel_id, state)
113
- require_session
114
-
115
106
  request = ScheduleRequest.new(:network_id => network_id, :channel_id => channel_id)
116
107
  if state
117
108
  request.always_on!
@@ -120,8 +111,8 @@ module Hacklet
120
111
  request.always_off!
121
112
  @logger.info("Turning off channel #{channel_id} on network 0x#{network_id.to_s(16)}")
122
113
  end
123
- transmit(request)
124
- ScheduleResponse.read(receive(6))
114
+ @serial.transmit(request)
115
+ ScheduleResponse.read(@serial.receive(6))
125
116
  end
126
117
 
127
118
  # Public: Unlocks the network, to add a new device.
@@ -129,8 +120,8 @@ module Hacklet
129
120
  # Returns the BootConfirmResponse
130
121
  def unlock_network
131
122
  @logger.info("Unlocking network")
132
- transmit(UnlockRequest.new)
133
- LockResponse.read(receive(6))
123
+ @serial.transmit(UnlockRequest.new)
124
+ LockResponse.read(@serial.receive(6))
134
125
  @logger.info("Unlocking complete")
135
126
  end
136
127
 
@@ -139,8 +130,8 @@ module Hacklet
139
130
  # Returns the BootConfirmResponse
140
131
  def lock_network
141
132
  @logger.info("Locking network")
142
- transmit(LockRequest.new)
143
- LockResponse.read(receive(6))
133
+ @serial.transmit(LockRequest.new)
134
+ LockResponse.read(@serial.receive(6))
144
135
  @logger.info("Locking complete")
145
136
  end
146
137
 
@@ -149,8 +140,9 @@ module Hacklet
149
140
  #
150
141
  # Returns the BootResponse
151
142
  def boot
152
- transmit(BootRequest.new)
153
- BootResponse.read(receive(27))
143
+ @logger.info("Booting")
144
+ @serial.transmit(BootRequest.new)
145
+ BootResponse.read(@serial.receive(27))
154
146
  end
155
147
 
156
148
  # Private: Confirms that booting was successful?
@@ -159,8 +151,9 @@ module Hacklet
159
151
  #
160
152
  # Returns the BootConfirmResponse
161
153
  def boot_confirm
162
- transmit(BootConfirmRequest.new)
163
- BootConfirmResponse.read(receive(6))
154
+ @serial.transmit(BootConfirmRequest.new)
155
+ BootConfirmResponse.read(@serial.receive(6))
156
+ @logger.info("Booting complete")
164
157
  end
165
158
 
166
159
  # Private: Updates the time of a device.
@@ -172,66 +165,9 @@ module Hacklet
172
165
  #
173
166
  # Returns nothing.
174
167
  def update_time(network_id)
175
- require_session
176
-
177
- transmit(UpdateTimeRequest.new(:network_id => network_id))
178
- UpdateTimeAckResponse.read(receive(6))
179
- UpdateTimeResponse.read(receive(8))
180
- end
181
-
182
- # Private: Initializes the serial port
183
- #
184
- # port - the String to the device to open as a serial port.
185
- #
186
- # Returns a SerialPort object.
187
- def open_serial_port(port)
188
- serial_port = SerialPort.new(port, 115200, 8, 1, SerialPort::NONE)
189
- serial_port.flow_control = SerialPort::NONE
190
- serial_port
191
- end
192
-
193
- # Private: Transmits the packet to the dongle.
194
- #
195
- # command - The binary string to send.
196
- #
197
- # Returns the number of bytes written.
198
- def transmit(command)
199
- @logger.debug("TX: #{unpack(command.to_binary_s).inspect}")
200
- @serial.write(command.to_binary_s) if @serial
201
- end
202
-
203
- # Private: Waits and receives the specified number of packets from the
204
- # dongle.
205
- #
206
- # bytes - The number of bytes to read.
207
- #
208
- # Returns a binary string containing the response.
209
- def receive(bytes)
210
- if @serial
211
- response = @serial.read(bytes)
212
- else
213
- response = "\x0\x0\x0\x0"
214
- end
215
- @logger.debug("RX: #{unpack(response).inspect}")
216
-
217
- response
218
- end
219
-
220
- # Private: Prints a binary string a concise hexidecimal form for debugging
221
- #
222
- # message - The message to parse.
223
- #
224
- # Returns a string of hexidecimal representing equivalent to the message.
225
- def unpack(message)
226
- message.unpack('H2'*message.size)
227
- end
228
-
229
- # Private: A helper to ensure that the serial port is active.
230
- #
231
- # Returns nothing.
232
- # Raises RuntimeError if the serial port is not active.
233
- def require_session
234
- raise RuntimeError.new("Must be executed within an open session") unless @serial && !@serial.closed?
168
+ @serial.transmit(UpdateTimeRequest.new(:network_id => network_id))
169
+ UpdateTimeAckResponse.read(@serial.receive(6))
170
+ UpdateTimeResponse.read(@serial.receive(8))
235
171
  end
236
172
  end
237
173
  end
@@ -0,0 +1,72 @@
1
+ require 'ftdi'
2
+
3
+ module Hacklet
4
+ class SerialConnection
5
+ def initialize(logger, port='/dev/ttyUSB0')
6
+ @logger = logger
7
+ @connection = Ftdi::Context.new
8
+ @connection.usb_open(0x0403, 0x8c81)
9
+ @connection.set_bitmode(0x00, Ftdi::BitbangMode[:reset])
10
+ @connection.baudrate = 115200
11
+ @connection.flowctrl = Ftdi::SIO_DISABLE_FLOW_CTRL
12
+ @connection.dtr = 1
13
+ @connection.rts = 1
14
+ @receive_buffer = ""
15
+ end
16
+
17
+ # Public: Closes the connection
18
+ #
19
+ # Returns nothing.
20
+ def close
21
+ @connection.usb_close
22
+ end
23
+
24
+ # Public: Transmits the packet to the dongle.
25
+ #
26
+ # command - The binary string to send.
27
+ #
28
+ # Returns the number of bytes written.
29
+ def transmit(command)
30
+ @logger.debug("TX: #{unpack(command.to_binary_s).inspect}")
31
+ @connection.write_data(command.to_binary_s)
32
+ end
33
+
34
+ # Public: Waits and receives the specified number of packets from the
35
+ # dongle.
36
+ #
37
+ # bytes - The number of bytes to read.
38
+ #
39
+ # Returns a binary string containing the response.
40
+ def receive(bytes)
41
+ response = ""
42
+ loop do
43
+ if bytes <= @receive_buffer.bytesize
44
+ array = @receive_buffer.bytes.to_a
45
+ response = array[0..(bytes - 1)].pack('c*')
46
+ @receive_buffer = array[(bytes)..-1].pack('c*')
47
+ break
48
+ end
49
+
50
+ chunk = @connection.read_data
51
+ if chunk.bytesize > 0
52
+ @receive_buffer += chunk
53
+ else
54
+ sleep(0.1)
55
+ end
56
+ end
57
+ @logger.debug("RX: #{unpack(response).inspect}")
58
+
59
+ response
60
+ end
61
+
62
+ private
63
+ # Private: Prints a binary string a concise hexidecimal form for debugging
64
+ #
65
+ # message - The message to parse.
66
+ #
67
+ # Returns a string of hexidecimal representing equivalent to the message.
68
+ def unpack(message)
69
+ message.unpack('H2'*message.size)
70
+ end
71
+ end
72
+ end
@@ -1,3 +1,3 @@
1
1
  module Hacklet
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -5,15 +5,12 @@ describe Hacklet::Command do
5
5
  Hacklet::Command
6
6
  end
7
7
 
8
- let(:dongle) { Hacklet::Dongle.new(Logger.new("/dev/null")) }
8
+ let(:dongle) do
9
+ serial_connection = double('serial_port')
10
+ Hacklet::Dongle.new(serial_connection, Logger.new("/dev/null"))
11
+ end
9
12
 
10
13
  it 'can turn on a socket' do
11
- serial_port = mock('serial_port')
12
- serial_port.should_receive(:close)
13
-
14
- dongle.should_receive(:open_serial_port).and_return(serial_port)
15
- dongle.should_receive(:boot)
16
- dongle.should_receive(:boot_confirm)
17
14
  dongle.should_receive(:lock_network)
18
15
  dongle.should_receive(:select_network).with(16)
19
16
  dongle.should_receive(:switch).with(16, 1, true)
@@ -22,12 +19,6 @@ describe Hacklet::Command do
22
19
  end
23
20
 
24
21
  it 'can turn off a socket' do
25
- serial_port = mock('serial_port')
26
- serial_port.should_receive(:close)
27
-
28
- dongle.should_receive(:open_serial_port).and_return(serial_port)
29
- dongle.should_receive(:boot)
30
- dongle.should_receive(:boot_confirm)
31
22
  dongle.should_receive(:lock_network)
32
23
  dongle.should_receive(:select_network).with(16)
33
24
  dongle.should_receive(:switch).with(16, 0, false)
@@ -36,13 +27,6 @@ describe Hacklet::Command do
36
27
  end
37
28
 
38
29
  it 'can read a socket' do
39
- serial_port = mock('serial_port')
40
- serial_port.stub(:closed?).and_return(false)
41
- serial_port.should_receive(:close)
42
-
43
- dongle.should_receive(:open_serial_port).and_return(serial_port)
44
- dongle.should_receive(:boot)
45
- dongle.should_receive(:boot_confirm)
46
30
  dongle.should_receive(:lock_network)
47
31
  dongle.should_receive(:select_network).with(16)
48
32
  dongle.should_receive(:request_samples).with(16, 1)
@@ -51,13 +35,6 @@ describe Hacklet::Command do
51
35
  end
52
36
 
53
37
  it 'can commission a device' do
54
- serial_port = mock('serial_port')
55
- serial_port.stub(:closed?).and_return(false)
56
- serial_port.should_receive(:close)
57
-
58
- dongle.should_receive(:open_serial_port).and_return(serial_port)
59
- dongle.should_receive(:boot)
60
- dongle.should_receive(:boot_confirm)
61
38
  dongle.should_receive(:commission)
62
39
 
63
40
  subject.run(dongle, ['commission'])
@@ -1,131 +1,154 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Hacklet::Dongle do
4
- subject do
5
- Hacklet::Dongle.new(Logger.new("/dev/null"))
6
- end
7
-
8
4
  it 'can open a new session' do
9
- serial_port = mock("SerialPort")
5
+ serial_port = double("Context").as_null_object
10
6
 
11
7
  # Boot
12
- serial_port.should_receive(:write).with([0x02, 0x40, 0x04, 0x00, 0x44].pack('c'*5))
13
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x84, 0x16, 0x01,
8
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x04, 0x00, 0x44].pack('c*'))
9
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x84, 0x16, 0x01,
14
10
  0x00, 0x00, 0x87, 0x03, 0x00, 0x30, 0x00, 0x33, 0x83, 0x69, 0x9A, 0x0B,
15
- 0x2F, 0x00, 0x00, 0x00, 0x58, 0x4F, 0x80, 0x0A, 0x1C, 0x81].pack('c'*27))
11
+ 0x2F, 0x00, 0x00, 0x00, 0x58, 0x4F, 0x80, 0x0A, 0x1C, 0x81].pack('c*'))
16
12
 
17
13
  # Boot Confirmation
18
- serial_port.should_receive(:write).with([0x02, 0x40, 0x00, 0x00, 0x40].pack('c'*5))
19
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x80, 0x01, 0x10, 0xd1].pack('c'*6))
14
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x00, 0x00, 0x40].pack('c*'))
15
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x80, 0x01, 0x10, 0xd1].pack('c*'))
20
16
 
21
- serial_port.should_receive(:close)
22
- subject.should_receive(:open_serial_port).and_return(serial_port)
23
- subject.open_session do
17
+ serial_port.should_receive(:usb_close)
18
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
19
+ Hacklet::Dongle.open(Logger.new('/dev/null')) do
24
20
  # noop
25
21
  end
26
22
  end
27
23
 
28
24
  it 'can find a new device' do
29
- serial_port = mock("SerialPort")
25
+ serial_port = double("Context").as_null_object
26
+
27
+ # Boot
28
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x04, 0x00, 0x44].pack('c*'))
29
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x84, 0x16, 0x01,
30
+ 0x00, 0x00, 0x87, 0x03, 0x00, 0x30, 0x00, 0x33, 0x83, 0x69, 0x9A, 0x0B,
31
+ 0x2F, 0x00, 0x00, 0x00, 0x58, 0x4F, 0x80, 0x0A, 0x1C, 0x81].pack('c*'))
32
+
33
+ # Boot Confirmation
34
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x00, 0x00, 0x40].pack('c*'))
35
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x80, 0x01, 0x10, 0xd1].pack('c*'))
30
36
 
31
37
  # Unlock Network
32
- serial_port.should_receive(:write).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x90, 0x01, 0x02].pack('c'*9))
33
- serial_port.should_receive(:read).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c'*6))
38
+ serial_port.should_receive(:write_data).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x90, 0x01, 0x02].pack('c*'))
39
+ serial_port.should_receive(:read_data).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c*'))
34
40
 
35
41
  # Ignored packet #1
36
- serial_port.should_receive(:read).and_return([0x02, 0x99, 0xd1, 0x23].pack('c'*4))
37
- serial_port.should_receive(:read).and_return([0x01, 0xcc, 0x1f, 0x10, 0x00,
42
+ serial_port.should_receive(:read_data).and_return([0x02, 0x99, 0xd1, 0x23].pack('c*'))
43
+ serial_port.should_receive(:read_data).and_return([0x01, 0xcc, 0x1f, 0x10, 0x00,
38
44
  0x00, 0x58, 0x4f, 0x80, 0x00, 0x77, 0x2a, 0x8a, 0x33, 0xa7, 0xf4, 0xf6,
39
45
  0x80, 0xd0, 0x9c, 0x5d, 0x3c, 0x84, 0xf5, 0x2b, 0x43, 0x00, 0x00, 0x00,
40
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcb].pack('c'*36))
46
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcb].pack('c*'))
41
47
 
42
48
  # Ignored packet #2
43
- serial_port.should_receive(:read).and_return([0x02, 0x98, 0xc0, 0x09].pack('c'*4))
44
- serial_port.should_receive(:read).and_return([0xcc, 0x1f, 0x10, 0x00, 0x00,
45
- 0x58, 0x4f, 0x80, 0x00, 0x05].pack('c'*10))
49
+ serial_port.should_receive(:read_data).and_return([0x02, 0x98, 0xc0, 0x09].pack('c*'))
50
+ serial_port.should_receive(:read_data).and_return([0xcc, 0x1f, 0x10, 0x00, 0x00,
51
+ 0x58, 0x4f, 0x80, 0x00, 0x05].pack('c*'))
46
52
 
47
53
  # Useful response
48
- serial_port.should_receive(:read).and_return([0x02, 0xa0, 0x13, 0x0b].pack('c'*4))
49
- serial_port.should_receive(:read).and_return([0x66, 0xad, 0xcc, 0x1f, 0x10, 0x00,
50
- 0x00, 0x58, 0x4f, 0x80, 0x8e, 0xa9].pack('c'*12))
54
+ serial_port.should_receive(:read_data).and_return([0x02, 0xa0, 0x13, 0x0b].pack('c*'))
55
+ serial_port.should_receive(:read_data).and_return([0x66, 0xad, 0xcc, 0x1f, 0x10, 0x00,
56
+ 0x00, 0x58, 0x4f, 0x80, 0x8e, 0xa9].pack('c*'))
51
57
 
52
58
  # Lock Network
53
- serial_port.should_receive(:write).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x00, 0x01, 0x92].pack('c'*9))
54
- serial_port.should_receive(:read).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c'*6))
59
+ serial_port.should_receive(:write_data).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x00, 0x01, 0x92].pack('c*'))
60
+ serial_port.should_receive(:read_data).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c*'))
55
61
 
56
62
  # Update Time
57
- serial_port.should_receive(:write).and_return([0x02, 0x40, 0x22, 0x06, 0x66, 0xAD, 0xdb, 0xb4, 0xa8, 0x51, 0x0b].pack('c'*11))
58
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x22, 0x01, 0x00, 0x63].pack('c'*6))
59
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0xA2, 0x03, 0x66, 0xAD, 0x00, 0x2a].pack('c'*8))
63
+ serial_port.should_receive(:write_data).and_return([0x02, 0x40, 0x22, 0x06, 0x66, 0xAD, 0xdb, 0xb4, 0xa8, 0x51, 0x0b].pack('c*'))
64
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x22, 0x01, 0x00, 0x63].pack('c*'))
65
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0xA2, 0x03, 0x66, 0xAD, 0x00, 0x2a].pack('c*'))
60
66
 
61
- serial_port.should_receive(:close)
62
- serial_port.stub!(:closed?).and_return(false)
63
- subject.should_receive(:open_serial_port).and_return(serial_port)
64
- subject.should_receive(:boot)
65
- subject.should_receive(:boot_confirm)
67
+ serial_port.should_receive(:usb_close)
66
68
 
67
- subject.open_session do |session|
69
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
70
+ Hacklet::Dongle.open(Logger.new('/dev/null')) do |dongle|
68
71
  Timecop.freeze(Time.at(0x51a8b4db)) do
69
- session.commission
72
+ dongle.commission
70
73
  end
71
74
  end
72
75
  end
73
76
 
74
77
  it 'can request a sample' do
75
- serial_port = mock("SerialPort")
78
+ serial_port = double("Context").as_null_object
79
+
80
+ # Boot
81
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x04, 0x00, 0x44].pack('c*'))
82
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x84, 0x16, 0x01,
83
+ 0x00, 0x00, 0x87, 0x03, 0x00, 0x30, 0x00, 0x33, 0x83, 0x69, 0x9A, 0x0B,
84
+ 0x2F, 0x00, 0x00, 0x00, 0x58, 0x4F, 0x80, 0x0A, 0x1C, 0x81].pack('c*'))
85
+
86
+ # Boot Confirmation
87
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x00, 0x00, 0x40].pack('c*'))
88
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x80, 0x01, 0x10, 0xd1].pack('c*'))
89
+
90
+ # Lock Network
91
+ serial_port.should_receive(:write_data).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x00, 0x01, 0x92].pack('c*'))
92
+ serial_port.should_receive(:read_data).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c*'))
76
93
 
77
94
  # Selecting the network
78
- serial_port.should_receive(:write).with([0x02, 0x40, 0x03, 0x04, 0xA7, 0xB4, 0x05, 0x00, 0x51].pack('c'*9))
79
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x03, 0x01, 0x00, 0x42].pack('c'*6))
95
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x03, 0x04, 0xA7, 0xB4, 0x05, 0x00, 0x51].pack('c*'))
96
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x03, 0x01, 0x00, 0x42].pack('c*'))
80
97
 
81
98
  # Requesting the sample
82
- serial_port.should_receive(:write).with([0x02, 0x40, 0x24, 0x06, 0xA7, 0xB4,
83
- 0x00, 0x01, 0x0A, 0x00, 0x7a].pack('c'*11))
84
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x24, 0x01, 0x00, 0x65].pack('c'*6))
85
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0xA4, 0x12].pack('c'*4))
86
- serial_port.should_receive(:read).and_return([0xA7, 0xB4, 0x00, 0x01, 0x0A,
99
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x24, 0x06, 0xA7, 0xB4,
100
+ 0x00, 0x01, 0x0A, 0x00, 0x7a].pack('c*'))
101
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x24, 0x01, 0x00, 0x65].pack('c*'))
102
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0xA4, 0x12].pack('c*'))
103
+ serial_port.should_receive(:read_data).and_return([0xA7, 0xB4, 0x00, 0x01, 0x0A,
87
104
  0x00, 0x69, 0x8D, 0x44, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
88
- 0x00, 0x1d].pack('c'*19))
89
-
90
- serial_port.should_receive(:close)
91
- serial_port.stub!(:closed?).and_return(false)
92
- subject.should_receive(:open_serial_port).and_return(serial_port)
93
- subject.should_receive(:boot)
94
- subject.should_receive(:boot_confirm)
95
- subject.should_receive(:lock_network)
96
-
97
- subject.open_session do |session|
98
- session.lock_network
99
- session.select_network(0xA7B4)
100
- session.request_samples(0xA7B4, 0x0001)
105
+ 0x00, 0x1d].pack('c*'))
106
+
107
+ serial_port.should_receive(:usb_close)
108
+
109
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
110
+ Hacklet::Dongle.open(Logger.new('/dev/null')) do |dongle|
111
+ dongle.lock_network
112
+ dongle.select_network(0xA7B4)
113
+ dongle.request_samples(0xA7B4, 0x0001)
101
114
  end
102
115
  end
103
116
 
104
117
  it 'can enable a socket' do
105
- serial_port = mock("SerialPort")
118
+ serial_port = double("Context").as_null_object
119
+
120
+ # Boot
121
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x04, 0x00, 0x44].pack('c*'))
122
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x84, 0x16, 0x01,
123
+ 0x00, 0x00, 0x87, 0x03, 0x00, 0x30, 0x00, 0x33, 0x83, 0x69, 0x9A, 0x0B,
124
+ 0x2F, 0x00, 0x00, 0x00, 0x58, 0x4F, 0x80, 0x0A, 0x1C, 0x81].pack('c*'))
125
+
126
+ # Boot Confirmation
127
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x00, 0x00, 0x40].pack('c*'))
128
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x80, 0x01, 0x10, 0xd1].pack('c*'))
129
+
130
+ # Lock Network
131
+ serial_port.should_receive(:write_data).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x00, 0x01, 0x92].pack('c*'))
132
+ serial_port.should_receive(:read_data).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c*'))
106
133
 
107
134
  # Selecting the network
108
- serial_port.should_receive(:write).with([0x02, 0x40, 0x03, 0x04, 0xA7, 0xB4, 0x05, 0x00, 0x51].pack('c'*9))
109
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x03, 0x01, 0x00, 0x42].pack('c'*6))
135
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x03, 0x04, 0xA7, 0xB4, 0x05, 0x00, 0x51].pack('c*'))
136
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x03, 0x01, 0x00, 0x42].pack('c*'))
110
137
 
111
138
  # Switching
112
139
  request = [0xff]*56
113
140
  request[5] = 0xA5
114
141
  request = [0x02, 0x40, 0x23, 0x3B, 0xA7, 0xB4, 0x00] + request + [0x11]
115
- serial_port.should_receive(:write).with(request.pack('c'*request.length))
116
- serial_port.should_receive(:read).and_return([0x02, 0x40, 0x23, 0x01, 0x00, 0x62].pack('c'*6))
117
-
118
- serial_port.should_receive(:close)
119
- serial_port.stub!(:closed?).and_return(false)
120
- subject.should_receive(:open_serial_port).and_return(serial_port)
121
- subject.should_receive(:boot)
122
- subject.should_receive(:boot_confirm)
123
- subject.should_receive(:lock_network)
124
-
125
- subject.open_session do |session|
126
- session.lock_network
127
- session.select_network(0xA7B4)
128
- session.switch(0xA7B4, 0x0000, true)
142
+ serial_port.should_receive(:write_data).with(request.pack('c*'))
143
+ serial_port.should_receive(:read_data).and_return([0x02, 0x40, 0x23, 0x01, 0x00, 0x62].pack('c*'))
144
+
145
+ serial_port.should_receive(:usb_close)
146
+
147
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
148
+ Hacklet::Dongle.open(Logger.new('/dev/null')) do |dongle|
149
+ dongle.lock_network
150
+ dongle.select_network(0xA7B4)
151
+ dongle.switch(0xA7B4, 0x0000, true)
129
152
  end
130
153
  end
131
154
  end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hacklet::SerialConnection do
4
+ subject { Hacklet::SerialConnection.new(Logger.new('/dev/null')) }
5
+
6
+ describe 'initialization' do
7
+ it 'creates a serial port with no flow control' do
8
+ serial_port = double('Context')
9
+ serial_port.stub(:usb_open).with(0x0403, 0x8c81)
10
+ serial_port.stub(:set_bitmode).with(0x00, Ftdi::BitbangMode[:reset])
11
+ serial_port.stub(:baudrate=).with(115200)
12
+ serial_port.stub(:flowctrl=).with(Ftdi::SIO_DISABLE_FLOW_CTRL)
13
+ serial_port.stub(:dtr=).with(1)
14
+ serial_port.stub(:rts=).with(1)
15
+
16
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
17
+ subject
18
+ end
19
+ end
20
+
21
+ describe 'transmitting' do
22
+ subject { Hacklet::SerialConnection.new(Logger.new('/dev/null')) }
23
+
24
+ it 'is successful' do
25
+ serial_port = double('Context').as_null_object
26
+ serial_port.should_receive(:write_data).with([0x02, 0x40, 0x04, 0x00, 0x44].pack('c*'))
27
+
28
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
29
+ subject.transmit(Hacklet::BootRequest.new)
30
+ end
31
+ end
32
+
33
+ describe 'receiving' do
34
+ subject { Hacklet::SerialConnection.new(Logger.new('/dev/null')) }
35
+
36
+ it 'is successful' do
37
+ serial_port = double('Context').as_null_object
38
+ serial_port.should_receive(:read_data).and_return("\x02")
39
+
40
+ Ftdi::Context.should_receive(:new).and_return(serial_port)
41
+ subject.receive(1).should eq("\x02")
42
+ end
43
+ end
44
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hacklet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,16 +9,16 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-02 00:00:00.000000000 Z
12
+ date: 2013-06-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: serialport
15
+ name: libftdi-ruby
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 1.1.0
21
+ version: 0.0.20
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: 1.1.0
29
+ version: 0.0.20
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: bindata
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -130,10 +130,12 @@ files:
130
130
  - lib/hacklet/command.rb
131
131
  - lib/hacklet/dongle.rb
132
132
  - lib/hacklet/messages.rb
133
+ - lib/hacklet/serial_connection.rb
133
134
  - lib/hacklet/version.rb
134
135
  - spec/hacklet/command_spec.rb
135
136
  - spec/hacklet/dongle_spec.rb
136
137
  - spec/hacklet/messages_spec.rb
138
+ - spec/hacklet/serial_connection_spec.rb
137
139
  - spec/hacklet_spec.rb
138
140
  - spec/spec_helper.rb
139
141
  homepage: http://github.com/mcolyer/hacklet
@@ -165,5 +167,6 @@ test_files:
165
167
  - spec/hacklet/command_spec.rb
166
168
  - spec/hacklet/dongle_spec.rb
167
169
  - spec/hacklet/messages_spec.rb
170
+ - spec/hacklet/serial_connection_spec.rb
168
171
  - spec/hacklet_spec.rb
169
172
  - spec/spec_helper.rb