xap_ruby 0.1.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.
- 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
|