hacklet 0.5.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/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/Rakefile.base +57 -0
- data/bin/hacklet +13 -0
- data/hacklet.gemspec +26 -0
- data/lib/hacklet.rb +4 -0
- data/lib/hacklet/command.rb +83 -0
- data/lib/hacklet/dongle.rb +217 -0
- data/lib/hacklet/messages.rb +238 -0
- data/lib/hacklet/version.rb +3 -0
- data/spec/hacklet/command_spec.rb +65 -0
- data/spec/hacklet/dongle_spec.rb +125 -0
- data/spec/hacklet/messages_spec.rb +21 -0
- data/spec/hacklet_spec.rb +5 -0
- data/spec/spec_helper.rb +2 -0
- metadata +153 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Matt Colyer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Hacklet [](https://travis-ci.org/mcolyer/hacklet)
|
2
|
+
|
3
|
+
A library, written in Ruby, for controlling the [Modlet] (smart) outlet.
|
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 into 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.
|
10
|
+
|
11
|
+
There are alot of other similar products but this is the first one that
|
12
|
+
I've see that [costs $50][amazon] and includes control as well as monitoring of
|
13
|
+
the independent sockets.
|
14
|
+
|
15
|
+
## Why
|
16
|
+
|
17
|
+
So why write another client?
|
18
|
+
|
19
|
+
Unfortunately the client software included with the device isn't
|
20
|
+
available on all platforms (Linux) and it's pretty heavyweight for what
|
21
|
+
it does.
|
22
|
+
|
23
|
+
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.
|
26
|
+
|
27
|
+
## Getting Started
|
28
|
+
|
29
|
+
Right now things are pretty rough and won't work without modifying
|
30
|
+
`bin/hacklet`. Eventually the `hacklet` script will allow for specifying
|
31
|
+
which Modlet and socket you'd like to read or control.
|
32
|
+
|
33
|
+
```
|
34
|
+
bundle install
|
35
|
+
sudo modprobe ftdi_sio vendor=0x0403 product=0x8c81
|
36
|
+
bin/hacklet
|
37
|
+
```
|
38
|
+
|
39
|
+
## Status
|
40
|
+
|
41
|
+
* [X] Reading Data
|
42
|
+
* [X] Controlling Sockets
|
43
|
+
* [X] Multiple Modlet Support
|
44
|
+
* [X] Commissioning New Devices
|
45
|
+
* [X] Useful utility
|
46
|
+
* [ ] Set time on New Devices
|
47
|
+
* [ ] Documentation
|
48
|
+
|
49
|
+
[Modlet]: http://themodlet.com
|
50
|
+
[amazon]: http://www.amazon.com/ThinkEco-TE1010-Modlet-Starter-White/dp/B00AAT43OA/
|
data/Rakefile
ADDED
data/Rakefile.base
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
module Bundler
|
5
|
+
class GemHelper
|
6
|
+
# Override Bundler's concept of release.
|
7
|
+
def release_gem
|
8
|
+
with_fixed_editor {
|
9
|
+
guard_on_master_branch
|
10
|
+
return if already_tagged?
|
11
|
+
build_gem
|
12
|
+
edit_changelog
|
13
|
+
sh "git commit --allow-empty -a -m 'Release #{version_tag}'"
|
14
|
+
tag_version {
|
15
|
+
# Bundler's git_push pushes all branches. Let's restrict it
|
16
|
+
# to only the master branch since we also ensure that you
|
17
|
+
# always release from the master branch.
|
18
|
+
perform_git_push "origin master --tags"
|
19
|
+
}
|
20
|
+
}
|
21
|
+
end
|
22
|
+
def with_fixed_editor
|
23
|
+
editor = ENV['EDITOR'] || ""
|
24
|
+
abort "You must set an EDITOR to edit the changelog" if editor.empty?
|
25
|
+
swaps = {
|
26
|
+
"mate" => "mate -w",
|
27
|
+
"subl" => "subl -w"
|
28
|
+
}
|
29
|
+
begin
|
30
|
+
ENV['EDITOR'] = swaps.fetch(editor, editor)
|
31
|
+
yield
|
32
|
+
ensure
|
33
|
+
ENV['EDITOR'] = editor
|
34
|
+
end
|
35
|
+
end
|
36
|
+
def guard_on_master_branch
|
37
|
+
unless `git branch` =~ /^\* master$/
|
38
|
+
abort "You must be on the master branch to release."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
def edit_changelog
|
42
|
+
unless `which git-changelog`.empty?
|
43
|
+
#sh "git-changelog"
|
44
|
+
else
|
45
|
+
abort "git-changelog isn't found. Install it with `brew install git-extras`"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Run all tests"
|
52
|
+
task :default => :test
|
53
|
+
|
54
|
+
desc "Update Rakefile.base"
|
55
|
+
task :selfupdate do
|
56
|
+
sh "curl -sO https://raw.github.com/rcarver/gembase/master/Rakefile.base"
|
57
|
+
end
|
data/bin/hacklet
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'hacklet'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
logger = Logger.new(STDOUT)
|
10
|
+
logger.level = Logger::INFO
|
11
|
+
dongle = Hacklet::Dongle.new(logger)
|
12
|
+
|
13
|
+
Hacklet::Command.run(dongle, ARGV)
|
data/hacklet.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hacklet/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "hacklet"
|
8
|
+
gem.version = Hacklet::VERSION
|
9
|
+
gem.authors = ["Matt Colyer"]
|
10
|
+
gem.email = ["matt@colyer.name"]
|
11
|
+
gem.description = %q{An Open Source client for the Modlet (smart) outlet}
|
12
|
+
gem.summary = %q{A daemon, written in ruby, for controlling the Modlet outlet.}
|
13
|
+
gem.homepage = "http://github.com/mcolyer/hacklet"
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_dependency "serialport", "~>1.1.0"
|
22
|
+
gem.add_dependency "bindata", "~>1.5.0"
|
23
|
+
gem.add_dependency "slop", "~>3.4.0"
|
24
|
+
gem.add_development_dependency "rake"
|
25
|
+
gem.add_development_dependency "rspec"
|
26
|
+
end
|
data/lib/hacklet.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'slop'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Hacklet
|
5
|
+
class Command
|
6
|
+
def self.run(dongle, arguments)
|
7
|
+
Slop.parse(arguments, :help => true) do
|
8
|
+
command 'on', :banner => 'Turn on the specifed socket' do
|
9
|
+
on :n, :network=, 'The network id (ex. 0x1234)', :required => true
|
10
|
+
on :s, :socket=, 'The socket id (ex. 0)', :required => true
|
11
|
+
on :d, :debug, 'Enables debug logging' do
|
12
|
+
dongle.logger.level = Logger::DEBUG
|
13
|
+
end
|
14
|
+
|
15
|
+
run do |opts, args|
|
16
|
+
network_id = opts[:network][2..-1].to_i(16)
|
17
|
+
socket_id = opts[:socket].to_i
|
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
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
command 'off', :banner => 'Turn off the specifed socket' do
|
28
|
+
on :n, :network=, 'The network id (ex. 0x1234)', :required => true
|
29
|
+
on :s, :socket=, 'The socket id (ex. 0)', :required => true
|
30
|
+
on :d, :debug, 'Enables debug logging' do
|
31
|
+
dongle.logger.level = Logger::DEBUG
|
32
|
+
end
|
33
|
+
|
34
|
+
run do |opts, args|
|
35
|
+
network_id = opts[:network][2..-1].to_i(16)
|
36
|
+
socket_id = opts[:socket].to_i
|
37
|
+
|
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
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
command 'read', :banner => 'Read all available samples from the specified socket' do
|
47
|
+
on :n, :network=, 'The network id (ex. 0x1234)', :required => true
|
48
|
+
on :s, :socket=, 'The socket id (ex. 0)', :required => true
|
49
|
+
on :d, :debug, 'Enables debug logging' do
|
50
|
+
dongle.logger.level = Logger::DEBUG
|
51
|
+
end
|
52
|
+
|
53
|
+
run do |opts, args|
|
54
|
+
network_id = opts[:network][2..-1].to_i(16)
|
55
|
+
socket_id = opts[:socket].to_i
|
56
|
+
|
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
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
command 'commission', :banner => 'Add a new device to the network' do
|
66
|
+
on :d, :debug, 'Enables debug logging' do
|
67
|
+
dongle.logger.level = Logger::DEBUG
|
68
|
+
end
|
69
|
+
|
70
|
+
run do |opts, args|
|
71
|
+
dongle.open_session do |session|
|
72
|
+
session.commission
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
run do
|
78
|
+
puts help
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'serialport'
|
2
|
+
require 'logger'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
module Hacklet
|
6
|
+
class Dongle
|
7
|
+
attr_reader :logger
|
8
|
+
|
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.
|
16
|
+
#
|
17
|
+
# port - Optional string for configuring the serial port device.
|
18
|
+
#
|
19
|
+
# Returns nothing.
|
20
|
+
def open_session(port='/dev/ttyUSB0')
|
21
|
+
@serial = open_serial_port(port)
|
22
|
+
begin
|
23
|
+
@logger.info("Booting")
|
24
|
+
boot
|
25
|
+
boot_confirm
|
26
|
+
@logger.info("Booting complete")
|
27
|
+
yield self
|
28
|
+
ensure
|
29
|
+
@serial.close
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Listens for new devices on the network.
|
34
|
+
#
|
35
|
+
# This must be executed within an open session.
|
36
|
+
#
|
37
|
+
# Returns nothing.
|
38
|
+
def commission
|
39
|
+
require_session
|
40
|
+
|
41
|
+
begin
|
42
|
+
unlock_network
|
43
|
+
Timeout.timeout(30) do
|
44
|
+
loop do
|
45
|
+
@logger.info("Listening for devices ...")
|
46
|
+
buffer = receive(4)
|
47
|
+
buffer += receive(buffer.bytes.to_a[3]+1)
|
48
|
+
if buffer.bytes.to_a[1] == 0xa0
|
49
|
+
response = BroadcastResponse.read(buffer)
|
50
|
+
@logger.info("Found device 0x%x on network 0x%x" % [response.device_id, response.network_id])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue Timeout::Error
|
55
|
+
ensure
|
56
|
+
lock_network
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Selects the network.
|
61
|
+
#
|
62
|
+
# This must be executed within an open session. I'm guessing it selects the
|
63
|
+
# network.
|
64
|
+
#
|
65
|
+
# network_id - 2 byte identified for the network.
|
66
|
+
#
|
67
|
+
# Returns nothing.
|
68
|
+
def select_network(network_id)
|
69
|
+
require_session
|
70
|
+
|
71
|
+
transmit(HandshakeRequest.new(:network_id => network_id))
|
72
|
+
HandshakeResponse.read(receive(6))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Request stored samples.
|
76
|
+
#
|
77
|
+
# network_id - 2 byte identified for the network.
|
78
|
+
# channel_id - 2 byte identified for the channel.
|
79
|
+
#
|
80
|
+
# TODO: This needs to return a more usable set of data.
|
81
|
+
# Returns the SamplesResponse.
|
82
|
+
def request_samples(network_id, channel_id)
|
83
|
+
require_session
|
84
|
+
|
85
|
+
@logger.info("Requesting samples")
|
86
|
+
transmit(SamplesRequest.new(:network_id => network_id, :channel_id => channel_id))
|
87
|
+
AckResponse.read(receive(6))
|
88
|
+
buffer = receive(4)
|
89
|
+
remaining_bytes = buffer.bytes.to_a[3] + 1
|
90
|
+
buffer += receive(remaining_bytes)
|
91
|
+
response = SamplesResponse.read(buffer)
|
92
|
+
|
93
|
+
response.converted_samples.each do |time, wattage|
|
94
|
+
@logger.info("#{wattage}w at #{time}")
|
95
|
+
end
|
96
|
+
@logger.info("#{response.sample_count} returned, #{response.stored_sample_count} remaining")
|
97
|
+
|
98
|
+
response
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public: Used to controls whether a socket is on or off.
|
102
|
+
#
|
103
|
+
# network_id - 2 byte identified for the network.
|
104
|
+
# channel_id - 1 byte identified for the channel.
|
105
|
+
# enabled - true enables the socket and false disables it.
|
106
|
+
#
|
107
|
+
# Returns the SwitchResponse.
|
108
|
+
def switch(network_id, channel_id, state)
|
109
|
+
require_session
|
110
|
+
|
111
|
+
request = ScheduleRequest.new(:network_id => network_id, :channel_id => channel_id)
|
112
|
+
if state
|
113
|
+
request.always_on!
|
114
|
+
@logger.info("Turning on channel #{channel_id} on network 0x#{network_id.to_s(16)}")
|
115
|
+
else
|
116
|
+
request.always_off!
|
117
|
+
@logger.info("Turning off channel #{channel_id} on network 0x#{network_id.to_s(16)}")
|
118
|
+
end
|
119
|
+
transmit(request)
|
120
|
+
ScheduleResponse.read(receive(6))
|
121
|
+
end
|
122
|
+
|
123
|
+
# Public: Unlocks the network, to add a new device.
|
124
|
+
#
|
125
|
+
# Returns the BootConfirmResponse
|
126
|
+
def unlock_network
|
127
|
+
@logger.info("Unlocking network")
|
128
|
+
transmit(UnlockRequest.new)
|
129
|
+
LockResponse.read(receive(6))
|
130
|
+
@logger.info("Unlocking complete")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Public: Locks the network, prevents adding new devices.
|
134
|
+
#
|
135
|
+
# Returns the BootConfirmResponse
|
136
|
+
def lock_network
|
137
|
+
@logger.info("Locking network")
|
138
|
+
transmit(LockRequest.new)
|
139
|
+
LockResponse.read(receive(6))
|
140
|
+
@logger.info("Locking complete")
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
# Private: Initializes the dongle for communication
|
145
|
+
#
|
146
|
+
# Returns the BootResponse
|
147
|
+
def boot
|
148
|
+
transmit(BootRequest.new)
|
149
|
+
BootResponse.read(receive(27))
|
150
|
+
end
|
151
|
+
|
152
|
+
# Private: Confirms that booting was successful?
|
153
|
+
#
|
154
|
+
# Not sure about this.
|
155
|
+
#
|
156
|
+
# Returns the BootConfirmResponse
|
157
|
+
def boot_confirm
|
158
|
+
transmit(BootConfirmRequest.new)
|
159
|
+
BootConfirmResponse.read(receive(6))
|
160
|
+
end
|
161
|
+
|
162
|
+
# Private: Initializes the serial port
|
163
|
+
#
|
164
|
+
# port - the String to the device to open as a serial port.
|
165
|
+
#
|
166
|
+
# Returns a SerialPort object.
|
167
|
+
def open_serial_port(port)
|
168
|
+
serial_port = SerialPort.new(port, 115200, 8, 1, SerialPort::NONE)
|
169
|
+
serial_port.flow_control = SerialPort::NONE
|
170
|
+
serial_port
|
171
|
+
end
|
172
|
+
|
173
|
+
# Private: Transmits the packet to the dongle.
|
174
|
+
#
|
175
|
+
# command - The binary string to send.
|
176
|
+
#
|
177
|
+
# Returns the number of bytes written.
|
178
|
+
def transmit(command)
|
179
|
+
@logger.debug("TX: #{unpack(command.to_binary_s).inspect}")
|
180
|
+
@serial.write(command.to_binary_s) if @serial
|
181
|
+
end
|
182
|
+
|
183
|
+
# Private: Waits and receives the specified number of packets from the
|
184
|
+
# dongle.
|
185
|
+
#
|
186
|
+
# bytes - The number of bytes to read.
|
187
|
+
#
|
188
|
+
# Returns a binary string containing the response.
|
189
|
+
def receive(bytes)
|
190
|
+
if @serial
|
191
|
+
response = @serial.read(bytes)
|
192
|
+
else
|
193
|
+
response = "\x0\x0\x0\x0"
|
194
|
+
end
|
195
|
+
@logger.debug("RX: #{unpack(response).inspect}")
|
196
|
+
|
197
|
+
response
|
198
|
+
end
|
199
|
+
|
200
|
+
# Private: Prints a binary string a concise hexidecimal form for debugging
|
201
|
+
#
|
202
|
+
# message - The message to parse.
|
203
|
+
#
|
204
|
+
# Returns a string of hexidecimal representing equivalent to the message.
|
205
|
+
def unpack(message)
|
206
|
+
message.unpack('H2'*message.size)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Private: A helper to ensure that the serial port is active.
|
210
|
+
#
|
211
|
+
# Returns nothing.
|
212
|
+
# Raises RuntimeError if the serial port is not active.
|
213
|
+
def require_session
|
214
|
+
raise RuntimeError.new("Must be executed within an open session") unless @serial && !@serial.closed?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'bindata'
|
2
|
+
|
3
|
+
module Hacklet
|
4
|
+
class Message < BinData::Record
|
5
|
+
def calculate_checksum
|
6
|
+
checksum_fields = @field_names - [:header, :checksum]
|
7
|
+
buffer = StringIO.new
|
8
|
+
io = BinData::IO.new(buffer)
|
9
|
+
checksum_fields.each do |field|
|
10
|
+
send(field).do_write(io)
|
11
|
+
end
|
12
|
+
buffer.rewind
|
13
|
+
|
14
|
+
buffer.read.bytes.inject(0) { |s,x| s^x }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class BootResponse < Message
|
19
|
+
endian :big
|
20
|
+
|
21
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
22
|
+
uint16 :command, :check_value => lambda { value == 0x4084 }
|
23
|
+
uint8 :payload_length, :check_value => lambda { value == 22 }
|
24
|
+
|
25
|
+
# TODO: Determine what's in here
|
26
|
+
string :data, :length => 12
|
27
|
+
|
28
|
+
uint64 :device_id
|
29
|
+
|
30
|
+
# TODO: Determine what's in here
|
31
|
+
uint16 :data2
|
32
|
+
|
33
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
34
|
+
end
|
35
|
+
|
36
|
+
class BootConfirmResponse < Message
|
37
|
+
endian :big
|
38
|
+
|
39
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
40
|
+
uint16 :command, :check_value => lambda { value == 0x4080 }
|
41
|
+
uint8 :payload_length, :check_value => lambda { value == 1 }
|
42
|
+
|
43
|
+
uint8 :data, :check_value => lambda { value == 0x10 }
|
44
|
+
|
45
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
46
|
+
end
|
47
|
+
|
48
|
+
class BroadcastResponse < Message
|
49
|
+
endian :big
|
50
|
+
|
51
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
52
|
+
uint16 :command, :check_value => lambda { value == 0xA013 }
|
53
|
+
uint8 :payload_length, :check_value => lambda { value == 11 }
|
54
|
+
|
55
|
+
uint16 :network_id
|
56
|
+
uint64 :device_id
|
57
|
+
|
58
|
+
# TODO: Not sure why this is here.
|
59
|
+
uint8 :data
|
60
|
+
|
61
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
62
|
+
end
|
63
|
+
|
64
|
+
class LockResponse < Message
|
65
|
+
endian :big
|
66
|
+
|
67
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
68
|
+
uint16 :command, :check_value => lambda { value == 0xA0F9 }
|
69
|
+
uint8 :payload_length, :check_value => lambda { value == 1 }
|
70
|
+
|
71
|
+
uint8be :data, :check_value => lambda { value == 0x00 }
|
72
|
+
|
73
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
74
|
+
end
|
75
|
+
|
76
|
+
class HandshakeResponse < Message
|
77
|
+
endian :big
|
78
|
+
|
79
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
80
|
+
uint16 :command, :check_value => lambda { value == 0x4003 }
|
81
|
+
uint8 :payload_length, :check_value => lambda { value == 1 }
|
82
|
+
|
83
|
+
uint8be :data, :check_value => lambda { value == 0x00 }
|
84
|
+
|
85
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
86
|
+
end
|
87
|
+
|
88
|
+
class AckResponse < Message
|
89
|
+
endian :big
|
90
|
+
|
91
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
92
|
+
uint16 :command, :check_value => lambda { value == 0x4024 }
|
93
|
+
uint8 :payload_length, :check_value => lambda { value == 1 }
|
94
|
+
|
95
|
+
uint8be :data, :check_value => lambda { value == 0x00 }
|
96
|
+
|
97
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
98
|
+
end
|
99
|
+
|
100
|
+
class SamplesResponse < Message
|
101
|
+
endian :big
|
102
|
+
|
103
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
104
|
+
uint16 :command, :check_value => lambda { value == 0x40A4 }
|
105
|
+
uint8 :payload_length
|
106
|
+
|
107
|
+
uint16 :network_id
|
108
|
+
uint16 :channel_id
|
109
|
+
|
110
|
+
# TODO: Confirm this is actually the device id
|
111
|
+
uint16 :data
|
112
|
+
|
113
|
+
uint32le :time
|
114
|
+
uint8 :sample_count
|
115
|
+
uint24le :stored_sample_count
|
116
|
+
array :samples, :type => [:uint16le], :initial_length => :sample_count
|
117
|
+
|
118
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
119
|
+
|
120
|
+
def converted_samples
|
121
|
+
t = time - 10
|
122
|
+
samples.map { |s| t += 10; [Time.at(t), (s/13.0).round] }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class ScheduleResponse < Message
|
127
|
+
endian :big
|
128
|
+
|
129
|
+
uint8 :header, :check_value => lambda { value == 0x02 }
|
130
|
+
uint16 :command, :check_value => lambda { value == 0x4023 }
|
131
|
+
uint8 :payload_length, :check_value => lambda { value == 1 }
|
132
|
+
|
133
|
+
uint8be :data, :check_value => lambda { value == 0x00 }
|
134
|
+
|
135
|
+
uint8 :checksum, :check_value => lambda { calculate_checksum == checksum }
|
136
|
+
end
|
137
|
+
|
138
|
+
class BootRequest < Message
|
139
|
+
endian :big
|
140
|
+
|
141
|
+
uint8 :header, :initial_value => 0x02
|
142
|
+
uint16 :command, :initial_value => 0x4004
|
143
|
+
uint8 :payload_length
|
144
|
+
|
145
|
+
uint8 :checksum, :value => :calculate_checksum
|
146
|
+
end
|
147
|
+
|
148
|
+
class BootConfirmRequest < Message
|
149
|
+
endian :big
|
150
|
+
|
151
|
+
uint8 :header, :initial_value => 0x02
|
152
|
+
uint16 :command, :initial_value => 0x4000
|
153
|
+
uint8 :payload_length
|
154
|
+
|
155
|
+
uint8 :checksum, :value => :calculate_checksum
|
156
|
+
end
|
157
|
+
|
158
|
+
class UnlockRequest < Message
|
159
|
+
endian :big
|
160
|
+
|
161
|
+
uint8 :header, :initial_value => 0x02
|
162
|
+
uint16 :command, :initial_value => 0xA236
|
163
|
+
uint8 :payload_length, :initial_value => 4
|
164
|
+
|
165
|
+
# TODO: What is this?
|
166
|
+
uint32 :data, :initial_value => 0xFCFF9001
|
167
|
+
|
168
|
+
uint8 :checksum, :value => :calculate_checksum
|
169
|
+
end
|
170
|
+
|
171
|
+
class LockRequest < Message
|
172
|
+
endian :big
|
173
|
+
|
174
|
+
uint8 :header, :initial_value => 0x02
|
175
|
+
uint16 :command, :initial_value => 0xA236
|
176
|
+
uint8 :payload_length, :initial_value => 4
|
177
|
+
|
178
|
+
# TODO: What is this?
|
179
|
+
uint32 :data, :initial_value => 0xFCFF0001
|
180
|
+
|
181
|
+
uint8 :checksum, :value => :calculate_checksum
|
182
|
+
end
|
183
|
+
|
184
|
+
class HandshakeRequest < Message
|
185
|
+
endian :big
|
186
|
+
|
187
|
+
uint8 :header, :initial_value => 0x02
|
188
|
+
uint16 :command, :initial_value => 0x4003
|
189
|
+
uint8 :payload_length, :initial_value => 4
|
190
|
+
|
191
|
+
uint16 :network_id
|
192
|
+
# TODO: What is this?
|
193
|
+
uint16 :data, :initial_value => 0x0500
|
194
|
+
|
195
|
+
uint8 :checksum, :value => :calculate_checksum
|
196
|
+
end
|
197
|
+
|
198
|
+
class SamplesRequest < Message
|
199
|
+
endian :big
|
200
|
+
|
201
|
+
uint8 :header, :initial_value => 0x02
|
202
|
+
uint16 :command, :initial_value => 0x4024
|
203
|
+
uint8 :payload_length, :initial_value => 6
|
204
|
+
|
205
|
+
uint16 :network_id
|
206
|
+
uint16 :channel_id
|
207
|
+
# TODO: What is this?
|
208
|
+
uint16 :data, :initial_value => 0x0A00
|
209
|
+
|
210
|
+
uint8 :checksum, :value => :calculate_checksum
|
211
|
+
end
|
212
|
+
|
213
|
+
class ScheduleRequest < Message
|
214
|
+
endian :big
|
215
|
+
|
216
|
+
uint8 :header, :initial_value => 0x02
|
217
|
+
uint16 :command, :initial_value => 0x4023
|
218
|
+
uint8 :payload_length, :initial_value => 59
|
219
|
+
|
220
|
+
uint16 :network_id
|
221
|
+
uint8 :channel_id
|
222
|
+
array :schedule, :type => [:uint8], :initial_length => 56
|
223
|
+
|
224
|
+
uint8 :checksum, :value => :calculate_checksum
|
225
|
+
|
226
|
+
def always_off!
|
227
|
+
bitmap = [0x7f]*56
|
228
|
+
bitmap[5] = 0x25
|
229
|
+
schedule.assign(bitmap)
|
230
|
+
end
|
231
|
+
|
232
|
+
def always_on!
|
233
|
+
bitmap = [0xff]*56
|
234
|
+
bitmap[5] = 0xa5
|
235
|
+
schedule.assign(bitmap)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hacklet::Command do
|
4
|
+
subject do
|
5
|
+
Hacklet::Command
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:dongle) { Hacklet::Dongle.new(Logger.new("/dev/null")) }
|
9
|
+
|
10
|
+
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
|
+
dongle.should_receive(:lock_network)
|
18
|
+
dongle.should_receive(:select_network).with(16)
|
19
|
+
dongle.should_receive(:switch).with(16, 1, true)
|
20
|
+
|
21
|
+
subject.run(dongle, ['on', '-n', '0x0010', '-s', '1'])
|
22
|
+
end
|
23
|
+
|
24
|
+
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
|
+
dongle.should_receive(:lock_network)
|
32
|
+
dongle.should_receive(:select_network).with(16)
|
33
|
+
dongle.should_receive(:switch).with(16, 0, false)
|
34
|
+
|
35
|
+
subject.run(dongle, ['off', '-n', '0x0010', '-s', '0'])
|
36
|
+
end
|
37
|
+
|
38
|
+
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
|
+
dongle.should_receive(:lock_network)
|
47
|
+
dongle.should_receive(:select_network).with(16)
|
48
|
+
dongle.should_receive(:request_samples).with(16, 1)
|
49
|
+
|
50
|
+
subject.run(dongle, ['read', '-n', '0x0010', '-s', '1'])
|
51
|
+
end
|
52
|
+
|
53
|
+
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
|
+
dongle.should_receive(:commission)
|
62
|
+
|
63
|
+
subject.run(dongle, ['commission'])
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hacklet::Dongle do
|
4
|
+
subject do
|
5
|
+
Hacklet::Dongle.new(Logger.new("/dev/null"))
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'can open a new session' do
|
9
|
+
serial_port = mock("SerialPort")
|
10
|
+
|
11
|
+
# 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,
|
14
|
+
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))
|
16
|
+
|
17
|
+
# 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))
|
20
|
+
|
21
|
+
serial_port.should_receive(:close)
|
22
|
+
subject.should_receive(:open_serial_port).and_return(serial_port)
|
23
|
+
subject.open_session do
|
24
|
+
# noop
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can find a new device' do
|
29
|
+
serial_port = mock("SerialPort")
|
30
|
+
|
31
|
+
# 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))
|
34
|
+
|
35
|
+
# 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,
|
38
|
+
0x00, 0x58, 0x4f, 0x80, 0x00, 0x77, 0x2a, 0x8a, 0x33, 0xa7, 0xf4, 0xf6,
|
39
|
+
0x80, 0xd0, 0x9c, 0x5d, 0x3c, 0x84, 0xf5, 0x2b, 0x43, 0x00, 0x00, 0x00,
|
40
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcb].pack('c'*36))
|
41
|
+
|
42
|
+
# 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))
|
46
|
+
|
47
|
+
# 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))
|
51
|
+
serial_port.should_receive(:read).and_raise(Timeout::Error)
|
52
|
+
|
53
|
+
# Lock Network
|
54
|
+
serial_port.should_receive(:write).with([0x02, 0xA2, 0x36, 0x04, 0xFC, 0xFF, 0x00, 0x01, 0x92].pack('c'*9))
|
55
|
+
serial_port.should_receive(:read).and_return([0x02, 0xA0, 0xF9, 0x01, 0x00, 0x58].pack('c'*6))
|
56
|
+
|
57
|
+
serial_port.should_receive(:close)
|
58
|
+
serial_port.stub!(:closed?).and_return(false)
|
59
|
+
subject.should_receive(:open_serial_port).and_return(serial_port)
|
60
|
+
subject.should_receive(:boot)
|
61
|
+
subject.should_receive(:boot_confirm)
|
62
|
+
|
63
|
+
subject.open_session do |session|
|
64
|
+
session.commission
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'can request a sample' do
|
69
|
+
serial_port = mock("SerialPort")
|
70
|
+
|
71
|
+
# Selecting the network
|
72
|
+
serial_port.should_receive(:write).with([0x02, 0x40, 0x03, 0x04, 0xA7, 0xB4, 0x05, 0x00, 0x51].pack('c'*9))
|
73
|
+
serial_port.should_receive(:read).and_return([0x02, 0x40, 0x03, 0x01, 0x00, 0x42].pack('c'*6))
|
74
|
+
|
75
|
+
# Requesting the sample
|
76
|
+
serial_port.should_receive(:write).with([0x02, 0x40, 0x24, 0x06, 0xA7, 0xB4,
|
77
|
+
0x00, 0x01, 0x0A, 0x00, 0x7a].pack('c'*11))
|
78
|
+
serial_port.should_receive(:read).and_return([0x02, 0x40, 0x24, 0x01, 0x00, 0x65].pack('c'*6))
|
79
|
+
serial_port.should_receive(:read).and_return([0x02, 0x40, 0xA4, 0x12].pack('c'*4))
|
80
|
+
serial_port.should_receive(:read).and_return([0xA7, 0xB4, 0x00, 0x01, 0x0A,
|
81
|
+
0x00, 0x69, 0x8D, 0x44, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
82
|
+
0x00, 0x1d].pack('c'*19))
|
83
|
+
|
84
|
+
serial_port.should_receive(:close)
|
85
|
+
serial_port.stub!(:closed?).and_return(false)
|
86
|
+
subject.should_receive(:open_serial_port).and_return(serial_port)
|
87
|
+
subject.should_receive(:boot)
|
88
|
+
subject.should_receive(:boot_confirm)
|
89
|
+
subject.should_receive(:lock_network)
|
90
|
+
|
91
|
+
subject.open_session do |session|
|
92
|
+
session.lock_network
|
93
|
+
session.select_network(0xA7B4)
|
94
|
+
session.request_samples(0xA7B4, 0x0001)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'can enable a socket' do
|
99
|
+
serial_port = mock("SerialPort")
|
100
|
+
|
101
|
+
# Selecting the network
|
102
|
+
serial_port.should_receive(:write).with([0x02, 0x40, 0x03, 0x04, 0xA7, 0xB4, 0x05, 0x00, 0x51].pack('c'*9))
|
103
|
+
serial_port.should_receive(:read).and_return([0x02, 0x40, 0x03, 0x01, 0x00, 0x42].pack('c'*6))
|
104
|
+
|
105
|
+
# Switching
|
106
|
+
request = [0xff]*56
|
107
|
+
request[5] = 0xA5
|
108
|
+
request = [0x02, 0x40, 0x23, 0x3B, 0xA7, 0xB4, 0x00] + request + [0x11]
|
109
|
+
serial_port.should_receive(:write).with(request.pack('c'*request.length))
|
110
|
+
serial_port.should_receive(:read).and_return([0x02, 0x40, 0x23, 0x01, 0x00, 0x62].pack('c'*6))
|
111
|
+
|
112
|
+
serial_port.should_receive(:close)
|
113
|
+
serial_port.stub!(:closed?).and_return(false)
|
114
|
+
subject.should_receive(:open_serial_port).and_return(serial_port)
|
115
|
+
subject.should_receive(:boot)
|
116
|
+
subject.should_receive(:boot_confirm)
|
117
|
+
subject.should_receive(:lock_network)
|
118
|
+
|
119
|
+
subject.open_session do |session|
|
120
|
+
session.lock_network
|
121
|
+
session.select_network(0xA7B4)
|
122
|
+
session.switch(0xA7B4, 0x0000, true)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Messages' do
|
4
|
+
describe 'responses' do
|
5
|
+
let(:bad_checksum) { "\x02\x40\x80\x01\x10\x01" }
|
6
|
+
|
7
|
+
describe Hacklet::BootConfirmResponse do
|
8
|
+
it 'detects an invalid checksum' do
|
9
|
+
expect { Hacklet::BootConfirmResponse.read(bad_checksum) }.to raise_error(BinData::ValidityError)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'requests' do
|
15
|
+
describe Hacklet::BootRequest do
|
16
|
+
it 'has a proper checksum' do
|
17
|
+
subject.checksum.should eq(0x44)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hacklet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Colyer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: serialport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.1.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bindata
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.5.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.5.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: slop
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.4.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.4.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: An Open Source client for the Modlet (smart) outlet
|
95
|
+
email:
|
96
|
+
- matt@colyer.name
|
97
|
+
executables:
|
98
|
+
- hacklet
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- .gitignore
|
103
|
+
- .rspec
|
104
|
+
- .travis.yml
|
105
|
+
- CHANGELOG.md
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- Rakefile.base
|
111
|
+
- bin/hacklet
|
112
|
+
- hacklet.gemspec
|
113
|
+
- lib/hacklet.rb
|
114
|
+
- lib/hacklet/command.rb
|
115
|
+
- lib/hacklet/dongle.rb
|
116
|
+
- lib/hacklet/messages.rb
|
117
|
+
- lib/hacklet/version.rb
|
118
|
+
- spec/hacklet/command_spec.rb
|
119
|
+
- spec/hacklet/dongle_spec.rb
|
120
|
+
- spec/hacklet/messages_spec.rb
|
121
|
+
- spec/hacklet_spec.rb
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
homepage: http://github.com/mcolyer/hacklet
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 1.8.23
|
145
|
+
signing_key:
|
146
|
+
specification_version: 3
|
147
|
+
summary: A daemon, written in ruby, for controlling the Modlet outlet.
|
148
|
+
test_files:
|
149
|
+
- spec/hacklet/command_spec.rb
|
150
|
+
- spec/hacklet/dongle_spec.rb
|
151
|
+
- spec/hacklet/messages_spec.rb
|
152
|
+
- spec/hacklet_spec.rb
|
153
|
+
- spec/spec_helper.rb
|