xap_ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ # Base class for object model of an xAP device.
2
+ # (C)2012 Mike Bourgeous
3
+
4
+ module Xap
5
+ # Classes that want to receive filtered xAP events from the main event loop
6
+ # should inherit from this class and override receive_message.
7
+ class XapDevice
8
+ attr_accessor :address, :uid, :interval
9
+
10
+ # Initializes this superclass with the given address and base UID,
11
+ # which will be used as the source address and UID of messages
12
+ # generated by this device, and the given heartbeat interval.
13
+ #
14
+ # address - a non-wildcarded XapAddress that doesn't contain ':'.
15
+ # uid - eight hexadecimal digits, the first two of which must be FF,
16
+ # the last two 00, and the middle two neither FF nor 00.
17
+ # interval - the number of seconds between xAP heartbeats sent by this
18
+ # device model when added to an XapHandler event loop.
19
+ def initialize address, uid, interval
20
+ raise 'Heartbeat interval must be a Fixnum or nil' if interval && !interval.is_a?(Fixnum)
21
+ set_address address
22
+ self.uid = uid
23
+ @interval = interval
24
+ end
25
+
26
+ # Sets the XapHandler that manages messages to and from this device.
27
+ # This should typically be called by XapHandler itself. Subclasses may
28
+ # override this method in order to send messages that are supposed to
29
+ # be sent on initialization, so long as they call this base
30
+ # implementation first. XapHandler will call this method with nil if
31
+ # the device is removed from the handler or the xAP socket is closed.
32
+ def handler= handler
33
+ raise 'handler must be a XapHandler' unless handler.nil? || handler.is_a?(XapHandler)
34
+ @handler = handler
35
+ end
36
+
37
+ # Called whenever a matching message is received by the associated
38
+ # handler.
39
+ def receive_message msg
40
+ puts "XXX: You forgot to override receive_message in #{self}: #{msg.inspect.lines.to_a.join("\t")}"
41
+ end
42
+
43
+ # Returns a string description of this device.
44
+ def to_s
45
+ "<#{self.class.name}: #{@address} #{@uid}>"
46
+ end
47
+
48
+ protected
49
+ # Uses the associated XapHandler to send the given message. Does
50
+ # nothing if there is no handler set.
51
+ def send_message message
52
+ @handler.send_message message if @handler
53
+ end
54
+
55
+ # TODO: Ability to request incoming messages matching a particular
56
+ # source address, using add_receiver/remove_receiver
57
+
58
+ # Changes the address used by this device. Subclasses should call this
59
+ # if, for example, the user-assigned name of the device is changed.
60
+ def set_address address
61
+ if !address.is_a?(XapAddress) || address.wildcard? || address.endpoint
62
+ raise 'address must be a non-wildcarded XapAddress without ":"'
63
+ end
64
+ @address = address
65
+ end
66
+
67
+ # Changes the UID used by this device. Subclasses should call this if
68
+ # the four-digit reassignable component of the UID is changed.
69
+ def uid= uid
70
+ unless uid =~ /^FF[0-9A-Z]{4}00$/i && !(uid.slice(2, 4) =~ /(?:^(?:00|FF)|(?:00|FF)$)/i)
71
+ raise "uid must be eight hex digits of the form FF(01..FE)(01..FE)00, not #{uid}."
72
+ end
73
+ @uid = uid
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,197 @@
1
+ # EventMachine packet transmission and receipt handler for the xAP protocol.
2
+ # (C)2012 Mike Bourgeous
3
+
4
+ module Xap
5
+ XAP_PORT=3639
6
+ BCAST_ADDR='255.255.255.255'
7
+
8
+ class XapHandler < EM::Connection
9
+ @@instance = nil
10
+
11
+ def self.instance
12
+ @@instance
13
+ end
14
+
15
+ def initialize servername
16
+ @@instance = self
17
+ @servername = servername
18
+ @devices = []
19
+ @receivers = []
20
+ @timers = {}
21
+ end
22
+
23
+ def unbind
24
+ @@instance = nil if @@instance == self
25
+
26
+ @devices.each do |d|
27
+ d.handler = nil
28
+ end
29
+ end
30
+
31
+ def receive_data d
32
+ handled = false
33
+ begin
34
+ msg = XapMessage.parse(d)
35
+ rescue Exception => e
36
+ Xap.log "Error parsing incoming message: #{e}\n\t#{e.backtrace.join("\n\t")}"
37
+ Xap.log "receive_data(#{d.length}) invalid: #{d.inspect}"
38
+ return
39
+ end
40
+
41
+ if msg.target_addr
42
+ @devices.each do |d|
43
+ begin
44
+ if msg.target_addr.base =~ d.address
45
+ d.receive_message msg
46
+ handled = true
47
+ end
48
+ rescue RuntimeError => e
49
+ Xap.log "Error processing message with device #{d}: #{e}\n\t#{e.backtrace.join("\n\t")}"
50
+ end
51
+ end
52
+ end
53
+
54
+ @receivers.each do |rcv|
55
+ if rcv[0] =~ msg.src_addr
56
+ rcv[1].call msg
57
+ end
58
+ end
59
+
60
+ if !handled && $DEBUG
61
+ Xap.log "Received a #{msg.class.name} message (#{msg.src_addr.inspect} => #{msg.target_addr.inspect})"
62
+ end
63
+ end
64
+
65
+ # Adds a device object to the list of devices. The device will be
66
+ # notified about incoming messages with a target matching the device's
67
+ # address. If the device's heartbeat interval is non-nil and greater
68
+ # than 0 then a periodic heartbeat will automatically be transmitted
69
+ # for the device,
70
+ def add_device device
71
+ raise 'device must be an XapDevice' unless device.is_a? XapDevice
72
+ raise 'device is already in this XapHandler' if @devices.include? device
73
+
74
+ @devices << device
75
+ if device.interval && device.interval > 0
76
+ @timers[device] = EM.add_periodic_timer(device.interval) {
77
+ send_heartbeat device.address, device.uid, device.interval
78
+ }
79
+ end
80
+
81
+ device.handler = self
82
+ end
83
+
84
+ # Removes the given device from message notifications. Cancels its
85
+ # heartbeat timer if it has one.
86
+ def remove_device device
87
+ @devices.delete device
88
+ timer = @timers.delete device
89
+ timer.cancel if timer
90
+ device.handler = nil
91
+ end
92
+
93
+ # Adds a callback to be called with the XapMessage when a message is
94
+ # received from the given source address (src_addr may be wildcarded).
95
+ def add_receiver src_addr, callback
96
+ raise 'src_addr must be an XapAddress' unless src_addr.is_a? XapAddress
97
+ raise 'callback must be callable' unless callback.respond_to? :call
98
+ @receivers << [src_addr, callback]
99
+ self
100
+ end
101
+
102
+ # Removes an address/callback pair from the list of callbacks called
103
+ # when a matching message is received.
104
+ def remove_receiver src_addr, callback
105
+ @receivers.delete_if { |rcv|
106
+ rcv[0] == src_addr && rcv[1] == callback
107
+ }
108
+ nil
109
+ end
110
+
111
+ # Sends an XapMessage to the network.
112
+ def send_message message
113
+ raise 'message must be an XapMessage' unless message.is_a? XapMessage
114
+ send_datagram(message.to_s, BCAST_ADDR, XAP_PORT)
115
+ end
116
+
117
+ # Broadcasts an xAP heartbeat from the given address and UID.
118
+ #
119
+ # src_addr and src_uid should be convertible to the exact strings that
120
+ # should go into the packet. interval is how often other devices
121
+ # should expect the heartbeat, in seconds.
122
+ #
123
+ # http://www.xapautomation.org/index.php?title=Protocol_definition#Device_Monitoring_-_Heartbeats
124
+ #
125
+ # The resulting packet will look like this:
126
+ # xap-hbeat
127
+ # {
128
+ # v=12
129
+ # hop=1
130
+ # uid=[src_uid]
131
+ # class=xap-hbeat.alive
132
+ # source=[src_addr]
133
+ # interval=[interval]
134
+ # }
135
+ def send_heartbeat src_addr, src_uid, interval = 60
136
+ msg = "xap-hbeat\n" +
137
+ "{\n" +
138
+ "v=12\n" +
139
+ "hop=1\n" +
140
+ "uid=#{src_uid}\n" +
141
+ "class=xap-hbeat.alive\n" +
142
+ "source=#{src_addr}\n" +
143
+ "interval=#{interval}\n" +
144
+ "}\n"
145
+
146
+ send_datagram(msg, BCAST_ADDR, XAP_PORT)
147
+ end
148
+ end
149
+
150
+ @@connection = nil
151
+
152
+ # Opens a UDP socket for sending and receiving xAP messages. The
153
+ # EventMachine event loop must be running.
154
+ def self.start_xap
155
+ # EventMachine doesn't seem to support using '::' for IP address
156
+ @@connection ||= EM.open_datagram_socket '0.0.0.0', XAP_PORT, XapHandler, "xAP Server" unless @@connection
157
+ @@connection
158
+
159
+ # TODO: xAP hub support
160
+ end
161
+
162
+ # Closes the xAP server UDP socket, if one exists.
163
+ def self.stop_xap
164
+ @@connection.close_connection_after_writing if @@connection
165
+ @@connection = nil
166
+ end
167
+
168
+ # Returns true if the xAP handler is connected to its UDP socket, false
169
+ # otherwise.
170
+ def self.xap_running?
171
+ !!XapHandler.instance
172
+ end
173
+
174
+ # Adds the given XapDevice to the current xAP socket server.
175
+ def self.add_device device
176
+ raise 'The xAP server is not running. Call start_xap first.' unless @@connection
177
+ XapHandler.instance.add_device device
178
+ end
179
+
180
+ # Removes the given XapDevice from the current xAP socket server.
181
+ def self.remove_device device
182
+ raise 'The xAP server is not running. Call start_xap first.' unless @@connection
183
+ XapHandler.instance.remove_device device
184
+ end
185
+
186
+ # Adds a message receiver to the current xAP socket server.
187
+ def self.add_receiver src_addr, callback
188
+ raise 'The xAP server is not running. Call start_xap first.' unless @@connection
189
+ XapHandler.instance.add_receiver src_addr, callback
190
+ end
191
+
192
+ # Removes a message receiver from the current xAP socket server.
193
+ def self.remove_receiver src_addr, callback
194
+ raise 'The xAP server is not running. Call start_xap first.' unless @@connection
195
+ XapHandler.instance.remove_receiver src_addr, callback
196
+ end
197
+ end
@@ -0,0 +1,194 @@
1
+ # xAP message base class definitions
2
+ # (C)2012 Mike Bourgeous
3
+
4
+ module Xap
5
+ # Base class for all xAP message types. Registered subclasses must implement
6
+ # a parse method that accepts the message hash as its first parameter.
7
+ class XapMessage
8
+ @@msgtypes = {}
9
+
10
+ attr_accessor :src_addr, :target_addr, :version, :hop, :uid, :msgclass, :headername
11
+
12
+ # Parses the given data as an xAP message. If the message type does
13
+ # not have a registered handler, an exception will be raised
14
+ def self.parse data
15
+ raise 'data must be (convertible to) a String' unless data.respond_to? :to_s
16
+
17
+ msghash = Xap::Parser::ParseXap.simple_parse data.to_s
18
+
19
+ headername = msghash.keys[0].downcase
20
+ raise "No handlers defined for #{headername} message headers." unless @@msgtypes[headername]
21
+
22
+ classname = msghash[headername]['class']
23
+ raise 'Message lacks a class field in its header.' unless classname || @@msgtypes[headername][nil]
24
+ classname.downcase!
25
+
26
+ handler = @@msgtypes[headername][classname] || @@msgtypes[headername][nil]
27
+ raise "No handler defined for #{headername}/#{classname} messages." unless handler
28
+
29
+ handler.parse msghash
30
+ end
31
+
32
+ # Registers the given klass as the handler for msgclass messages, with
33
+ # the header block called headername ('xap-header' by default).
34
+ #
35
+ # Specify nil for msgclass to register a fallback handler for messages
36
+ # with the given header name that don't have a specific class handler
37
+ # registered.
38
+ def self.register_class klass, msgclass, headername='xap-header'
39
+ unless klass.is_a?(Class) && klass < XapMessage
40
+ raise "klass must be a Class that inherits XapMessage, not #{klass.inspect}"
41
+ end
42
+ raise 'msgclass must be nil or a String' unless msgclass.nil? || msgclass.is_a?(String)
43
+
44
+ Xap.log "Registered support for #{headername}/#{msgclass} messages via #{klass.name}"
45
+
46
+ # TODO: Support regex for msgclass?
47
+ headername.downcase!
48
+ msgclass.downcase! if msgclass.is_a? String
49
+ @@msgtypes[headername] = @@msgtypes[headername] || {}
50
+ @@msgtypes[headername][msgclass] = klass
51
+ end
52
+
53
+ # msgclass - the message's class
54
+ # src_addr - the message's source address
55
+ # src_uid - the message's source UID (TODO: merge with XapAddress?)
56
+ # target_addr - the message's target address, or nil for no target
57
+ def initialize msgclass, src_addr, src_uid, target_addr = nil
58
+ raise 'Do not instantiate XapMessage directly (use a subclass)' if self.class == XapMessage
59
+ raise 'src_addr must be an XapAddress' unless src_addr.is_a? XapAddress
60
+ raise 'target_addr must be nil or an XapAddress' unless target_addr.nil? || target_addr.is_a?(XapAddress)
61
+
62
+ @src_addr = src_addr
63
+ @target_addr = target_addr
64
+ @version = 12
65
+ @hop = 1
66
+ @uid = src_uid
67
+ @msgclass = msgclass
68
+ @blocks = {}
69
+ @headers = {}
70
+ end
71
+
72
+ # Returns a string representation of the message suitable for
73
+ # transmission on the network.
74
+ def to_s
75
+ s = "#{headername}\n" +
76
+ "{\n" +
77
+ "v=#{@version}\n" +
78
+ "hop=#{@hop}\n" +
79
+ "uid=#{@uid}\n" +
80
+ "class=#{@msgclass}\n" +
81
+ "source=#{@src_addr}\n"
82
+
83
+ s << "target=#{@target_addr}\n" if @target_addr
84
+
85
+ if @headers
86
+ @headers.each do |k, v|
87
+ s << "#{k}=#{v}\n"
88
+ end
89
+ end
90
+
91
+ s << "}\n"
92
+
93
+ if @blocks
94
+ @blocks.each do |name, block|
95
+ s << "#{name}\n{\n"
96
+ block.each do |k, v|
97
+ s << "#{k}=#{v}\n"
98
+ end
99
+ s << "}\n"
100
+ end
101
+ end
102
+
103
+ s
104
+ end
105
+
106
+ # Returns the last two digits of the UID
107
+ def uid_endpoint
108
+ @uid[-2, 2]
109
+ end
110
+
111
+ # Parses standard xAP header information from the given header hash,
112
+ # such as protocol version, hop count, unique ID, message class,
113
+ # message source, and message target.
114
+ #
115
+ # Example usage: parse_header(message_hash['xap-header'])
116
+ protected
117
+ def parse_header header
118
+ # TODO: Mixed-case header field names?
119
+ @src_addr = XapAddress.parse header['source']
120
+ @target_addr = XapAddress.parse header['target']
121
+ @version = header['v']
122
+ @hop = header['hop']
123
+ @uid = header['uid'] # TODO: Parse/validate UID/add UID class/merge with Address?
124
+ @msgclass = header['class']
125
+ end
126
+
127
+ # Adds a custom field to the message header.
128
+ def add_header key, value
129
+ @headers ||= {}
130
+ @headers[key] = value
131
+ end
132
+
133
+ # Adds the given hash as a block under the given name (TODO: hexadecimal fields)
134
+ # FIXME: multiple blocks can have the same name in xAP according to
135
+ # http://www.xapautomation.org/index.php?title=Protocol_definition#Message_Grammar
136
+ # so the blocks hash and to_hash in the parser need to be replaced with
137
+ # arrays.
138
+ def add_block name, hash
139
+ @blocks ||= {}
140
+ @blocks[name] = hash
141
+ end
142
+
143
+ # Sets the block list to the given hash
144
+ def set_blocks hash
145
+ @blocks = hash
146
+ end
147
+ end
148
+
149
+ # A fallback class (or inheritable utility class) for messages not supported by
150
+ # a loaded schema.
151
+ class XapUnsupportedMessage < XapMessage
152
+ XapMessage.register_class self, nil
153
+
154
+ def self.parse hash
155
+ self.new hash, nil, nil
156
+ end
157
+
158
+ def initialize msgclass, src_addr, src_uid, target_addr = nil
159
+ @headername ||= 'xap-header'
160
+ if msgclass.is_a?(Hash)
161
+ parse_header msgclass[@headername]
162
+
163
+ blocks = msgclass.clone
164
+ blocks.delete @headername
165
+ set_blocks blocks
166
+ else
167
+ super msgclass, src_addr, src_uid, target_addr
168
+ end
169
+ end
170
+ end
171
+
172
+ # An xAP heartbeat message.
173
+ class XapHeartbeat < XapMessage
174
+ XapMessage.register_class self, 'xap-hbeat.alive', 'xap-hbeat'
175
+
176
+ attr_accessor :interval
177
+
178
+ def self.parse hash
179
+ self.new hash, nil
180
+ end
181
+
182
+ def initialize src_addr, src_uid, interval = 60
183
+ @headername = 'xap-hbeat'
184
+ if src_addr.is_a?(Hash)
185
+ parse_header src_addr[@headername]
186
+ interval = src_addr[@headername]['interval'] || interval
187
+ else
188
+ super 'xap-hbeat.alive', src_addr, src_uid
189
+ end
190
+ add_header 'interval', interval
191
+ @interval = interval
192
+ end
193
+ end
194
+ end