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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+
2
+ 0.5.0 / 2013-05-30
3
+ ==================
4
+
5
+ * Can commission new modlets, read data from them and control them
6
+ through a simple utility program. This is the minimal required in
7
+ order to be useful.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hacklet.gemspec
4
+ gemspec
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 [![Build Status](https://travis-ci.org/mcolyer/hacklet.png)](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
@@ -0,0 +1,11 @@
1
+ load "Rakefile.base"
2
+
3
+ desc "Run the tests"
4
+ task :test => :spec
5
+
6
+ # desc "Run the server"
7
+ # task :run do
8
+ # end
9
+
10
+ require 'rspec/core/rake_task'
11
+ RSpec::Core::RakeTask.new
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,4 @@
1
+ require "hacklet/version"
2
+ require "hacklet/messages"
3
+ require "hacklet/dongle"
4
+ require "hacklet/command"
@@ -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,3 @@
1
+ module Hacklet
2
+ VERSION = "0.5.0"
3
+ 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
@@ -0,0 +1,5 @@
1
+ describe Hacklet do
2
+ it 'should have a version number' do
3
+ Hacklet::VERSION.should_not be_nil
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'hacklet'
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