asterism 0.0.0 → 0.2.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 +581 -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/objects.rb +224 -0
- data/lib/asterism/cruby/ros.rb +305 -0
- data/lib/asterism/cruby/runner.rb +253 -0
- data/lib/asterism/cruby/zenoh.rb +422 -0
- data/lib/asterism/cruby.rb +30 -0
- data/lib/asterism/version.rb +5 -0
- data/lib/asterism.rb +22 -4
- data/mrblib/asterism.rb +617 -0
- data/mrblib/cdr.rb +512 -0
- data/mrblib/future.rb +88 -0
- data/mrblib/proxy.rb +97 -0
- data/mrblib/ros.rb +861 -0
- data/tools/asterism_msggen.rb +826 -0
- metadata +122 -9
data/lib/asterism.rb
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
# Asterism:
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# Asterism for CRuby: Ruby objects on other machines called like local ones
|
|
2
|
+
# (Asterism.connect / expose / [] / poll), and a ROS 2 node over rmw_zenoh's
|
|
3
|
+
# wire format (Asterism::ROS, Asterism::CDR, generated message types).
|
|
4
|
+
#
|
|
5
|
+
# The Ruby code is the one the mruby / PicoRuby boards run: mrblib/ of this
|
|
6
|
+
# repository, which is also the mrbgem (mrbgem.rake). Nothing is copied:
|
|
7
|
+
# this file loads mrblib/ as it is, and lib/asterism/cruby.rb adds what
|
|
8
|
+
# CRuby needs. The Zenoh binding is the asterism-zenoh gem (Asterism::Zenoh,
|
|
9
|
+
# over zenoh-c), with the same API as the boards' zenoh-pico binding.
|
|
10
|
+
require "msgpack"
|
|
11
|
+
require "asterism/zenoh"
|
|
12
|
+
require_relative "asterism/version"
|
|
13
|
+
|
|
14
|
+
# The same order as an mrbgem's mrblib (alphabetical).
|
|
15
|
+
%w[asterism cdr future proxy ros].each do |f|
|
|
16
|
+
require_relative "../mrblib/#{f}"
|
|
17
|
+
end
|
|
18
|
+
require_relative "asterism/cruby"
|
|
19
|
+
# The Ruby-like API on top (blocks, a receiving thread, Enumerators,
|
|
20
|
+
# pattern matching): CRuby only, see README "The Ruby-like API".
|
|
21
|
+
%w[runner zenoh objects ros].each do |f|
|
|
22
|
+
require_relative "asterism/cruby/#{f}"
|
|
5
23
|
end
|
data/mrblib/asterism.rb
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
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
|
+
release
|
|
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
|
+
@known = [] # the other nodes told to on_join so far
|
|
199
|
+
self
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Whether objects of node/app are alive already (a second copy of the
|
|
203
|
+
# same application). Waits for the answer (a router answers at once).
|
|
204
|
+
def self.already_there?(s, node, app)
|
|
205
|
+
g = s.liveliness_get("#{ROOT}/#{node}/#{app}/**", 1000)
|
|
206
|
+
t0 = now_ms
|
|
207
|
+
found = false
|
|
208
|
+
while true
|
|
209
|
+
s.poll
|
|
210
|
+
ended = g.done? # before taking the replies (see Future#collect)
|
|
211
|
+
found = true if g.each_reply.size > 0
|
|
212
|
+
break if found || ended || now_ms - t0 > 1500
|
|
213
|
+
pause(WAIT_STEP_MS)
|
|
214
|
+
end
|
|
215
|
+
found
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.connected?
|
|
219
|
+
return false if @session.nil?
|
|
220
|
+
if @session.closed?
|
|
221
|
+
lost("the connection was lost")
|
|
222
|
+
return false
|
|
223
|
+
end
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.node_id
|
|
228
|
+
@node
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.app
|
|
232
|
+
@app
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Why the connection went away (nil while connected or after close).
|
|
236
|
+
def self.lost_reason
|
|
237
|
+
@lost
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Closes the connection (and with it every exposed object), and forgets
|
|
241
|
+
# the on_join / on_leave blocks. Idempotent.
|
|
242
|
+
def self.close
|
|
243
|
+
release
|
|
244
|
+
@known = nil
|
|
245
|
+
@on_join = nil
|
|
246
|
+
@on_leave = nil
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def self.release
|
|
251
|
+
s = @session
|
|
252
|
+
@session = nil
|
|
253
|
+
@objects = {}
|
|
254
|
+
@alive = {}
|
|
255
|
+
@nodes = {}
|
|
256
|
+
@proxies = {}
|
|
257
|
+
s.close if s
|
|
258
|
+
@node_token = nil
|
|
259
|
+
@queryable = nil
|
|
260
|
+
@watch = nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.session!
|
|
264
|
+
raise Disconnected, (@lost || "not connected") unless @session
|
|
265
|
+
@session
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The connection went away: closed, but the next Asterism.poll still tells
|
|
269
|
+
# on_leave about every node known until then.
|
|
270
|
+
def self.lost(reason)
|
|
271
|
+
@lost = reason
|
|
272
|
+
release
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# ------------------------------------------------------------ exposing
|
|
276
|
+
|
|
277
|
+
# Makes obj callable from other machines as <node>/<app>/<name>. Only the
|
|
278
|
+
# listed methods can be called. methods: is an Array of names, or a Hash
|
|
279
|
+
# name => number of arguments (checked before calling; -1 for any).
|
|
280
|
+
def self.expose(name, obj, methods:)
|
|
281
|
+
s = session!
|
|
282
|
+
name = check_name("object", name)
|
|
283
|
+
table = {}
|
|
284
|
+
if methods.is_a?(Hash)
|
|
285
|
+
methods.each { |m, n| table[m.to_s] = n.to_i }
|
|
286
|
+
else
|
|
287
|
+
methods.each { |m| table[m.to_s] = -1 }
|
|
288
|
+
end
|
|
289
|
+
raise ArgumentError, "no method to expose" if table.empty?
|
|
290
|
+
table.each_key do |m|
|
|
291
|
+
raise ArgumentError, "#{obj.class} has no public method #{m}" unless obj.respond_to?(m)
|
|
292
|
+
end
|
|
293
|
+
unexpose(name)
|
|
294
|
+
token = s.liveliness("#{ROOT}/#{@node}/#{@app}/#{name}")
|
|
295
|
+
@objects[name] = [obj, table, token]
|
|
296
|
+
"#{@node}/#{@app}/#{name}"
|
|
297
|
+
rescue Asterism::Zenoh::Error => e
|
|
298
|
+
raise Disconnected, e.message
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def self.unexpose(name)
|
|
302
|
+
entry = @objects ? @objects.delete(name.to_s) : nil
|
|
303
|
+
entry[2].close if entry
|
|
304
|
+
!entry.nil?
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def self.exposed
|
|
308
|
+
@objects ? @objects.keys : []
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------ polling
|
|
312
|
+
|
|
313
|
+
# Call from the application's update loop. Answers the calls that came
|
|
314
|
+
# in, follows who is alive and calls the on_join / on_leave blocks. false
|
|
315
|
+
# once the connection is closed.
|
|
316
|
+
def self.poll
|
|
317
|
+
ok = @session ? pump : false
|
|
318
|
+
tell_nodes
|
|
319
|
+
ok
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# ------------------------------------------------------------ nodes coming and going
|
|
323
|
+
|
|
324
|
+
# on_join { |node| }: another node appeared (its node token or one of its
|
|
325
|
+
# objects). The nodes there already join on the first polls after
|
|
326
|
+
# connecting. on_leave { |node| }: a node is gone (no token and no object
|
|
327
|
+
# left); when the connection is lost, every node known until then leaves.
|
|
328
|
+
#
|
|
329
|
+
# The blocks run from Asterism.poll only, never from the polling inside a
|
|
330
|
+
# waiting call. What a block raises comes out of Asterism.poll (on CRuby
|
|
331
|
+
# with a receiving thread: to on_error). Asterism.close forgets them.
|
|
332
|
+
def self.on_join(&blk)
|
|
333
|
+
raise ArgumentError, "on_join needs a block" unless blk
|
|
334
|
+
(@on_join ||= []) << blk
|
|
335
|
+
self
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def self.on_leave(&blk)
|
|
339
|
+
raise ArgumentError, "on_leave needs a block" unless blk
|
|
340
|
+
(@on_leave ||= []) << blk
|
|
341
|
+
self
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Compares the nodes alive now with those told before and calls the
|
|
345
|
+
# blocks for the difference.
|
|
346
|
+
def self.tell_nodes
|
|
347
|
+
known = @known
|
|
348
|
+
return if known.nil?
|
|
349
|
+
now = @session ? nodes : []
|
|
350
|
+
now.shift if @session # this node
|
|
351
|
+
joined = []
|
|
352
|
+
left = []
|
|
353
|
+
now.each { |n| joined << n unless known.include?(n) }
|
|
354
|
+
known.each { |n| left << n unless now.include?(n) }
|
|
355
|
+
# After a loss, everything has been told: nothing more to compare.
|
|
356
|
+
@known = @session ? now : nil
|
|
357
|
+
tell_each("on_join", @on_join, joined)
|
|
358
|
+
tell_each("on_leave", @on_leave, left)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# While loops, not each with a block: the blocks are the application's,
|
|
362
|
+
# and nothing else stands between them and the update loop on the stack.
|
|
363
|
+
def self.tell_each(where, blocks, names)
|
|
364
|
+
return if blocks.nil? || names.empty?
|
|
365
|
+
i = 0
|
|
366
|
+
while i < names.size
|
|
367
|
+
j = 0
|
|
368
|
+
while j < blocks.size
|
|
369
|
+
tell(where, blocks[j], names[i])
|
|
370
|
+
j += 1
|
|
371
|
+
end
|
|
372
|
+
i += 1
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# One block for one node (CRuby routes what it raises to on_error).
|
|
377
|
+
def self.tell(_where, blk, node)
|
|
378
|
+
blk.call(node)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def self.pump
|
|
382
|
+
s = @session
|
|
383
|
+
return false unless s
|
|
384
|
+
unless s.poll
|
|
385
|
+
lost("the connection was lost")
|
|
386
|
+
return false
|
|
387
|
+
end
|
|
388
|
+
answer_calls
|
|
389
|
+
follow_liveliness
|
|
390
|
+
!@session.nil?
|
|
391
|
+
rescue Asterism::Zenoh::Error => e
|
|
392
|
+
lost(e.message)
|
|
393
|
+
false
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Polls until the block is true. Answers incoming calls meanwhile.
|
|
397
|
+
def self.wait_until
|
|
398
|
+
raise Error, "calls nested too deep (max #{MAX_NESTING})" if (@depth || 0) >= MAX_NESTING
|
|
399
|
+
@depth = (@depth || 0) + 1
|
|
400
|
+
begin
|
|
401
|
+
until yield
|
|
402
|
+
break unless pump
|
|
403
|
+
break if yield
|
|
404
|
+
pause(WAIT_STEP_MS)
|
|
405
|
+
end
|
|
406
|
+
ensure
|
|
407
|
+
@depth -= 1
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def self.depth
|
|
412
|
+
(@depth || 0)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def self.follow_liveliness
|
|
416
|
+
w = @watch
|
|
417
|
+
return unless w
|
|
418
|
+
# Array forms instead of blocks called from C (see Future#collect).
|
|
419
|
+
events = w.each_pending
|
|
420
|
+
i = 0
|
|
421
|
+
while i < events.size
|
|
422
|
+
key = events[i][0]
|
|
423
|
+
alive = events[i][1]
|
|
424
|
+
i += 1
|
|
425
|
+
rest = key[ROOT.length + 1, key.length].to_s
|
|
426
|
+
parts = rest.split("/")
|
|
427
|
+
if parts.size == 1
|
|
428
|
+
if alive
|
|
429
|
+
@nodes[rest] = true
|
|
430
|
+
else
|
|
431
|
+
@nodes.delete(rest)
|
|
432
|
+
end
|
|
433
|
+
elsif parts.size == 3
|
|
434
|
+
if alive
|
|
435
|
+
@alive[rest] = true
|
|
436
|
+
else
|
|
437
|
+
@alive.delete(rest)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# The node IDs alive now, this one first.
|
|
444
|
+
def self.nodes
|
|
445
|
+
return [] unless @session
|
|
446
|
+
list = [@node]
|
|
447
|
+
@nodes.each_key { |n| list << n unless list.include?(n) }
|
|
448
|
+
@alive.each_key do |path|
|
|
449
|
+
n = path.split("/")[0]
|
|
450
|
+
list << n unless list.include?(n)
|
|
451
|
+
end
|
|
452
|
+
list
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# The exposed objects alive now whose <node>/<app>/<object> matches the
|
|
456
|
+
# pattern (* is one chunk, ** any number), this application's own
|
|
457
|
+
# included. With a block, yields a proxy for each; else returns them.
|
|
458
|
+
def self.each(pattern = "**")
|
|
459
|
+
paths = []
|
|
460
|
+
if @session
|
|
461
|
+
@objects.each_key do |name|
|
|
462
|
+
path = "#{@node}/#{@app}/#{name}"
|
|
463
|
+
paths << path if glob?(pattern, path)
|
|
464
|
+
end
|
|
465
|
+
@alive.each_key { |path| paths << path if glob?(pattern, path) && !paths.include?(path) }
|
|
466
|
+
end
|
|
467
|
+
proxies = paths.map { |path| self[path] }
|
|
468
|
+
return proxies unless block_given?
|
|
469
|
+
proxies.each { |px| yield px }
|
|
470
|
+
proxies.size
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# A proxy for <node>/<app>/<object>. Nothing is sent until a method is
|
|
474
|
+
# called on it.
|
|
475
|
+
def self.[](path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
476
|
+
split_path(path)
|
|
477
|
+
@proxies ||= {}
|
|
478
|
+
key = "#{path}|#{timeout_ms}"
|
|
479
|
+
@proxies[key] ||= Proxy.new(path.to_s, timeout_ms)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# ------------------------------------------------------------ calling
|
|
483
|
+
|
|
484
|
+
# Starts a call; returns a Future. Raises EncodeError (before sending)
|
|
485
|
+
# and Disconnected.
|
|
486
|
+
def self.call_async(path, method, args, kwargs, timeout_ms)
|
|
487
|
+
parts = split_path(path)
|
|
488
|
+
payload = Codec.pack([method.to_s, args, kwargs])
|
|
489
|
+
if parts[0] == @node && parts[1] == @app && @session
|
|
490
|
+
# This application's own object: no network (a session does not see
|
|
491
|
+
# its own queryable), same encoding and checks as a remote call.
|
|
492
|
+
return Future.answered(path, method, dispatch(parts[2], payload))
|
|
493
|
+
end
|
|
494
|
+
begin
|
|
495
|
+
g = session!.get("#{ROOT}/#{path}/call", timeout_ms, nil, payload)
|
|
496
|
+
rescue Asterism::Zenoh::Error => e
|
|
497
|
+
lost(e.message)
|
|
498
|
+
raise Disconnected, e.message
|
|
499
|
+
end
|
|
500
|
+
Future.new(path, method, g, now_ms + timeout_ms, timeout_ms)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def self.call(path, method, args, kwargs, timeout_ms)
|
|
504
|
+
call_async(path, method, args, kwargs, timeout_ms).value
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# The meta reply of path: {"methods" => [[name, arity], ...]}.
|
|
508
|
+
def self.meta(path, timeout_ms = DEFAULT_TIMEOUT_MS)
|
|
509
|
+
parts = split_path(path)
|
|
510
|
+
if parts[0] == @node && parts[1] == @app && @session
|
|
511
|
+
m = meta_of(parts[2])
|
|
512
|
+
raise RemoteError.new("NameError", "no object #{parts[2]} exposed by #{@node}/#{@app}") unless m
|
|
513
|
+
return m
|
|
514
|
+
end
|
|
515
|
+
begin
|
|
516
|
+
g = session!.get("#{ROOT}/#{path}/meta", timeout_ms)
|
|
517
|
+
rescue Asterism::Zenoh::Error => e
|
|
518
|
+
lost(e.message)
|
|
519
|
+
raise Disconnected, e.message
|
|
520
|
+
end
|
|
521
|
+
f = Future.new(path, "meta", g, now_ms + timeout_ms, timeout_ms)
|
|
522
|
+
f.raw_value
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# ------------------------------------------------------------ answering
|
|
526
|
+
|
|
527
|
+
def self.answer_calls
|
|
528
|
+
qa = @queryable
|
|
529
|
+
return unless qa
|
|
530
|
+
# Without a block, so that a handler that waits (and so polls) does not
|
|
531
|
+
# re-enter the queryable's iteration.
|
|
532
|
+
queries = qa.each_pending
|
|
533
|
+
i = 0
|
|
534
|
+
while i < queries.size
|
|
535
|
+
q = queries[i]
|
|
536
|
+
i += 1
|
|
537
|
+
begin
|
|
538
|
+
answer(q)
|
|
539
|
+
ensure
|
|
540
|
+
begin
|
|
541
|
+
q.finish
|
|
542
|
+
rescue Asterism::Zenoh::Error
|
|
543
|
+
# the session went away meanwhile
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# asterism/<node>/<app>/<object>/<call|meta>; the node and app may come
|
|
550
|
+
# as wildcards (the query matched this application's queryable).
|
|
551
|
+
def self.answer(q)
|
|
552
|
+
parts = q.key.split("/")
|
|
553
|
+
return unless parts.size == 5
|
|
554
|
+
obj = parts[3]
|
|
555
|
+
kind = parts[4]
|
|
556
|
+
prefix = "#{ROOT}/#{@node}/#{@app}/"
|
|
557
|
+
if kind == "meta"
|
|
558
|
+
names = obj == "*" ? @objects.keys : [obj]
|
|
559
|
+
names.each do |n|
|
|
560
|
+
m = meta_of(n)
|
|
561
|
+
q.reply("#{prefix}#{n}/meta", Codec.pack(m)) if m
|
|
562
|
+
end
|
|
563
|
+
elsif kind == "call" && obj != "*"
|
|
564
|
+
q.reply("#{prefix}#{obj}/call", dispatch(obj, q.payload))
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def self.meta_of(name)
|
|
569
|
+
entry = @objects[name]
|
|
570
|
+
return nil unless entry
|
|
571
|
+
list = []
|
|
572
|
+
entry[1].each { |m, n| list << [m, n] }
|
|
573
|
+
{ "methods" => list }
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# Runs a call on an exposed object; returns the packed reply.
|
|
577
|
+
def self.dispatch(obj_name, payload)
|
|
578
|
+
req = Codec.unpack(payload)
|
|
579
|
+
unless req.is_a?(Array) && req.size == 3 && req[0].is_a?(String) && req[1].is_a?(Array) && req[2].is_a?(Hash)
|
|
580
|
+
return error_reply("ArgumentError", "malformed call (expected [method, args, kwargs])")
|
|
581
|
+
end
|
|
582
|
+
entry = @objects[obj_name]
|
|
583
|
+
return error_reply("NameError", "no object #{obj_name} exposed by #{@node}/#{@app}") unless entry
|
|
584
|
+
name = req[0]
|
|
585
|
+
args = req[1]
|
|
586
|
+
kw = req[2]
|
|
587
|
+
arity = entry[1][name]
|
|
588
|
+
if arity.nil?
|
|
589
|
+
return error_reply("NoMethodError", "undefined method '#{name}' for #{@node}/#{@app}/#{obj_name} (not exposed)")
|
|
590
|
+
end
|
|
591
|
+
if arity >= 0 && args.size != arity
|
|
592
|
+
return error_reply("ArgumentError", "wrong number of arguments (given #{args.size}, expected #{arity})")
|
|
593
|
+
end
|
|
594
|
+
begin
|
|
595
|
+
value = invoke(entry[0], name, args, kw)
|
|
596
|
+
Codec.pack(["ok", value])
|
|
597
|
+
rescue EncodeError => e
|
|
598
|
+
error_reply("Asterism::EncodeError", "the return value: #{e.message}")
|
|
599
|
+
rescue => e
|
|
600
|
+
error_reply(e.class.to_s, e.message)
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def self.invoke(obj, name, args, kw)
|
|
605
|
+
if kw.empty?
|
|
606
|
+
obj.public_send(name, *args)
|
|
607
|
+
else
|
|
608
|
+
opts = {}
|
|
609
|
+
kw.each { |k, v| opts[k.to_s.to_sym] = v }
|
|
610
|
+
obj.public_send(name, *args, **opts)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def self.error_reply(klass, message)
|
|
615
|
+
::MessagePack.pack(["error", klass.to_s, message.to_s])
|
|
616
|
+
end
|
|
617
|
+
end
|