xap_ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +59 -0
- data/Rakefile +3 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/xap.rb +30 -0
- data/lib/xap/parser.rb +6 -0
- data/lib/xap/parser/parse_xap.rb +47 -0
- data/lib/xap/parser/xap.treetop +47 -0
- data/lib/xap/parser/xap_nodes.rb +120 -0
- data/lib/xap/schema.rb +7 -0
- data/lib/xap/schema/xap_bsc.rb +416 -0
- data/lib/xap/schema/xap_bsc_device.rb +384 -0
- data/lib/xap/xap_address.rb +169 -0
- data/lib/xap/xap_dev.rb +76 -0
- data/lib/xap/xap_handler.rb +197 -0
- data/lib/xap/xap_msg.rb +194 -0
- data/lib/xap_ruby.rb +6 -0
- data/lib/xap_ruby/version.rb +3 -0
- data/xap_ruby.gemspec +43 -0
- metadata +163 -0
data/lib/xap/xap_dev.rb
ADDED
@@ -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
|
data/lib/xap/xap_msg.rb
ADDED
@@ -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
|