hacklet 0.5.1 → 0.6.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.
- data/.travis.yml +2 -0
- data/CHANGELOG.md +14 -0
- data/README.md +65 -21
- data/bin/hacklet +3 -2
- data/hacklet.gemspec +1 -1
- data/lib/hacklet.rb +1 -0
- data/lib/hacklet/command.rb +10 -18
- data/lib/hacklet/dongle.rb +39 -103
- data/lib/hacklet/serial_connection.rb +72 -0
- data/lib/hacklet/version.rb +1 -1
- data/spec/hacklet/command_spec.rb +4 -27
- data/spec/hacklet/dongle_spec.rb +98 -75
- data/spec/hacklet/serial_connection_spec.rb +44 -0
- metadata +8 -5
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
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
|
25
|
-
|
27
|
+
bundled client but in a lightweight manner without dependencies on
|
28
|
+
external services.
|
26
29
|
|
27
|
-
##
|
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
|
-
|
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)!
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|
-
###
|
95
|
+
### Contribution Workflow
|
59
96
|
|
60
|
-
*
|
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/
|
70
|
-
[
|
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
|
data/bin/hacklet
CHANGED
data/hacklet.gemspec
CHANGED
@@ -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 "
|
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"
|
data/lib/hacklet.rb
CHANGED
data/lib/hacklet/command.rb
CHANGED
@@ -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.
|
20
|
-
|
21
|
-
|
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.
|
39
|
-
|
40
|
-
|
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.
|
58
|
-
|
59
|
-
|
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.
|
72
|
-
session.commission
|
73
|
-
end
|
65
|
+
dongle.commission
|
74
66
|
end
|
75
67
|
end
|
76
68
|
|
data/lib/hacklet/dongle.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
10
|
+
# logger - The Logger instance to log to, defaults to STDOUT.
|
18
11
|
#
|
19
12
|
# Returns nothing.
|
20
|
-
def
|
21
|
-
|
13
|
+
def self.open(logger=Logger.new(STDOUT))
|
14
|
+
serial = SerialConnection.new(logger)
|
15
|
+
dongle = Dongle.new(serial, logger)
|
22
16
|
begin
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@logger.info("Booting complete")
|
27
|
-
yield self
|
17
|
+
dongle.send(:boot)
|
18
|
+
dongle.send(:boot_confirm)
|
19
|
+
yield dongle
|
28
20
|
ensure
|
29
|
-
|
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
|
-
|
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
|
-
|
153
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
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
|
data/lib/hacklet/version.rb
CHANGED
@@ -5,15 +5,12 @@ describe Hacklet::Command do
|
|
5
5
|
Hacklet::Command
|
6
6
|
end
|
7
7
|
|
8
|
-
let(:dongle)
|
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'])
|
data/spec/hacklet/dongle_spec.rb
CHANGED
@@ -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 =
|
5
|
+
serial_port = double("Context").as_null_object
|
10
6
|
|
11
7
|
# Boot
|
12
|
-
serial_port.should_receive(:
|
13
|
-
serial_port.should_receive(:
|
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'
|
11
|
+
0x2F, 0x00, 0x00, 0x00, 0x58, 0x4F, 0x80, 0x0A, 0x1C, 0x81].pack('c*'))
|
16
12
|
|
17
13
|
# Boot Confirmation
|
18
|
-
serial_port.should_receive(:
|
19
|
-
serial_port.should_receive(:
|
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(:
|
22
|
-
|
23
|
-
|
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 =
|
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(:
|
33
|
-
serial_port.should_receive(:
|
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(:
|
37
|
-
serial_port.should_receive(:
|
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'
|
46
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcb].pack('c*'))
|
41
47
|
|
42
48
|
# Ignored packet #2
|
43
|
-
serial_port.should_receive(:
|
44
|
-
serial_port.should_receive(:
|
45
|
-
0x58, 0x4f, 0x80, 0x00, 0x05].pack('c'
|
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(:
|
49
|
-
serial_port.should_receive(:
|
50
|
-
0x00, 0x58, 0x4f, 0x80, 0x8e, 0xa9].pack('c'
|
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(:
|
54
|
-
serial_port.should_receive(:
|
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(:
|
58
|
-
serial_port.should_receive(:
|
59
|
-
serial_port.should_receive(:
|
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(:
|
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
|
-
|
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
|
-
|
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 =
|
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(:
|
79
|
-
serial_port.should_receive(:
|
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(:
|
83
|
-
0x00, 0x01, 0x0A, 0x00, 0x7a].pack('c'
|
84
|
-
serial_port.should_receive(:
|
85
|
-
serial_port.should_receive(:
|
86
|
-
serial_port.should_receive(:
|
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'
|
89
|
-
|
90
|
-
serial_port.should_receive(:
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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 =
|
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(:
|
109
|
-
serial_port.should_receive(:
|
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(:
|
116
|
-
serial_port.should_receive(:
|
117
|
-
|
118
|
-
serial_port.should_receive(:
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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.
|
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-
|
12
|
+
date: 2013-06-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
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:
|
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:
|
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
|