ban 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/ban.gemspec +35 -0
- data/bin/ban +10 -0
- data/doc/ban-layout.png +0 -0
- data/doc/ban-layout.svg +2905 -0
- data/doc/ban.fzz +0 -0
- data/examples/client.rb +26 -0
- data/ino.ini +5 -0
- data/lib/ban.rb +50 -0
- data/lib/ban/board.rb +49 -0
- data/lib/ban/cli.rb +45 -0
- data/lib/ban/event.rb +7 -0
- data/lib/ban/events/door_event.rb +31 -0
- data/lib/ban/events/ir_event.rb +31 -0
- data/lib/ban/events/rc_event.rb +59 -0
- data/lib/ban/server.rb +37 -0
- data/lib/ban/version.rb +3 -0
- data/spec/ban/events/door_event_spec.rb +25 -0
- data/spec/ban/events/ir_event_spec.rb +16 -0
- data/spec/ban/events/rc_event_spec.rb +42 -0
- data/spec/ban_spec.rb +41 -0
- data/spec/spec_helper.rb +5 -0
- data/src/HomeControl.ino +144 -0
- metadata +194 -0
data/doc/ban.fzz
ADDED
Binary file
|
data/examples/client.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'faye/websocket'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
def rc_turn(state, address)
|
6
|
+
{ "rc-turn-#{state}" => { 'address' => address } }.to_json
|
7
|
+
end
|
8
|
+
|
9
|
+
EM.run do
|
10
|
+
ws = Faye::WebSocket::Client.new('ws://localhost:8080/')
|
11
|
+
light = '11110D'
|
12
|
+
|
13
|
+
ws.on :open do |event|
|
14
|
+
puts "Connected"
|
15
|
+
ws.send(rc_turn(:on, light))
|
16
|
+
EM.add_timer(5) { ws.send(rc_turn(:off, light)) }
|
17
|
+
end
|
18
|
+
|
19
|
+
ws.on :message do |event|
|
20
|
+
p JSON.parse(event.data)
|
21
|
+
end
|
22
|
+
|
23
|
+
ws.on :close do |event|
|
24
|
+
puts "Connection closed"
|
25
|
+
end
|
26
|
+
end
|
data/ino.ini
ADDED
data/lib/ban.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'ban/version'
|
2
|
+
require 'logger'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# used gems
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'em-websocket'
|
8
|
+
require 'arduino_firmata'
|
9
|
+
require 'thor'
|
10
|
+
|
11
|
+
module Ban
|
12
|
+
Logger = Logger.new(STDOUT)
|
13
|
+
|
14
|
+
# @param [Array<Fixnum>] data 7 bit encoded data from firmata (midi)
|
15
|
+
# @return [String] ascii
|
16
|
+
def self.decode7bit(data)
|
17
|
+
msg = ""
|
18
|
+
i = 0
|
19
|
+
while i < data.size
|
20
|
+
lsb, msb = data[i], data[i + 1]
|
21
|
+
|
22
|
+
if msb == 0
|
23
|
+
msg << lsb.chr
|
24
|
+
else
|
25
|
+
msg << (lsb + 0b10000000).chr
|
26
|
+
end
|
27
|
+
i += 2
|
28
|
+
end
|
29
|
+
msg
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [String] data the data to convert to midi 7 bit encoded string
|
33
|
+
# @return [Array<Fixnum>] array of midi bytes
|
34
|
+
def self.encode7bit(data)
|
35
|
+
msg = []
|
36
|
+
data.each_byte do |byte|
|
37
|
+
msg << (byte & 0b01111111) # LSB
|
38
|
+
msg << (byte >> 7 & 0b01111111) # MSB
|
39
|
+
end
|
40
|
+
msg
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
require 'ban/server'
|
45
|
+
require 'ban/event'
|
46
|
+
require 'ban/events/door_event'
|
47
|
+
require 'ban/events/ir_event'
|
48
|
+
require 'ban/events/rc_event'
|
49
|
+
require 'ban/board'
|
50
|
+
require 'ban/cli'
|
data/lib/ban/board.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Ban
|
2
|
+
class Board
|
3
|
+
include EventEmitter
|
4
|
+
SEND_RC_ON = 0x20
|
5
|
+
SEND_RC_OFF = 0x21
|
6
|
+
STRING_DATA = 0x71
|
7
|
+
EVENTS = {
|
8
|
+
DoorEvent::CMD => DoorEvent,
|
9
|
+
IrEvent::CMD => IrEvent,
|
10
|
+
RcEvent::CMD => RcEvent
|
11
|
+
}
|
12
|
+
|
13
|
+
def start(path)
|
14
|
+
@path = path
|
15
|
+
@firmata = ArduinoFirmata.connect @path, nonblock_io: true,
|
16
|
+
eventmachine: true
|
17
|
+
Logger.debug "Firmata Version: #{@firmata.version}"
|
18
|
+
|
19
|
+
arduino = self
|
20
|
+
@firmata.on :sysex do |cmd, data|
|
21
|
+
arduino.received_sysex(cmd, data)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
@firmata.close
|
27
|
+
end
|
28
|
+
|
29
|
+
def received_sysex(event_code, data)
|
30
|
+
if klass = EVENTS[event_code]
|
31
|
+
emit :event, klass.parse(data)
|
32
|
+
else
|
33
|
+
emit :error, msg: 'Unknown event!', event_code: event_code, data: data
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def turn_on(address)
|
38
|
+
send_command(SEND_RC_ON, address)
|
39
|
+
end
|
40
|
+
|
41
|
+
def turn_off(address)
|
42
|
+
send_command(SEND_RC_OFF, address)
|
43
|
+
end
|
44
|
+
|
45
|
+
def send_command(command, data)
|
46
|
+
@firmata.sysex(STRING_DATA, Ban.encode7bit(command.chr + data))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/ban/cli.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Ban
|
2
|
+
class CLI < Thor
|
3
|
+
option :device, default: nil
|
4
|
+
option :port, type: :numeric, default: 8080
|
5
|
+
option :interface, type: :string, default: '0.0.0.0'
|
6
|
+
desc "server", "starts the ban server"
|
7
|
+
def server
|
8
|
+
device = options[:device]
|
9
|
+
interface = options[:interface]
|
10
|
+
port = options[:port]
|
11
|
+
|
12
|
+
EM.epoll
|
13
|
+
EM.run do
|
14
|
+
board = Board.new
|
15
|
+
server = Server.new
|
16
|
+
|
17
|
+
board.on :event do |event|
|
18
|
+
if event.valid?
|
19
|
+
Ban::Logger.debug "#{event.class}: #{event}"
|
20
|
+
server.broadcast event
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
server.on :command do |command|
|
25
|
+
Ban::Logger.debug "Received cmd #{command}"
|
26
|
+
if options = command['rc-turn-off']
|
27
|
+
Ban::Logger.debug "Turn off #{options['address']}"
|
28
|
+
board.turn_off(options['address'])
|
29
|
+
elsif options = command['rc-turn-on']
|
30
|
+
Ban::Logger.debug "Turn on #{options['address']}"
|
31
|
+
board.turn_on(options['address'])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
trap(:INT) { board.close; EM.stop }
|
36
|
+
trap(:TERM) { board.close; EM.stop }
|
37
|
+
|
38
|
+
Ban::Logger.info "Connecting to #{device || 'auto'}"
|
39
|
+
board.start(device)
|
40
|
+
Ban::Logger.info "Starting WebSocket Server on #{interface}:#{port}"
|
41
|
+
server.start(interface, port)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/ban/event.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ban
|
2
|
+
class DoorEvent < Event
|
3
|
+
CMD = 0x37
|
4
|
+
|
5
|
+
attr_reader :state
|
6
|
+
|
7
|
+
def self.parse(data)
|
8
|
+
new(Ban.decode7bit(data).downcase)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(state)
|
12
|
+
@state = state
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
open? ? 'door-opened' : 'door-closed'
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_hash
|
20
|
+
{ 'state' => @state }
|
21
|
+
end
|
22
|
+
|
23
|
+
def open?
|
24
|
+
@state == 'open'
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"door: #{open? ? 'opened' : 'closed'}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ban
|
2
|
+
class IrEvent < Event
|
3
|
+
CMD = 0x36
|
4
|
+
|
5
|
+
attr_reader :code
|
6
|
+
|
7
|
+
def self.parse(data)
|
8
|
+
new(Ban.decode7bit(data).to_i)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(code)
|
12
|
+
@code = code
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
'ir-received'
|
17
|
+
end
|
18
|
+
|
19
|
+
def hex
|
20
|
+
code.to_s(16)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_hash
|
24
|
+
{ 'code' => @code, 'hex' => hex }
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"#{code} (0x#{hex})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Ban
|
2
|
+
class RcEvent < Event
|
3
|
+
CMD = 0x35
|
4
|
+
|
5
|
+
attr_reader :decimal, :bits, :delay, :protocol
|
6
|
+
|
7
|
+
def self.parse(data)
|
8
|
+
new(*Ban.decode7bit(data).split('|').map(&:to_i))
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(decimal, bits, delay, protocol)
|
12
|
+
@decimal, @bits, @delay, @protocol = decimal, bits, delay, protocol
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
(tristate[-1] == 'F') ? 'rc-turned-on' : 'rc-turned-off'
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
tristate
|
21
|
+
true
|
22
|
+
rescue ArgumentError
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
{
|
28
|
+
'decimal' => decimal,
|
29
|
+
'bits' => bits,
|
30
|
+
'binary' => binary,
|
31
|
+
'tristate' => tristate,
|
32
|
+
'delay' => delay,
|
33
|
+
'protocol' => protocol
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"decimal: #{decimal} (#{bits} bits) binary #{binary} " +
|
39
|
+
"tri-state: #{tristate} pulse length: #{delay} (ms) " +
|
40
|
+
"protocol: #{protocol}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def binary
|
44
|
+
("%#{bits}s" % decimal.to_s(2)).gsub(' ', '0')
|
45
|
+
end
|
46
|
+
|
47
|
+
def tristate
|
48
|
+
binary.gsub(/[0-9]{2}/) do |match|
|
49
|
+
if match == '00'
|
50
|
+
'0'
|
51
|
+
elsif match == '01'
|
52
|
+
'F'
|
53
|
+
else
|
54
|
+
raise ArgumentError, "#{match} not applicable"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/ban/server.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Ban
|
2
|
+
class Server
|
3
|
+
include EventEmitter
|
4
|
+
def initialize
|
5
|
+
@clients = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def broadcast(event)
|
9
|
+
@clients.each do |client|
|
10
|
+
client.send({ event.name => event.to_hash }.to_json)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def start(interface, port)
|
15
|
+
@server = EM::WebSocket.run(host: interface, port: port) do |ws|
|
16
|
+
ws.onopen do |handshake|
|
17
|
+
Ban::Logger.debug "WebSocket connection open"
|
18
|
+
@clients << ws
|
19
|
+
end
|
20
|
+
|
21
|
+
ws.onclose do
|
22
|
+
Ban::Logger.debug "WebSocket connection closed"
|
23
|
+
@clients.delete(ws)
|
24
|
+
end
|
25
|
+
|
26
|
+
ws.onmessage do |command_json|
|
27
|
+
begin
|
28
|
+
command = JSON.parse(command_json)
|
29
|
+
emit :command, command
|
30
|
+
rescue JSON::ParserError
|
31
|
+
Ban::Logger.warn "Received invalid json: #{command_json}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/ban/version.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ban::DoorEvent do
|
4
|
+
context 'open' do
|
5
|
+
let(:data) { [79, 0, 80, 0, 69, 0, 78, 0] }
|
6
|
+
|
7
|
+
subject { described_class.parse(data) }
|
8
|
+
|
9
|
+
its(:name) { should == 'door-opened' }
|
10
|
+
its(:valid?) { should == true }
|
11
|
+
its(:to_hash) { should == { "state" => "open" } }
|
12
|
+
its(:to_s) { should == "door: opened" }
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'close' do
|
16
|
+
let(:data) { [67, 0, 76, 0, 79, 0, 83, 0, 69, 0] }
|
17
|
+
|
18
|
+
subject { described_class.parse(data) }
|
19
|
+
|
20
|
+
its(:name) { should == 'door-closed' }
|
21
|
+
its(:valid?) { should == true }
|
22
|
+
its(:to_hash) { should == { "state" => "close" } }
|
23
|
+
its(:to_s) { should == "door: closed" }
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ban::IrEvent do
|
4
|
+
let(:data) { [51, 0, 48, 0, 54, 0, 56, 0, 57, 0] }
|
5
|
+
|
6
|
+
subject { described_class.parse(data) }
|
7
|
+
|
8
|
+
its(:name) { should == 'ir-received' }
|
9
|
+
its(:valid?) { should == true }
|
10
|
+
its(:to_hash) do
|
11
|
+
should == { "code" => 30689, "hex" => "77e1" }
|
12
|
+
end
|
13
|
+
its(:to_s) do
|
14
|
+
should == "30689 (0x77e1)"
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ban::RcEvent do
|
4
|
+
subject { described_class.parse(data) }
|
5
|
+
|
6
|
+
context 'valid' do
|
7
|
+
let(:data) do
|
8
|
+
[
|
9
|
+
50, 0, 49, 0, 53, 0, 56, 0, 56, 0, 124, 0, 50, 0,
|
10
|
+
52, 0, 124, 0, 51, 0, 50, 0, 50, 0, 124, 0, 49, 0
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
its(:name) { should == 'rc-turned-off' }
|
15
|
+
its(:valid?) { should == true }
|
16
|
+
its(:to_hash) do
|
17
|
+
should == {
|
18
|
+
"decimal" => 21588,
|
19
|
+
"bits" => 24,
|
20
|
+
"binary" => "000000000101010001010100",
|
21
|
+
"tristate" => "0000FFF0FFF0",
|
22
|
+
"delay" => 322,
|
23
|
+
"protocol" => 1
|
24
|
+
}
|
25
|
+
end
|
26
|
+
its(:to_s) do
|
27
|
+
should == "decimal: 21588 (24 bits) binary 000000000101010001010100 " +
|
28
|
+
"tri-state: 0000FFF0FFF0 pulse length: 322 (ms) protocol: 1"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'invalid' do
|
33
|
+
let(:data) do
|
34
|
+
[
|
35
|
+
50, 0, 49, 0, 53, 0, 56, 0, 30, 0, 124, 0, 50, 0,
|
36
|
+
52, 0, 124, 0, 51, 0, 50, 0, 50, 0, 124, 0, 49, 0
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
its(:valid?) { should == false }
|
41
|
+
end
|
42
|
+
end
|