Skip to content

Commit

Permalink
a first working version of the state mapper
Browse files Browse the repository at this point in the history
  • Loading branch information
Elias Karakoulakis committed Jan 4, 2012
1 parent 643b654 commit d545344
Show file tree
Hide file tree
Showing 11 changed files with 424 additions and 181 deletions.
8 changes: 4 additions & 4 deletions ansible_callback.rb
Expand Up @@ -41,12 +41,12 @@ module AnsibleCallback
def declare_callback(cb, &cb_body)
raise "declare_callback: 1st argument must be a Symbol" unless cb.is_a?Symbol
raise "declare_callback: 2nd argument must be a Proc" unless cb_body.is_a?Proc
@callbacks = {} unless @callbacks.is_a?Hash
@callbacks = {} if @callbacks.nil?
if (cb.to_s[0..1] == "on") then
puts "Registering callback (#{cb}) for #{self}"
puts "Registering callback (#{cb}) for #{self.inspect}"
@callbacks[cb] = cb_body
elsif (cb.to_s == "default") then
puts "Registering DEFAULT callback for #{self}"
puts "Registering DEFAULT callback for #{self.inspect}"
@callbacks.default = cb_body
end
end
Expand All @@ -62,7 +62,7 @@ def fire_callback(cb, *args)
@callbacks = {} unless @callbacks.is_a?Hash
default = @callbacks.has_key?(cb)
if (cb_proc = @callbacks[cb]).is_a?Proc then
puts "firing callback(#{cb}) args: #{args.inspect}"
puts "firing callback(#{cb}) args: #{args.inspect}" if $DEBUG
cb_proc .call(self, cb.to_s, *args)
else
#puts "WARNING: callback #{cb} not found for #{self}, iv=#{iv} cb_proc=#{cb_proc.inspect}"
Expand Down
23 changes: 15 additions & 8 deletions ansible_value.rb
Expand Up @@ -22,8 +22,12 @@
http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
=end

module AnsibleValue
require 'ansible_callback'

module AnsibleValue

include AnsibleCallback

attr_reader :previous_value, :current_value
attr_reader :last_update

Expand All @@ -35,10 +39,12 @@ def matches?(hash)
hash.each { |iv_symbol, filter|
raise "#{self.class}: AnsibleValue.match?(hash)'s keys must be Symbols.." unless iv_symbol.is_a?Symbol
if val = instance_eval('@'+iv_symbol.to_s) then
#puts "match.val(#{iv_symbol}) == #{val}"
#puts "match.val(#{iv_symbol}) == #{val.inspect}"
result = result & case filter
# if the filter is a regular expression, use it to match the instance value
when Regexp then filter.match(val.to_s)
when Array then filter.include?(val)
# if the filter is an array, use set intersectionfrom_frame
when Array then (filter & val).length > 0
else filter == val
end
else
Expand All @@ -52,14 +58,14 @@ def matches?(hash)
@@AllValues = []

# lookup an AnsibleValue by a filter hash
# returns an array of matching values
def AnsibleValue.[](filter_hash)
#puts "AnsibleValue[] called, filter_hash=#{filter_hash}"
result_set = nil
result_set = []
@@AllValues.each { |v|
raise "ooops! @@AllValues contains a non-AnsibleValue!" unless v.is_a?(AnsibleValue)
if v.matches?(filter_hash) then
puts "Found a matching value! #{v}" if $DEBUG
result_set = Array.new unless result_set.is_a?Array
#puts "Found a matching value! #{v}" if $DEBUG
result_set << v
end
}
Expand All @@ -72,7 +78,7 @@ def AnsibleValue.[](filter_hash)
def AnsibleValue.insert(newvalue)
if @@AllValues.include?(newvalue) then
# newvalue is already stored in @@AllValues, find it and return it
return( @@AllValues.find{|val| val == newvalue} )
return( @@AllValues.find{|val| newvalue == val} )
else
puts "Adding a new value to @@AllValues (#{newvalue})" if $DEBUG
@@AllValues << newvalue
Expand All @@ -86,13 +92,14 @@ def AnsibleValue.insert(newvalue)
def update(newval)
unless @current_value == newval then
@last_update = Time.now
puts "==> updating value #{self}, with #{newval.inspect}"
puts "==> updating value #{self}, with #{newval.class}:#{newval.inspect}"
# previous value was different, update it and fire onUpdate handler
@previous_value = @current_value
@current_value = newval
# trigger onUpdate callback, if any
fire_callback(:onUpdate, @current_value)
end
return(@current_value)
end

end #module
99 changes: 53 additions & 46 deletions knx/knx_protocol.rb
Expand Up @@ -25,52 +25,59 @@
require 'rubygems'
require 'bit-struct'

class KNX_TP_ControlField < BitStruct
unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req"
unsigned :rep_flag, 1, "Repeat flag"
unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame"
unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)"
unsigned :unused1, 2, "two unused bits (should be 00)"
end

class KNX_L_DATA_Frame < BitStruct
# octet 0: TP1 control field
unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req"
unsigned :rep_flag, 1, "Repeat flag"
unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame"
unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)"
unsigned :unused1, 2, "two unused bits (should be 00)"
# octet 1+2: source
unsigned :src_addr, 16, "Source Address"
# octet 3+4: destination
unsigned :dst_addr, 16, "Destination Address"
# octet 5: control fields
unsigned :daf, 1, "Dest.Address flag 0=physical 1=group"
unsigned :ctrlfield, 3, "Network control field"
unsigned :datalength, 4, "Data length (bytes after octet #6)"
# octet 6 .. plus 2 bits from octet 7: TPCI+APCI
unsigned :tpci, 2, "TPCI control bits 8+7"
unsigned :seq, 4, "Packet sequence"
unsigned :apci, 4, "APCI control bits"
# octet 7 ... end
unsigned :apci_data, 6, "APCI/Data combined"
rest :data, "rest of frame"
end

#########################################################

APCICODES = "A_GroupValue_Read A_GroupValue_Response A_GroupValue_Write \
A_PhysicalAddress_Write A_PhysicalAddress_Read A_PhysicalAddress_Response \
A_ADC_Read A_ADC_Response A_Memory_Read A_Memory_Response A_Memory_Write \
A_UserMemory A_DeviceDescriptor_Read A_DeviceDescriptor_Response A_Restart \
A_OTHER".split()

TPDUCODES = "T_DATA_XXX_REQ T_DATA_CONNECTED_REQ T_DISCONNECT_REQ T_ACK".split()

PRIOCLASSES = "system alarm high low".split()

#########################################################

module Ansible

module KNX

class TP_ControlField < BitStruct
unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req"
unsigned :rep_flag, 1, "Repeat flag"
unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame"
unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)"
unsigned :unused1, 2, "two unused bits (should be 00)"
end

class L_DATA_Frame < BitStruct
# octet 0: TP1 control field
unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req"
unsigned :rep_flag, 1, "Repeat flag"
unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame"
unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)"
unsigned :unused1, 2, "two unused bits (should be 00)"
# octet 1+2: source
unsigned :src_addr, 16, "Source Address"
# octet 3+4: destination
unsigned :dst_addr, 16, "Destination Address"
# octet 5: control fields
unsigned :daf, 1, "Dest.Address flag 0=physical 1=group"
unsigned :ctrlfield, 3, "Network control field"
unsigned :datalength, 4, "Data length (bytes after octet #6)"
# octet 6 .. plus 2 bits from octet 7: TPCI+APCI
unsigned :tpci, 2, "TPCI control bits 8+7"
unsigned :seq, 4, "Packet sequence"
unsigned :apci, 4, "APCI control bits"
# octet 7 ... end
unsigned :apci_data, 6, "APCI/Data combined"
rest :data, "rest of frame"
end

#########################################################

APCICODES = "A_GroupValue_Read A_GroupValue_Response A_GroupValue_Write \
A_PhysicalAddress_Write A_PhysicalAddress_Read A_PhysicalAddress_Response \
A_ADC_Read A_ADC_Response A_Memory_Read A_Memory_Response A_Memory_Write \
A_UserMemory A_DeviceDescriptor_Read A_DeviceDescriptor_Response A_Restart \
A_OTHER".split()

TPDUCODES = "T_DATA_XXX_REQ T_DATA_CONNECTED_REQ T_DISCONNECT_REQ T_ACK".split()

PRIOCLASSES = "system alarm high low".split()

#########################################################

end #module KNX

end #module

#~ data = [188, 17, 200, 18, 1, 242, 0, 128, 80, 171] .pack("c*")
#~ knxpacket = KNX_L_DATA_Frame.new(data)
Expand Down
157 changes: 96 additions & 61 deletions knx/knx_transceiver.rb
@@ -1,5 +1,26 @@
#~ Project Ansible
#~ (c) 2011 Elias Karakoulakis <elias.karakoulakis@gmail.com>
=begin
Project Ansible - An extensible home automation scripting framework
----------------------------------------------------
Copyright (c) 2011 Elias Karakoulakis <elias.karakoulakis@gmail.com>
SOFTWARE NOTICE AND LICENSE
Project Ansible is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.
Project Ansible is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with Project Ansible. If not, see <http://www.gnu.org/licenses/>.
for more information on the LGPL, see:
http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
=end

require 'cgi'

Expand All @@ -25,78 +46,92 @@

module Ansible

class KNX_Transceiver < Transceiver
include AnsibleCallback

attr_reader :stomp
module KNX

def initialize(connURL)
begin
puts("KNX: init connection to #{connURL}")
@monitor_conn = EIBConnection.new()
@monitor_conn.EIBSocketURL(connURL)
@send_conn = EIBConnection.new()
@send_conn.EIBSocketURL(connURL)
@knxbuf = EIBBuffer.new()
super()
rescue Exception => e
puts "Initializing #{self}: " + e + "\n\t" + e.backtrace.join("\n\t")
class KNX_Transceiver < Transceiver
include AnsibleCallback

# a special exception to break the knx tranceiver loop
class NormalExit < Exception; end

attr_reader :stomp

def initialize(connURL)
begin
puts("KNX: init connection to #{connURL}")
@monitor_conn = EIBConnection.new()
@monitor_conn.EIBSocketURL(connURL)
@send_conn = EIBConnection.new()
@send_conn.EIBSocketURL(connURL)
@send_mutex = Mutex.new()
@knxbuf = EIBBuffer.new()
super()
rescue Exception => e
puts "#{self}.initialize() EXCEPTION: #{e}\n\t" + e.backtrace.join("\n\t")
end
end
end

# the main KNX transceiver thread
def run()
puts("KNX Transceiver thread is running!")
begin
#### part 1: connect to STOMP broker
@stomp = OnStomp.connect "stomp://localhost"
#### part 2: subscribe to command channel, listen for messages and pass them to KNX
# @stomp.subscribe KNX_COMMAND_TOPIC do |msg|
# dest = msg.headers['dest_addr'].to_i
# #TODO: check address limits
# apdu = Marshal.load(CGI.unescape(msg.body))
# send_apdu_raw(dest, apdu)
# end
##### part 3: monitor KNX bus, post all activity to /knx/monitor
@monitor_thread = Thread.new {

# the main KNX transceiver thread
def run()
puts("KNX Transceiver thread is running!")
@stomp = nil
begin
#### part 1: connect to STOMP broker
@stomp = OnStomp.connect "stomp://localhost"
#### part 2: subscribe to command channel, listen for messages and pass them to KNX
# @stomp.subscribe KNX_COMMAND_TOPIC do |msg|
# dest = msg.headers['dest_addr'].to_i
# #TODO: check address limits
# apdu = Marshal.load(CGI.unescape(msg.body))
# send_apdu_raw(dest, apdu)
# end
##### part 3: monitor KNX bus, post all activity to /knx/monitor
vbm = @monitor_conn.EIBOpenVBusmonitor()
loop do
src, dest ="", ""
len = @monitor_conn.EIBGetBusmonitorPacket(@knxbuf)
@monitor_conn.EIBGetGroup_Src(@buf, src, dest)
frame = KNX_L_DATA_Frame.new(@knxbuf.buffer.pack('c*'))
#puts "knxbuffer=="+@knxbuf.buffer.inspect
frame = L_DATA_Frame.new(@knxbuf.buffer.pack('c*'))
#puts "frame:\n\t"
headers = {}
frame.fields.each { |fld|
headers[fld.name] = CGI.escape(fld.inspect_in_object(frame, :default))
fldvalue = fld.inspect_in_object(frame, :default)
#puts "\t#{fld.name} == #{fldvalue}"
headers[fld.name] = CGI.escape(fldvalue)
}
message = "KNX transceiver: #{APCICODES[headers.apci]} packet from #{addr2str(frame.src_addr)} to #{addr2str(frame.dst_addr, frame.daf)}, priority:#{PRIOCLASSES[headers.prio]}"
@stomp.send(KNX_MONITOR_TOPIC, message, headers)
fire_callback(:onKNXactivity, frame)
@stomp.send(KNX_MONITOR_TOPIC, "KNX Activity", headers)
fire_callback(:onKNXtelegram, frame)
#
end
rescue NormalExit => e
puts("KNX transceiver terminating gracefully...")
rescue Exception => e
puts("Exception in KNX server thread: #{e}")
puts("backtrace:\n " << e.backtrace.join("\n "))
retry
ensure
@monitor_conn.EIBClose() if @monitor_conn
@stomp.disconnect if @stomp
end
end #def run()

def send_apdu_raw(dest, apdu)
@send_mutex.synchronize {
puts("KNX transceiver: sending to group address #{dest}, #{apdu.inspect}")
if (@send_conn.EIBOpenT_Group(dest, 1) == -1) then
raise("KNX client: error setting socket mode")
end
@send_conn.EIBSendAPDU(apdu)
@send_conn.EIBReset()
}
@monitor_thread.join
rescue NormalExit => e
puts("KNX transceiver terminating gracefully...")
rescue Exception => e
puts("Exception in KNX server thread: #{e}")
puts("backtrace:\n " << e.backtrace.join("\n "))
ensure
@monitor_conn.EIBClose()
@stomp.disconnect
end
end #def Thread.run()

def send_apdu_raw(dest, apdu)
puts("KNX transceiver: sending to group address #{dest}, #{apdu.inspect}")
if (@send_conn.EIBOpenT_Group(dest, 1) == -1) then
raise("KNX client: error setting socket mode")
end
@send_conn.EIBSendAPDU(apdu)
@send_conn.EIBReset()
end

end #class

end #class

end #module
end #module KNX

end #module Ansible

#KNX = Ansible::KNX_Transceiver.new("ip:192.168.0.10")
#KNX.thread.join

0 comments on commit d545344

Please sign in to comment.