asterism 0.0.0 → 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 +4 -4
- data/LICENSE +21 -0
- data/NOTICE +27 -0
- data/README.md +405 -3
- data/data/msgs/LICENSE-Apache-2.0.txt +202 -0
- data/data/msgs/NOTICE +17 -0
- data/data/msgs/builtin_interfaces/msg/Duration.rb +33 -0
- data/data/msgs/builtin_interfaces/msg/Time.rb +33 -0
- data/data/msgs/example_interfaces/srv/AddTwoInts.rb +64 -0
- data/data/msgs/geometry_msgs/msg/Accel.rb +35 -0
- data/data/msgs/geometry_msgs/msg/AccelStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Point.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Point32.rb +36 -0
- data/data/msgs/geometry_msgs/msg/PointStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Pose.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Pose2D.rb +36 -0
- data/data/msgs/geometry_msgs/msg/PoseStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Quaternion.rb +39 -0
- data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Transform.rb +36 -0
- data/data/msgs/geometry_msgs/msg/TransformStamped.rb +39 -0
- data/data/msgs/geometry_msgs/msg/Twist.rb +35 -0
- data/data/msgs/geometry_msgs/msg/TwistStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Vector3.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Wrench.rb +35 -0
- data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +36 -0
- data/data/msgs/sensor_msgs/msg/BatteryState.rb +100 -0
- data/data/msgs/sensor_msgs/msg/FluidPressure.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Illuminance.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Imu.rb +52 -0
- data/data/msgs/sensor_msgs/msg/JointState.rb +44 -0
- data/data/msgs/sensor_msgs/msg/MagneticField.rb +39 -0
- data/data/msgs/sensor_msgs/msg/NavSatFix.rb +55 -0
- data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +43 -0
- data/data/msgs/sensor_msgs/msg/Range.rb +52 -0
- data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Temperature.rb +38 -0
- data/data/msgs/std_msgs/msg/Bool.rb +30 -0
- data/data/msgs/std_msgs/msg/Byte.rb +30 -0
- data/data/msgs/std_msgs/msg/ByteMultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Char.rb +30 -0
- data/data/msgs/std_msgs/msg/ColorRGBA.rb +39 -0
- data/data/msgs/std_msgs/msg/Empty.rb +26 -0
- data/data/msgs/std_msgs/msg/Float32.rb +30 -0
- data/data/msgs/std_msgs/msg/Float32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Float64.rb +30 -0
- data/data/msgs/std_msgs/msg/Float64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Header.rb +35 -0
- data/data/msgs/std_msgs/msg/Int16.rb +30 -0
- data/data/msgs/std_msgs/msg/Int16MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int32.rb +30 -0
- data/data/msgs/std_msgs/msg/Int32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int64.rb +30 -0
- data/data/msgs/std_msgs/msg/Int64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int8.rb +30 -0
- data/data/msgs/std_msgs/msg/Int8MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +36 -0
- data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +35 -0
- data/data/msgs/std_msgs/msg/String.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt16.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt32.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt64.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt8.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +35 -0
- data/lib/asterism/cruby.rb +24 -0
- data/lib/asterism/version.rb +5 -0
- data/lib/asterism.rb +17 -4
- data/mrblib/asterism.rb +545 -0
- data/mrblib/cdr.rb +512 -0
- data/mrblib/future.rb +83 -0
- data/mrblib/proxy.rb +97 -0
- data/mrblib/ros.rb +691 -0
- data/tools/asterism_msggen.rb +826 -0
- metadata +118 -9
data/mrblib/asterism.rb
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# Asterism: Ruby objects on other machines, called like local ones.
|
|
2
|
+
#
|
|
3
|
+
# Asterism.connect("tcp/192.0.2.2:7447", node: "fmruby-bbbbbb", app: "demo")
|
|
4
|
+
# Asterism.expose("apu", apu, methods: [:play, :stop])
|
|
5
|
+
# p = Asterism["linux/demo/apu"] # <node>/<app>/<object>
|
|
6
|
+
# p.play("cde") # waits for the answer (2 s by default)
|
|
7
|
+
# Asterism.poll # call regularly (answers incoming calls)
|
|
8
|
+
#
|
|
9
|
+
# Keys (doc/ruby_asterism/design.md ch. 4):
|
|
10
|
+
# asterism/<node>/<app>/<object>/call get: payload [method, args, kwargs]
|
|
11
|
+
# reply ["ok", value] or
|
|
12
|
+
# ["error", class name, message]
|
|
13
|
+
# asterism/<node>/<app>/<object>/meta get: reply {"methods" => [[name, arity], ...]}
|
|
14
|
+
# asterism/<node> liveliness: the node is up
|
|
15
|
+
# asterism/<node>/<app>/<object> liveliness: the object is exposed
|
|
16
|
+
#
|
|
17
|
+
# Everything is polled: nothing runs behind the application's back. A call
|
|
18
|
+
# that waits for its answer keeps polling meanwhile and answers the calls
|
|
19
|
+
# that come in, so two machines calling each other do not lock up.
|
|
20
|
+
module Asterism
|
|
21
|
+
ROOT = "asterism"
|
|
22
|
+
DEFAULT_TIMEOUT_MS = 2000
|
|
23
|
+
# Waits that may be open inside each other (a call answered while waiting
|
|
24
|
+
# that itself calls and waits, and so on).
|
|
25
|
+
MAX_NESTING = 4
|
|
26
|
+
# Pause between polls while a call waits (ms).
|
|
27
|
+
WAIT_STEP_MS = 2
|
|
28
|
+
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
|
|
31
|
+
# A value that MessagePack cannot carry (only nil, true, false, Integer,
|
|
32
|
+
# Float, String, Symbol (sent as a String), Array and Hash can be sent).
|
|
33
|
+
class EncodeError < Error; end
|
|
34
|
+
|
|
35
|
+
# No answer within the time limit (the object is gone, or slow).
|
|
36
|
+
class Timeout < Error; end
|
|
37
|
+
|
|
38
|
+
# The connection is closed or was lost (Asterism::Zenoh::Error inside).
|
|
39
|
+
class Disconnected < Error; end
|
|
40
|
+
|
|
41
|
+
# The remote method raised, or could not be called (not exposed, wrong
|
|
42
|
+
# number of arguments). remote_class is the class name on the other side.
|
|
43
|
+
class RemoteError < Error
|
|
44
|
+
attr_reader :remote_class, :remote_message
|
|
45
|
+
|
|
46
|
+
def initialize(remote_class, remote_message)
|
|
47
|
+
@remote_class = remote_class.to_s
|
|
48
|
+
@remote_message = remote_message.to_s
|
|
49
|
+
super("#{@remote_class}: #{@remote_message}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------ values
|
|
54
|
+
|
|
55
|
+
module Codec
|
|
56
|
+
MAX_DEPTH = 16
|
|
57
|
+
|
|
58
|
+
# The value as it will be sent (Symbols become Strings), or EncodeError.
|
|
59
|
+
def self.check(v, depth = 0)
|
|
60
|
+
raise ::Asterism::EncodeError, "value nested too deep (max #{MAX_DEPTH})" if depth > MAX_DEPTH
|
|
61
|
+
if v.nil? || v == true || v == false || v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(String)
|
|
62
|
+
v
|
|
63
|
+
elsif v.is_a?(Symbol)
|
|
64
|
+
v.to_s
|
|
65
|
+
elsif v.is_a?(Array)
|
|
66
|
+
v.map { |e| check(e, depth + 1) }
|
|
67
|
+
elsif v.is_a?(Hash)
|
|
68
|
+
h = {}
|
|
69
|
+
v.each { |k, x| h[check(k, depth + 1)] = check(x, depth + 1) }
|
|
70
|
+
h
|
|
71
|
+
else
|
|
72
|
+
raise ::Asterism::EncodeError, "cannot send a #{v.class} (only nil, true, false, Integer, " \
|
|
73
|
+
"Float, String, Symbol, Array, Hash)"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.pack(v)
|
|
78
|
+
::MessagePack.pack(check(v))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# nil when the bytes are not MessagePack.
|
|
82
|
+
def self.unpack(bytes)
|
|
83
|
+
return nil if bytes.nil? || bytes.empty?
|
|
84
|
+
::MessagePack.unpack(bytes)
|
|
85
|
+
rescue
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------ clock
|
|
91
|
+
|
|
92
|
+
# Milliseconds and a short pause. PicoRuby's Machine when it is there,
|
|
93
|
+
# otherwise Time and sleep.
|
|
94
|
+
def self.now_ms
|
|
95
|
+
if machine?
|
|
96
|
+
::Machine.board_millis
|
|
97
|
+
else
|
|
98
|
+
(Time.now.to_f * 1000).to_i
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.pause(ms)
|
|
103
|
+
if machine?
|
|
104
|
+
::Machine.delay_ms(ms)
|
|
105
|
+
else
|
|
106
|
+
sleep(ms / 1000.0)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.machine?
|
|
111
|
+
@machine = (Object.const_defined?(:Machine) && ::Machine.respond_to?(:delay_ms)) if @machine.nil?
|
|
112
|
+
@machine
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------ names
|
|
116
|
+
|
|
117
|
+
# A node, app or object name: one key chunk, no wildcard or special
|
|
118
|
+
# characters.
|
|
119
|
+
def self.check_name(what, name)
|
|
120
|
+
s = name.to_s
|
|
121
|
+
bad = s.empty? || s.start_with?("@")
|
|
122
|
+
["/", "*", "$", "?", "#"].each { |c| bad = true if s.include?(c) }
|
|
123
|
+
raise ArgumentError, "invalid #{what} name: #{name.inspect}" if bad
|
|
124
|
+
s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# "node/app/object" -> [node, app, object]
|
|
128
|
+
def self.split_path(path)
|
|
129
|
+
parts = path.to_s.split("/")
|
|
130
|
+
raise ArgumentError, "expected <node>/<app>/<object>, got #{path.inspect}" unless parts.size == 3
|
|
131
|
+
check_name("node", parts[0])
|
|
132
|
+
check_name("app", parts[1])
|
|
133
|
+
check_name("object", parts[2])
|
|
134
|
+
parts
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Glob over "/"-separated chunks: * is one chunk, ** any number of them.
|
|
138
|
+
def self.glob?(pattern, path)
|
|
139
|
+
glob_parts?(pattern.split("/"), 0, path.split("/"), 0)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.glob_parts?(pat, i, parts, j)
|
|
143
|
+
while i < pat.size
|
|
144
|
+
if pat[i] == "**"
|
|
145
|
+
k = j
|
|
146
|
+
while k <= parts.size
|
|
147
|
+
return true if glob_parts?(pat, i + 1, parts, k)
|
|
148
|
+
k += 1
|
|
149
|
+
end
|
|
150
|
+
return false
|
|
151
|
+
end
|
|
152
|
+
return false if j >= parts.size
|
|
153
|
+
return false unless pat[i] == "*" || pat[i] == parts[j]
|
|
154
|
+
i += 1
|
|
155
|
+
j += 1
|
|
156
|
+
end
|
|
157
|
+
j == parts.size
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------ connection
|
|
161
|
+
|
|
162
|
+
# Opens the connection. locator, mode: and listen: go to
|
|
163
|
+
# Asterism::Zenoh::Session.open (client of a router by default; mode: :peer
|
|
164
|
+
# with or without listen: for no router). node: is this machine's ID and
|
|
165
|
+
# app: this application's name. Raises Disconnected when it cannot connect,
|
|
166
|
+
# and Error when the same <node>/<app> is already on the network.
|
|
167
|
+
def self.connect(locator, node:, app:, mode: nil, listen: nil)
|
|
168
|
+
raise Error, "already connected (Asterism.close first)" if connected?
|
|
169
|
+
close
|
|
170
|
+
node = check_name("node", node)
|
|
171
|
+
app = check_name("app", app)
|
|
172
|
+
begin
|
|
173
|
+
s = Asterism::Zenoh::Session.open(locator, mode: mode, listen: listen)
|
|
174
|
+
rescue Asterism::Zenoh::Error => e
|
|
175
|
+
raise Disconnected, e.message
|
|
176
|
+
end
|
|
177
|
+
begin
|
|
178
|
+
if already_there?(s, node, app)
|
|
179
|
+
s.close
|
|
180
|
+
raise Error, "#{node}/#{app} is already on the network"
|
|
181
|
+
end
|
|
182
|
+
@node_token = s.liveliness("#{ROOT}/#{node}")
|
|
183
|
+
@queryable = s.queryable("#{ROOT}/#{node}/#{app}/**", 32)
|
|
184
|
+
@watch = s.liveliness_watch("#{ROOT}/**", 64)
|
|
185
|
+
rescue Asterism::Zenoh::Error => e
|
|
186
|
+
s.close
|
|
187
|
+
raise Disconnected, e.message
|
|
188
|
+
end
|
|
189
|
+
@session = s
|
|
190
|
+
@node = node
|
|
191
|
+
@app = app
|
|
192
|
+
@objects = {} # name => [object, {method name => arity}, token]
|
|
193
|
+
@alive = {} # "node/app/object" => true (from liveliness)
|
|
194
|
+
@nodes = {} # node => true (from liveliness)
|
|
195
|
+
@proxies = {}
|
|
196
|
+
@depth = 0
|
|
197
|
+
@lost = nil
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Whether objects of node/app are alive already (a second copy of the
|
|
202
|
+
# same application). Waits for the answer (a router answers at once).
|
|
203
|
+
def self.already_there?(s, node, app)
|
|
204
|
+
g = s.liveliness_get("#{ROOT}/#{node}/#{app}/**", 1000)
|
|
205
|
+
t0 = now_ms
|
|
206
|
+
found = false
|
|
207
|
+
while true
|
|
208
|
+
s.poll
|
|
209
|
+
found = true if g.each_reply.size > 0
|
|
210
|
+
break if found || g.done? || now_ms - t0 > 1500
|
|
211
|
+
pause(WAIT_STEP_MS)
|
|
212
|
+
end
|
|
213
|
+
found
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.connected?
|
|
217
|
+
return false if @session.nil?
|
|
218
|
+
if @session.closed?
|
|
219
|
+
lost("the connection was lost")
|
|
220
|
+
return false
|
|
221
|
+
end
|
|
222
|
+
true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.node_id
|
|
226
|
+
@node
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def self.app
|
|
230
|
+
@app
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Why the connection went away (nil while connected or after close).
|
|
234
|
+
def self.lost_reason
|
|
235
|
+
@lost
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Closes the connection (and with it every exposed object). Idempotent.
|
|
239
|
+
def self.close
|
|
240
|
+
s = @session
|
|
241
|
+
@session = nil
|
|
242
|
+
@objects = {}
|
|
243
|
+
@alive = {}
|
|
244
|
+
@nodes = {}
|
|
245
|
+
@proxies = {}
|
|
246
|
+
s.close if s
|
|
247
|
+
@node_token = nil
|
|
248
|
+
@queryable = nil
|
|
249
|
+
@watch = nil
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def self.session!
|
|
254
|
+
raise Disconnected, (@lost || "not connected") unless @session
|
|
255
|
+
@session
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.lost(reason)
|
|
259
|
+
@lost = reason
|
|
260
|
+
close
|
|
261
|
+
@lost = reason
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# ------------------------------------------------------------ exposing
|
|
265
|
+
|
|
266
|
+
# Makes obj callable from other machines as <node>/<app>/<name>. Only the
|
|
267
|
+
# listed methods can be called. methods: is an Array of names, or a Hash
|
|
268
|
+
# name => number of arguments (checked before calling; -1 for any).
|
|
269
|
+
def self.expose(name, obj, methods:)
|
|
270
|
+
s = session!
|
|
271
|
+
name = check_name("object", name)
|
|
272
|
+
table = {}
|
|
273
|
+
if methods.is_a?(Hash)
|
|
274
|
+
methods.each { |m, n| table[m.to_s] = n.to_i }
|
|
275
|
+
else
|
|
276
|
+
methods.each { |m| table[m.to_s] = -1 }
|
|
277
|
+
end
|
|
278
|
+
raise ArgumentError, "no method to expose" if table.empty?
|
|
279
|
+
table.each_key do |m|
|
|
280
|
+
raise ArgumentError, "#{obj.class} has no public method #{m}" unless obj.respond_to?(m)
|
|
281
|
+
end
|
|
282
|
+
unexpose(name)
|
|
283
|
+
token = s.liveliness("#{ROOT}/#{@node}/#{@app}/#{name}")
|
|
284
|
+
@objects[name] = [obj, table, token]
|
|
285
|
+
"#{@node}/#{@app}/#{name}"
|
|
286
|
+
rescue Asterism::Zenoh::Error => e
|
|
287
|
+
raise Disconnected, e.message
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def self.unexpose(name)
|
|
291
|
+
entry = @objects ? @objects.delete(name.to_s) : nil
|
|
292
|
+
entry[2].close if entry
|
|
293
|
+
!entry.nil?
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def self.exposed
|
|
297
|
+
@objects ? @objects.keys : []
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ------------------------------------------------------------ polling
|
|
301
|
+
|
|
302
|
+
# Call from the application's update loop. Answers the calls that came
|
|
303
|
+
# in and follows who is alive. false once the connection is closed.
|
|
304
|
+
def self.poll
|
|
305
|
+
return false unless @session
|
|
306
|
+
pump
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def self.pump
|
|
310
|
+
s = @session
|
|
311
|
+
return false unless s
|
|
312
|
+
unless s.poll
|
|
313
|
+
lost("the connection was lost")
|
|
314
|
+
return false
|
|
315
|
+
end
|
|
316
|
+
answer_calls
|
|
317
|
+
follow_liveliness
|
|
318
|
+
!@session.nil?
|
|
319
|
+
rescue Asterism::Zenoh::Error => e
|
|
320
|
+
lost(e.message)
|
|
321
|
+
false
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Polls until the block is true. Answers incoming calls meanwhile.
|
|
325
|
+
def self.wait_until
|
|
326
|
+
raise Error, "calls nested too deep (max #{MAX_NESTING})" if (@depth || 0) >= MAX_NESTING
|
|
327
|
+
@depth = (@depth || 0) + 1
|
|
328
|
+
begin
|
|
329
|
+
until yield
|
|
330
|
+
break unless pump
|
|
331
|
+
break if yield
|
|
332
|
+
pause(WAIT_STEP_MS)
|
|
333
|
+
end
|
|
334
|
+
ensure
|
|
335
|
+
@depth -= 1
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def self.depth
|
|
340
|
+
(@depth || 0)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def self.follow_liveliness
|
|
344
|
+
w = @watch
|
|
345
|
+
return unless w
|
|
346
|
+
# Array forms instead of blocks called from C (see Future#collect).
|
|
347
|
+
events = w.each_pending
|
|
348
|
+
i = 0
|
|
349
|
+
while i < events.size
|
|
350
|
+
key = events[i][0]
|
|
351
|
+
alive = events[i][1]
|
|
352
|
+
i += 1
|
|
353
|
+
rest = key[ROOT.length + 1, key.length].to_s
|
|
354
|
+
parts = rest.split("/")
|
|
355
|
+
if parts.size == 1
|
|
356
|
+
if alive
|
|
357
|
+
@nodes[rest] = true
|
|
358
|
+
else
|
|
359
|
+
@nodes.delete(rest)
|
|
360
|
+
end
|
|
361
|
+
elsif parts.size == 3
|
|
362
|
+
if alive
|
|
363
|
+
@alive[rest] = true
|
|
364
|
+
else
|
|
365
|
+
@alive.delete(rest)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# The node IDs alive now, this one first.
|
|
372
|
+
def self.nodes
|
|
373
|
+
return [] unless @session
|
|
374
|
+
list = [@node]
|
|
375
|
+
@nodes.each_key { |n| list << n unless list.include?(n) }
|
|
376
|
+
@alive.each_key do |path|
|
|
377
|
+
n = path.split("/")[0]
|
|
378
|
+
list << n unless list.include?(n)
|
|
379
|
+
end
|
|
380
|
+
list
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# The exposed objects alive now whose <node>/<app>/<object> matches the
|
|
384
|
+
# pattern (* is one chunk, ** any number), this application's own
|
|
385
|
+
# included. With a block, yields a proxy for each; else returns them.
|
|
386
|
+
def self.each(pattern = "**")
|
|
387
|
+
paths = []
|
|
388
|
+
if @session
|
|
389
|
+
@objects.each_key do |name|
|
|
390
|
+
path = "#{@node}/#{@app}/#{name}"
|
|
391
|
+
paths << path if glob?(pattern, path)
|
|
392
|
+
end
|
|
393
|
+
@alive.each_key { |path| paths << path if glob?(pattern, path) && !paths.include?(path) }
|
|
394
|
+
end
|
|
395
|
+
proxies = paths.map { |path| self[path] }
|
|
396
|
+
return proxies unless block_given?
|
|
397
|
+
proxies.each { |px| yield px }
|
|
398
|
+
proxies.size
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# A proxy for <node>/<app>/<object>. Nothing is sent until a method is
|
|
402
|
+
# called on it.
|
|
403
|
+
def self.[](path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
404
|
+
split_path(path)
|
|
405
|
+
@proxies ||= {}
|
|
406
|
+
key = "#{path}|#{timeout_ms}"
|
|
407
|
+
@proxies[key] ||= Proxy.new(path.to_s, timeout_ms)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# ------------------------------------------------------------ calling
|
|
411
|
+
|
|
412
|
+
# Starts a call; returns a Future. Raises EncodeError (before sending)
|
|
413
|
+
# and Disconnected.
|
|
414
|
+
def self.call_async(path, method, args, kwargs, timeout_ms)
|
|
415
|
+
parts = split_path(path)
|
|
416
|
+
payload = Codec.pack([method.to_s, args, kwargs])
|
|
417
|
+
if parts[0] == @node && parts[1] == @app && @session
|
|
418
|
+
# This application's own object: no network (a session does not see
|
|
419
|
+
# its own queryable), same encoding and checks as a remote call.
|
|
420
|
+
return Future.answered(path, method, dispatch(parts[2], payload))
|
|
421
|
+
end
|
|
422
|
+
begin
|
|
423
|
+
g = session!.get("#{ROOT}/#{path}/call", timeout_ms, nil, payload)
|
|
424
|
+
rescue Asterism::Zenoh::Error => e
|
|
425
|
+
lost(e.message)
|
|
426
|
+
raise Disconnected, e.message
|
|
427
|
+
end
|
|
428
|
+
Future.new(path, method, g, now_ms + timeout_ms, timeout_ms)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def self.call(path, method, args, kwargs, timeout_ms)
|
|
432
|
+
call_async(path, method, args, kwargs, timeout_ms).value
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# The meta reply of path: {"methods" => [[name, arity], ...]}.
|
|
436
|
+
def self.meta(path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
437
|
+
parts = split_path(path)
|
|
438
|
+
if parts[0] == @node && parts[1] == @app && @session
|
|
439
|
+
m = meta_of(parts[2])
|
|
440
|
+
raise RemoteError.new("NameError", "no object #{parts[2]} exposed by #{@node}/#{@app}") unless m
|
|
441
|
+
return m
|
|
442
|
+
end
|
|
443
|
+
begin
|
|
444
|
+
g = session!.get("#{ROOT}/#{path}/meta", timeout_ms)
|
|
445
|
+
rescue Asterism::Zenoh::Error => e
|
|
446
|
+
lost(e.message)
|
|
447
|
+
raise Disconnected, e.message
|
|
448
|
+
end
|
|
449
|
+
f = Future.new(path, "meta", g, now_ms + timeout_ms, timeout_ms)
|
|
450
|
+
f.raw_value
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# ------------------------------------------------------------ answering
|
|
454
|
+
|
|
455
|
+
def self.answer_calls
|
|
456
|
+
qa = @queryable
|
|
457
|
+
return unless qa
|
|
458
|
+
# Without a block, so that a handler that waits (and so polls) does not
|
|
459
|
+
# re-enter the queryable's iteration.
|
|
460
|
+
queries = qa.each_pending
|
|
461
|
+
i = 0
|
|
462
|
+
while i < queries.size
|
|
463
|
+
q = queries[i]
|
|
464
|
+
i += 1
|
|
465
|
+
begin
|
|
466
|
+
answer(q)
|
|
467
|
+
ensure
|
|
468
|
+
begin
|
|
469
|
+
q.finish
|
|
470
|
+
rescue Asterism::Zenoh::Error
|
|
471
|
+
# the session went away meanwhile
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# asterism/<node>/<app>/<object>/<call|meta>; the node and app may come
|
|
478
|
+
# as wildcards (the query matched this application's queryable).
|
|
479
|
+
def self.answer(q)
|
|
480
|
+
parts = q.key.split("/")
|
|
481
|
+
return unless parts.size == 5
|
|
482
|
+
obj = parts[3]
|
|
483
|
+
kind = parts[4]
|
|
484
|
+
prefix = "#{ROOT}/#{@node}/#{@app}/"
|
|
485
|
+
if kind == "meta"
|
|
486
|
+
names = obj == "*" ? @objects.keys : [obj]
|
|
487
|
+
names.each do |n|
|
|
488
|
+
m = meta_of(n)
|
|
489
|
+
q.reply("#{prefix}#{n}/meta", Codec.pack(m)) if m
|
|
490
|
+
end
|
|
491
|
+
elsif kind == "call" && obj != "*"
|
|
492
|
+
q.reply("#{prefix}#{obj}/call", dispatch(obj, q.payload))
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def self.meta_of(name)
|
|
497
|
+
entry = @objects[name]
|
|
498
|
+
return nil unless entry
|
|
499
|
+
list = []
|
|
500
|
+
entry[1].each { |m, n| list << [m, n] }
|
|
501
|
+
{ "methods" => list }
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Runs a call on an exposed object; returns the packed reply.
|
|
505
|
+
def self.dispatch(obj_name, payload)
|
|
506
|
+
req = Codec.unpack(payload)
|
|
507
|
+
unless req.is_a?(Array) && req.size == 3 && req[0].is_a?(String) && req[1].is_a?(Array) && req[2].is_a?(Hash)
|
|
508
|
+
return error_reply("ArgumentError", "malformed call (expected [method, args, kwargs])")
|
|
509
|
+
end
|
|
510
|
+
entry = @objects[obj_name]
|
|
511
|
+
return error_reply("NameError", "no object #{obj_name} exposed by #{@node}/#{@app}") unless entry
|
|
512
|
+
name = req[0]
|
|
513
|
+
args = req[1]
|
|
514
|
+
kw = req[2]
|
|
515
|
+
arity = entry[1][name]
|
|
516
|
+
if arity.nil?
|
|
517
|
+
return error_reply("NoMethodError", "undefined method '#{name}' for #{@node}/#{@app}/#{obj_name} (not exposed)")
|
|
518
|
+
end
|
|
519
|
+
if arity >= 0 && args.size != arity
|
|
520
|
+
return error_reply("ArgumentError", "wrong number of arguments (given #{args.size}, expected #{arity})")
|
|
521
|
+
end
|
|
522
|
+
begin
|
|
523
|
+
value = invoke(entry[0], name, args, kw)
|
|
524
|
+
Codec.pack(["ok", value])
|
|
525
|
+
rescue EncodeError => e
|
|
526
|
+
error_reply("Asterism::EncodeError", "the return value: #{e.message}")
|
|
527
|
+
rescue => e
|
|
528
|
+
error_reply(e.class.to_s, e.message)
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def self.invoke(obj, name, args, kw)
|
|
533
|
+
if kw.empty?
|
|
534
|
+
obj.public_send(name, *args)
|
|
535
|
+
else
|
|
536
|
+
opts = {}
|
|
537
|
+
kw.each { |k, v| opts[k.to_s.to_sym] = v }
|
|
538
|
+
obj.public_send(name, *args, **opts)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def self.error_reply(klass, message)
|
|
543
|
+
::MessagePack.pack(["error", klass.to_s, message.to_s])
|
|
544
|
+
end
|
|
545
|
+
end
|