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/mrblib/ros.rb
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
# Asterism::ROS: a minimal ROS 2 node over rmw_zenoh's wire format
|
|
2
|
+
# (doc/ruby_asterism/design.md ch. 5; topics R1, services R2, message types
|
|
3
|
+
# R3). The message and service types are generated from .msg / .srv files
|
|
4
|
+
# (tools/asterism_msggen.rb) and loaded when the application asks for them.
|
|
5
|
+
#
|
|
6
|
+
# s = Asterism::Zenoh::Session.open("tcp/192.0.2.2:7447")
|
|
7
|
+
# node = Asterism::ROS::Node.new(s, "fmruby_talker")
|
|
8
|
+
# str = Asterism::ROS.require_type("std_msgs/msg/String")
|
|
9
|
+
# pub = node.publisher("/chatter", str)
|
|
10
|
+
# sub = node.subscription("/chatter_back", "std_msgs/msg/String") # by name
|
|
11
|
+
# add = Asterism::ROS.require_type("example_interfaces/srv/AddTwoInts")
|
|
12
|
+
# node.service("/add_two_ints_fmrb", add) { |req| { sum: req.a + req.b } }
|
|
13
|
+
# cli = node.client("/add_two_ints", add)
|
|
14
|
+
# node.subscribe("/cmd", "std_msgs/msg/String") { |msg, info| puts msg.data }
|
|
15
|
+
# node.every(1.0) { pub << { data: "tick" } }
|
|
16
|
+
# loop do
|
|
17
|
+
# node.poll # session.poll, services, then the
|
|
18
|
+
# # subscribe and every blocks
|
|
19
|
+
# pub << { data: "hello" } # or pub.publish(str.new(data: "hello"))
|
|
20
|
+
# sub.each_pending { |msg, info| puts msg.data } # info: Attachment or nil
|
|
21
|
+
# end
|
|
22
|
+
# cli.call(a: 2, b: 3).sum # waits (polling), or
|
|
23
|
+
# c = cli.call_async(a: 2, b: 3); c.done?; c.value
|
|
24
|
+
# node.close
|
|
25
|
+
#
|
|
26
|
+
# What goes on the wire (rmw_zenoh_cpp 0.2.x, ROS 2 Jazzy):
|
|
27
|
+
# - data key: <domain>/<topic without the outer slashes>/<DDS type name>/<type hash>
|
|
28
|
+
# e.g. 0/chatter/std_msgs::msg::dds_::String_/RIHS01_df66...
|
|
29
|
+
# - payload: CDR (Asterism::CDR)
|
|
30
|
+
# - attachment (required by rmw_zenoh subscribers; a sample without one is
|
|
31
|
+
# dropped): int64 sequence number, int64 source time (ns since the epoch),
|
|
32
|
+
# both little endian, then the GID as a one-byte length (16) and 16 bytes.
|
|
33
|
+
# - liveliness tokens, which make the node and its topics appear in
|
|
34
|
+
# `ros2 node list` / `ros2 topic list`:
|
|
35
|
+
# @ros2_lv/<domain>/<zid>/<nid>/<nid>/NN/<enclave>/<namespace>/<node>
|
|
36
|
+
# @ros2_lv/<domain>/<zid>/<nid>/<id>/MP|MS/<enclave>/<namespace>/<node>/
|
|
37
|
+
# <topic>/<DDS type name>/<type hash>/<qos>
|
|
38
|
+
# with "/" written as "%" in enclave, namespace and topic ("%" alone for "/").
|
|
39
|
+
# - services: the server is a complete queryable on
|
|
40
|
+
# <domain>/<service>/<DDS service type>/<service type hash>, the client
|
|
41
|
+
# gets that key with target ALL_COMPLETE and no consolidation. Request and
|
|
42
|
+
# reply both carry the attachment above: the client's sequence number and
|
|
43
|
+
# GID go out with the request, and the reply carries them back (with the
|
|
44
|
+
# server's time), which is how the client pairs them. Tokens SS (server)
|
|
45
|
+
# and SC (client), shaped like MP / MS.
|
|
46
|
+
#
|
|
47
|
+
# Nothing here depends on the host: it uses an Asterism::Zenoh::Session
|
|
48
|
+
# (put with attachment:, subscribe, liveliness, zid) and Asterism::CDR.
|
|
49
|
+
module Asterism
|
|
50
|
+
module ROS
|
|
51
|
+
LIVELINESS_ROOT = "@ros2_lv"
|
|
52
|
+
# rmw_zenoh's QoS chunk for the default profile (reliable, volatile,
|
|
53
|
+
# keep last 10): fields that equal the default are left empty.
|
|
54
|
+
DEFAULT_QOS = "::,10:,:,:,,"
|
|
55
|
+
GID_SIZE = 16
|
|
56
|
+
|
|
57
|
+
# A name chunk of a liveliness token: "/" becomes "%", "" or "/" is "%".
|
|
58
|
+
def self.mangle(name)
|
|
59
|
+
s = name.to_s
|
|
60
|
+
return "%" if s.empty? || s == "/"
|
|
61
|
+
out = ""
|
|
62
|
+
i = 0
|
|
63
|
+
n = s.bytesize
|
|
64
|
+
while i < n
|
|
65
|
+
c = s.byteslice(i, 1)
|
|
66
|
+
out << (c == "/" ? "%" : c)
|
|
67
|
+
i += 1
|
|
68
|
+
end
|
|
69
|
+
out
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Drops one leading and one trailing "/".
|
|
73
|
+
def self.strip_slashes(name)
|
|
74
|
+
s = name.to_s
|
|
75
|
+
s = s.byteslice(1, s.bytesize - 1) if s.bytesize > 0 && s.byteslice(0, 1) == "/"
|
|
76
|
+
s = s.byteslice(0, s.bytesize - 1) if s.bytesize > 0 && s.byteslice(s.bytesize - 1, 1) == "/"
|
|
77
|
+
s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A full topic name: absolute names stay, relative ones go under the
|
|
81
|
+
# namespace. "~" and substitutions are not supported.
|
|
82
|
+
def self.resolve(topic, namespace)
|
|
83
|
+
t = topic.to_s
|
|
84
|
+
raise ArgumentError, "empty topic name" if t.empty?
|
|
85
|
+
return t if t.byteslice(0, 1) == "/"
|
|
86
|
+
ns = namespace.to_s
|
|
87
|
+
ns == "/" || ns.empty? ? "/" + t : ns + "/" + t
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Nanoseconds since the epoch, or 0 when there is no clock.
|
|
91
|
+
def self.now_ns
|
|
92
|
+
if Object.const_defined?(:Time)
|
|
93
|
+
t = ::Time.now
|
|
94
|
+
t.to_i * 1_000_000_000 + t.usec * 1000
|
|
95
|
+
else
|
|
96
|
+
0
|
|
97
|
+
end
|
|
98
|
+
rescue
|
|
99
|
+
0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 16 bytes derived from a string (32-bit FNV-1a, four lanes with
|
|
103
|
+
# different seeds; small enough not to overflow a 64-bit Integer).
|
|
104
|
+
# rmw_zenoh hashes the entity's liveliness key the same way (with XXH3);
|
|
105
|
+
# receivers only use the GID to tell publishers apart.
|
|
106
|
+
def self.gid_for(key)
|
|
107
|
+
out = ""
|
|
108
|
+
lane = 0
|
|
109
|
+
while lane < 4
|
|
110
|
+
h = 0x811c9dc5 ^ (lane * 0x9e3779b9 & 0xffffffff)
|
|
111
|
+
i = 0
|
|
112
|
+
n = key.bytesize
|
|
113
|
+
while i < n
|
|
114
|
+
h = ((h ^ key.getbyte(i)) * 0x01000193) & 0xffffffff
|
|
115
|
+
i += 1
|
|
116
|
+
end
|
|
117
|
+
out << ::Asterism::CDR.le_bytes(h, 4)
|
|
118
|
+
lane += 1
|
|
119
|
+
end
|
|
120
|
+
out
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A service call got no answer in time (or nobody serves that name).
|
|
124
|
+
class Timeout < ::StandardError; end
|
|
125
|
+
|
|
126
|
+
# Milliseconds from an arbitrary origin (the board's clock when there is
|
|
127
|
+
# one), and a short pause; the same helpers Asterism's calls use.
|
|
128
|
+
def self.now_ms
|
|
129
|
+
::Asterism.now_ms
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.pause(ms)
|
|
133
|
+
::Asterism.pause(ms)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The attachment of a sample: sequence number, source time, publisher GID.
|
|
137
|
+
class Attachment
|
|
138
|
+
attr_reader :sequence, :stamp_ns, :gid
|
|
139
|
+
|
|
140
|
+
def initialize(sequence, stamp_ns, gid)
|
|
141
|
+
@sequence = sequence
|
|
142
|
+
@stamp_ns = stamp_ns
|
|
143
|
+
@gid = gid
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# For pattern matching: case info in {sequence:, gid:}.
|
|
147
|
+
def deconstruct_keys(_keys)
|
|
148
|
+
{ sequence: @sequence, stamp_ns: @stamp_ns, gid: @gid }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def encode
|
|
152
|
+
g = @gid.to_s
|
|
153
|
+
::Asterism::CDR.le_bytes(@sequence, 8) + ::Asterism::CDR.le_bytes(@stamp_ns, 8) +
|
|
154
|
+
::Asterism::CDR.le_bytes(g.bytesize, 1) + g
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# nil when the bytes are not an attachment of this shape.
|
|
158
|
+
def self.decode(bytes)
|
|
159
|
+
return nil if bytes.nil? || bytes.bytesize < 17
|
|
160
|
+
glen = bytes.getbyte(16)
|
|
161
|
+
return nil if bytes.bytesize < 17 + glen
|
|
162
|
+
new(le_int(bytes, 0), le_int(bytes, 8), bytes.byteslice(17, glen))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.le_int(bytes, at)
|
|
166
|
+
top = bytes.getbyte(at + 7)
|
|
167
|
+
v = top >= 0x80 ? top - 0x100 : top
|
|
168
|
+
i = 6
|
|
169
|
+
while i >= 0
|
|
170
|
+
v = (v << 8) | bytes.getbyte(at + i)
|
|
171
|
+
i -= 1
|
|
172
|
+
end
|
|
173
|
+
v
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Where Asterism::ROS.require_type looks for generated types
|
|
178
|
+
# (<dir>/<pkg>/<msg|srv>/<Name>.rb, tools/asterism_msggen.rb). The
|
|
179
|
+
# application may add its own directories (or set them on CRuby).
|
|
180
|
+
TYPE_PATH = ["/usr/share/asterism/msgs"]
|
|
181
|
+
|
|
182
|
+
# The type has no generated file in TYPE_PATH.
|
|
183
|
+
class UnknownType < ::StandardError; end
|
|
184
|
+
|
|
185
|
+
def self.type_path
|
|
186
|
+
TYPE_PATH
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Loads a generated message or service type by its ROS name
|
|
190
|
+
# ("geometry_msgs/msg/Twist", "example_interfaces/srv/AddTwoInts") and
|
|
191
|
+
# returns it (Asterism::ROS::GeometryMsgs::Twist). The file loads the
|
|
192
|
+
# types it is made of first. A type already defined (loaded before, or
|
|
193
|
+
# written in Ruby by the application) is returned without a file.
|
|
194
|
+
def self.require_type(name)
|
|
195
|
+
parts = name.to_s.split("/")
|
|
196
|
+
if parts.size != 3 || (parts[1] != "msg" && parts[1] != "srv")
|
|
197
|
+
raise ArgumentError, "bad type name #{name.inspect} (want pkg/msg/Name or pkg/srv/Name)"
|
|
198
|
+
end
|
|
199
|
+
t = type_constant(parts[0], parts[2])
|
|
200
|
+
return t if t
|
|
201
|
+
found = nil
|
|
202
|
+
TYPE_PATH.each do |dir|
|
|
203
|
+
path = "#{dir}/#{parts[0]}/#{parts[1]}/#{parts[2]}.rb"
|
|
204
|
+
found = path if found.nil? && File.exist?(path)
|
|
205
|
+
end
|
|
206
|
+
load_type_file(found) if found
|
|
207
|
+
t = type_constant(parts[0], parts[2])
|
|
208
|
+
raise UnknownType, "no generated type #{name} in #{TYPE_PATH.join(', ')}" if t.nil?
|
|
209
|
+
t
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# On mruby the file is evaluated in this VM (Kernel#eval): unlike
|
|
213
|
+
# require, which runs each file in a Sandbox task of its own that stays
|
|
214
|
+
# for the life of the VM, this keeps only the compiled code (measured in
|
|
215
|
+
# report/r3.md). CRuby requires it.
|
|
216
|
+
def self.load_type_file(path)
|
|
217
|
+
if RUBY_ENGINE == "ruby"
|
|
218
|
+
require File.expand_path(path)
|
|
219
|
+
else
|
|
220
|
+
src = File.open(path, "r") { |f| f.read }
|
|
221
|
+
eval(src)
|
|
222
|
+
end
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Asterism::ROS::<Pkg>::<Name>, or nil.
|
|
227
|
+
def self.type_constant(pkg, name)
|
|
228
|
+
mod_name = ""
|
|
229
|
+
pkg.split("_").each do |w|
|
|
230
|
+
mod_name << w.byteslice(0, 1).upcase << w.byteslice(1, w.bytesize - 1) if w.bytesize > 0
|
|
231
|
+
end
|
|
232
|
+
mod_sym = mod_name.to_sym
|
|
233
|
+
return nil unless ::Asterism::ROS.const_defined?(mod_sym, false)
|
|
234
|
+
mod = ::Asterism::ROS.const_get(mod_sym)
|
|
235
|
+
sym = name.to_sym
|
|
236
|
+
return nil unless mod.const_defined?(sym, false)
|
|
237
|
+
mod.const_get(sym)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# A type given as a generated type or its ROS name (String).
|
|
241
|
+
def self.type_of(type)
|
|
242
|
+
type.is_a?(::String) ? require_type(type) : type
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# For the generated types' from: a Hash of fields with Symbol keys
|
|
246
|
+
# (String keys are turned into Symbols). Anything else raises TypeError.
|
|
247
|
+
def self.fields_of(v, type)
|
|
248
|
+
raise TypeError, "#{type::ROS_NAME} from #{v.class}: want a Hash or a #{type::ROS_NAME}" unless v.is_a?(::Hash)
|
|
249
|
+
out = {}
|
|
250
|
+
v.each { |k, x| out[k.is_a?(::Symbol) ? k : k.to_s.to_sym] = x }
|
|
251
|
+
out
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# The base of the generated message types (Request / Response of a
|
|
255
|
+
# service too). A type adds ROS_NAME, TYPE_NAME, TYPE_HASH, FIELDS, an
|
|
256
|
+
# accessor per field, initialize(**fields) and the CDR steps
|
|
257
|
+
# write(w, msg) / read(r); the rest is here.
|
|
258
|
+
class Message
|
|
259
|
+
# A message from nil (all defaults), a Hash of fields (Symbol or
|
|
260
|
+
# String keys; missing fields take their defaults, unknown ones raise
|
|
261
|
+
# ArgumentError) or a message of this type (itself).
|
|
262
|
+
def self.from(v)
|
|
263
|
+
return v if v.is_a?(self)
|
|
264
|
+
return new if v.nil?
|
|
265
|
+
new(**::Asterism::ROS.fields_of(v, self))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# CDR bytes (with the 4-byte header) of a message or a Hash.
|
|
269
|
+
def self.encode(msg)
|
|
270
|
+
w = ::Asterism::CDR::Writer.new
|
|
271
|
+
write(w, from(msg))
|
|
272
|
+
w.to_s
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def self.decode(bytes)
|
|
276
|
+
read(::Asterism::CDR::Reader.new(bytes))
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# The fields as a Hash (nested messages as Hashes too).
|
|
280
|
+
def to_h
|
|
281
|
+
h = {}
|
|
282
|
+
fs = self.class::FIELDS
|
|
283
|
+
i = 0
|
|
284
|
+
while i < fs.size
|
|
285
|
+
h[fs[i]] = ::Asterism::ROS::Message.plain(__send__(fs[i]))
|
|
286
|
+
i += 1
|
|
287
|
+
end
|
|
288
|
+
h
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def self.plain(v)
|
|
292
|
+
if v.is_a?(::Asterism::ROS::Message)
|
|
293
|
+
v.to_h
|
|
294
|
+
elsif v.is_a?(::Array) && v.size > 0 && v[0].is_a?(::Asterism::ROS::Message)
|
|
295
|
+
v.map { |e| e.to_h }
|
|
296
|
+
else
|
|
297
|
+
v
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# For pattern matching: case msg in {linear: {x:}, angular: {z:}}.
|
|
302
|
+
# Nested messages stay messages (they match the same way).
|
|
303
|
+
def deconstruct_keys(keys)
|
|
304
|
+
h = {}
|
|
305
|
+
fs = self.class::FIELDS
|
|
306
|
+
i = 0
|
|
307
|
+
while i < fs.size
|
|
308
|
+
f = fs[i]
|
|
309
|
+
h[f] = __send__(f) if keys.nil? || keys.include?(f)
|
|
310
|
+
i += 1
|
|
311
|
+
end
|
|
312
|
+
h
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def ==(other)
|
|
316
|
+
other.class == self.class && to_h == other.to_h
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def inspect
|
|
320
|
+
"#<#{self.class::ROS_NAME} #{to_h.inspect}>"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
alias to_s inspect
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
class Node
|
|
327
|
+
attr_reader :name, :namespace, :domain, :zid, :key, :session
|
|
328
|
+
|
|
329
|
+
# session: an open Asterism::Zenoh::Session. Declares the node's
|
|
330
|
+
# liveliness token at once.
|
|
331
|
+
def initialize(session, name, namespace: "/", domain: 0, enclave: "/")
|
|
332
|
+
@session = session
|
|
333
|
+
@name = name.to_s
|
|
334
|
+
raise ArgumentError, "bad node name #{@name.inspect}" if @name.empty? || @name.include?("/")
|
|
335
|
+
@namespace = namespace.to_s
|
|
336
|
+
@domain = domain.to_i
|
|
337
|
+
@enclave = enclave.to_s
|
|
338
|
+
@zid = session.zid
|
|
339
|
+
@nid = 0
|
|
340
|
+
@next_id = 1
|
|
341
|
+
@entities = []
|
|
342
|
+
@services = []
|
|
343
|
+
@handled = [] # subscriptions with a block
|
|
344
|
+
@timers = []
|
|
345
|
+
@key = "#{LIVELINESS_ROOT}/#{@domain}/#{@zid}/#{@nid}/#{@nid}/NN/" \
|
|
346
|
+
"#{::Asterism::ROS.mangle(@enclave)}/#{::Asterism::ROS.mangle(@namespace)}/#{@name}"
|
|
347
|
+
@token = session.liveliness(@key)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def publisher(topic, type, qos: DEFAULT_QOS)
|
|
351
|
+
e = ::Asterism::ROS::Publisher.new(self, @session, topic, ::Asterism::ROS.type_of(type), qos)
|
|
352
|
+
@entities << e
|
|
353
|
+
e
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def subscription(topic, type, qos: DEFAULT_QOS, depth: 16)
|
|
357
|
+
e = ::Asterism::ROS::Subscription.new(self, @session, topic, ::Asterism::ROS.type_of(type), qos, depth)
|
|
358
|
+
@entities << e
|
|
359
|
+
e
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Serves `service` (a name) with the block: it gets the request (an
|
|
363
|
+
# object of type::Request) and returns the response (a type::Response
|
|
364
|
+
# or a Hash of its fields). The block runs from node.poll (or
|
|
365
|
+
# service.handle_pending), never behind the application's back.
|
|
366
|
+
def service(service, type, qos: DEFAULT_QOS, depth: 8, &handler)
|
|
367
|
+
raise ArgumentError, "service needs a block" unless handler
|
|
368
|
+
e = ::Asterism::ROS::Service.new(self, @session, service, ::Asterism::ROS.type_of(type), qos, depth, handler)
|
|
369
|
+
@entities << e
|
|
370
|
+
@services << e
|
|
371
|
+
e
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def client(service, type, qos: DEFAULT_QOS)
|
|
375
|
+
e = ::Asterism::ROS::Client.new(self, @session, service, ::Asterism::ROS.type_of(type), qos)
|
|
376
|
+
@entities << e
|
|
377
|
+
e
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Subscribes and gives each message (and its Attachment, or nil) to
|
|
381
|
+
# the block, from node.poll. Returns the Subscription (close ends it).
|
|
382
|
+
# The block runs on the update loop's stack: do not wait in it (no
|
|
383
|
+
# service call, no proxy call); start a call_async and look at it
|
|
384
|
+
# later instead.
|
|
385
|
+
def subscribe(topic, type, qos: DEFAULT_QOS, depth: 16, &handler)
|
|
386
|
+
raise ArgumentError, "subscribe needs a block (or use subscription)" unless handler
|
|
387
|
+
e = subscription(topic, type, qos: qos, depth: depth)
|
|
388
|
+
e.handler = handler
|
|
389
|
+
@handled << e
|
|
390
|
+
e
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Calls the block every `seconds` (the first time one period from now),
|
|
394
|
+
# from node.poll. Returns a Timer (cancel). It keeps the period; after
|
|
395
|
+
# a long stall it starts again from now instead of firing the missed
|
|
396
|
+
# times at once. As for subscribe: do not wait in the block.
|
|
397
|
+
def every(seconds, &blk)
|
|
398
|
+
raise ArgumentError, "every needs a block" unless blk
|
|
399
|
+
t = ::Asterism::ROS::Timer.new(self, seconds, blk)
|
|
400
|
+
@timers << t
|
|
401
|
+
t
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def cancel_timer(timer)
|
|
405
|
+
@timers.delete(timer)
|
|
406
|
+
nil
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# node.call("/add_two_ints", AddTwoInts, a: 1, b: 2) -> response.
|
|
410
|
+
# A client per service name is made on first use and kept.
|
|
411
|
+
def call(service, type, request = nil, timeout_ms: ::Asterism::ROS::Client::DEFAULT_TIMEOUT_MS, **fields)
|
|
412
|
+
@clients ||= {}
|
|
413
|
+
c = @clients[service]
|
|
414
|
+
if c.nil? || c.closed?
|
|
415
|
+
c = client(service, type)
|
|
416
|
+
@clients[service] = c
|
|
417
|
+
end
|
|
418
|
+
c.call(request, timeout_ms: timeout_ms, **fields)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Polls the session (Asterism::Zenoh::Session#poll), answers the
|
|
422
|
+
# requests that came in for this node's services, then gives the
|
|
423
|
+
# messages that came in to the subscribe blocks and fires the timers
|
|
424
|
+
# that are due. Returns what session.poll returns (false once the
|
|
425
|
+
# session is closed).
|
|
426
|
+
def poll(steps = 8)
|
|
427
|
+
ok = pump(steps)
|
|
428
|
+
deliver
|
|
429
|
+
fire_timers
|
|
430
|
+
ok
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# What a waiting service call polls: the session and the services, not
|
|
434
|
+
# the subscribe / every blocks (those run from node.poll only).
|
|
435
|
+
def pump(steps = 8)
|
|
436
|
+
ok = @session.poll(steps)
|
|
437
|
+
i = 0
|
|
438
|
+
while i < @services.size
|
|
439
|
+
@services[i].handle_pending
|
|
440
|
+
i += 1
|
|
441
|
+
end
|
|
442
|
+
ok
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# While loops, not each with a block: the application's blocks are
|
|
446
|
+
# called from here, and nothing else should stand between them and
|
|
447
|
+
# the update loop on the stack.
|
|
448
|
+
def deliver
|
|
449
|
+
i = 0
|
|
450
|
+
while i < @handled.size
|
|
451
|
+
sub = @handled[i]
|
|
452
|
+
if sub.closed?
|
|
453
|
+
@handled.delete_at(i)
|
|
454
|
+
next
|
|
455
|
+
end
|
|
456
|
+
got = sub.each_pending
|
|
457
|
+
j = 0
|
|
458
|
+
while j < got.size
|
|
459
|
+
handle(sub, got[j][0], got[j][1])
|
|
460
|
+
j += 1
|
|
461
|
+
end
|
|
462
|
+
i += 1
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def fire_timers
|
|
467
|
+
return if @timers.empty?
|
|
468
|
+
now = ::Asterism::ROS.now_ms
|
|
469
|
+
due = []
|
|
470
|
+
i = 0
|
|
471
|
+
while i < @timers.size
|
|
472
|
+
due << @timers[i] if @timers[i].due?(now)
|
|
473
|
+
i += 1
|
|
474
|
+
end
|
|
475
|
+
i = 0
|
|
476
|
+
while i < due.size
|
|
477
|
+
fire(due[i], now)
|
|
478
|
+
i += 1
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# One message for one subscribe block, one timer firing (CRuby routes
|
|
483
|
+
# what they raise to on_error).
|
|
484
|
+
def handle(sub, msg, info)
|
|
485
|
+
sub.handler.call(msg, info)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def fire(timer, now)
|
|
489
|
+
timer.fire(now)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Withdraws the node and its publishers, subscriptions, services and
|
|
493
|
+
# clients. Idempotent.
|
|
494
|
+
def close
|
|
495
|
+
@entities.each { |e| e.close }
|
|
496
|
+
@entities = []
|
|
497
|
+
@services = []
|
|
498
|
+
@handled = []
|
|
499
|
+
@timers = []
|
|
500
|
+
@clients = {}
|
|
501
|
+
@token.close if @token
|
|
502
|
+
@token = nil
|
|
503
|
+
nil
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def closed?
|
|
507
|
+
@token.nil?
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# For the entities: [data key, liveliness token key] of a topic.
|
|
511
|
+
def entity_keys(kind, topic, type, qos)
|
|
512
|
+
full = ::Asterism::ROS.resolve(topic, @namespace)
|
|
513
|
+
data = "#{@domain}/#{::Asterism::ROS.strip_slashes(full)}/#{type::TYPE_NAME}/#{type::TYPE_HASH}"
|
|
514
|
+
id = @next_id
|
|
515
|
+
@next_id += 1
|
|
516
|
+
token = "#{LIVELINESS_ROOT}/#{@domain}/#{@zid}/#{@nid}/#{id}/#{kind}/" \
|
|
517
|
+
"#{::Asterism::ROS.mangle(@enclave)}/#{::Asterism::ROS.mangle(@namespace)}/#{@name}/" \
|
|
518
|
+
"#{::Asterism::ROS.mangle(full)}/#{type::TYPE_NAME}/#{type::TYPE_HASH}/#{qos}"
|
|
519
|
+
[data, token]
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
class Publisher
|
|
524
|
+
attr_reader :topic_key, :token_key, :gid, :sequence
|
|
525
|
+
|
|
526
|
+
def initialize(node, session, topic, type, qos)
|
|
527
|
+
@session = session
|
|
528
|
+
@type = type
|
|
529
|
+
@topic_key, @token_key = node.entity_keys("MP", topic, type, qos)
|
|
530
|
+
@gid = ::Asterism::ROS.gid_for(@token_key)
|
|
531
|
+
@sequence = 0
|
|
532
|
+
@token = session.liveliness(@token_key)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# msg: a message of the type, or a Hash of its fields (missing ones
|
|
536
|
+
# take their defaults).
|
|
537
|
+
def publish(msg)
|
|
538
|
+
raise ::Asterism::Zenoh::Error, "publisher closed" if @token.nil?
|
|
539
|
+
# Read once (a second thread may publish meanwhile on CRuby).
|
|
540
|
+
seq = @sequence + 1
|
|
541
|
+
@sequence = seq
|
|
542
|
+
att = ::Asterism::ROS::Attachment.new(seq, ::Asterism::ROS.now_ns, @gid)
|
|
543
|
+
@session.put(@topic_key, @type.encode(msg), attachment: att.encode)
|
|
544
|
+
nil
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# pub << { linear: { x: 0.1 } }
|
|
548
|
+
def <<(msg)
|
|
549
|
+
publish(msg)
|
|
550
|
+
self
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def close
|
|
554
|
+
@token.close if @token
|
|
555
|
+
@token = nil
|
|
556
|
+
nil
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
class Subscription
|
|
561
|
+
attr_reader :topic_key, :token_key, :errors
|
|
562
|
+
# The block of node.subscribe (nil for node.subscription).
|
|
563
|
+
attr_accessor :handler
|
|
564
|
+
|
|
565
|
+
def initialize(node, session, topic, type, qos, depth)
|
|
566
|
+
@type = type
|
|
567
|
+
@topic_key, @token_key = node.entity_keys("MS", topic, type, qos)
|
|
568
|
+
@errors = 0
|
|
569
|
+
@sub = session.subscribe(@topic_key, depth)
|
|
570
|
+
@token = session.liveliness(@token_key)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Yields each message received so far (decoded) and its Attachment (nil
|
|
574
|
+
# when the sample had none). Samples that do not decode are counted in
|
|
575
|
+
# errors and skipped. Returns the number yielded; without a block, an
|
|
576
|
+
# Array of [msg, attachment].
|
|
577
|
+
def each_pending
|
|
578
|
+
out = []
|
|
579
|
+
@sub.each_pending.each do |entry|
|
|
580
|
+
msg = begin
|
|
581
|
+
@type.decode(entry[1])
|
|
582
|
+
rescue ::Asterism::CDR::DecodeError
|
|
583
|
+
@errors += 1
|
|
584
|
+
nil
|
|
585
|
+
end
|
|
586
|
+
next if msg.nil?
|
|
587
|
+
out << [msg, ::Asterism::ROS::Attachment.decode(entry[2])]
|
|
588
|
+
end
|
|
589
|
+
return out unless block_given?
|
|
590
|
+
out.each { |m| yield m[0], m[1] }
|
|
591
|
+
out.size
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def dropped
|
|
595
|
+
@sub.dropped
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def close
|
|
599
|
+
@sub.close
|
|
600
|
+
@token.close if @token
|
|
601
|
+
@token = nil
|
|
602
|
+
nil
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def closed?
|
|
606
|
+
@token.nil?
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# node.every: a block called every period, from node.poll.
|
|
611
|
+
class Timer
|
|
612
|
+
# period: seconds as given; fired: how many times it ran.
|
|
613
|
+
attr_reader :period, :fired
|
|
614
|
+
|
|
615
|
+
def initialize(node, seconds, blk)
|
|
616
|
+
@period_ms = (seconds * 1000).to_i
|
|
617
|
+
raise ArgumentError, "the period must be positive" unless @period_ms > 0
|
|
618
|
+
@node = node
|
|
619
|
+
@period = seconds
|
|
620
|
+
@blk = blk
|
|
621
|
+
@next = ::Asterism::ROS.now_ms + @period_ms
|
|
622
|
+
@fired = 0
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def due?(now_ms)
|
|
626
|
+
now_ms >= @next
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Keeps the period without drifting; after a long stall it starts
|
|
630
|
+
# again from now instead of firing the missed times at once.
|
|
631
|
+
def fire(now_ms)
|
|
632
|
+
@next += @period_ms
|
|
633
|
+
@next = now_ms + @period_ms if @next <= now_ms
|
|
634
|
+
@fired += 1
|
|
635
|
+
@blk.call
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def cancel
|
|
639
|
+
@node.cancel_timer(self)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def inspect
|
|
643
|
+
"#<Asterism::ROS::Timer every #{@period} s>"
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
module Asterism
|
|
650
|
+
module ROS
|
|
651
|
+
# The server side of a service (node.service). A complete queryable on
|
|
652
|
+
# the service key, plus the SS token.
|
|
653
|
+
class Service
|
|
654
|
+
attr_reader :service_key, :token_key, :handled, :errors
|
|
655
|
+
|
|
656
|
+
def initialize(node, session, service, type, qos, depth, handler)
|
|
657
|
+
@type = type
|
|
658
|
+
@handler = handler
|
|
659
|
+
@service_key, @token_key = node.entity_keys("SS", service, type, qos)
|
|
660
|
+
@handled = 0
|
|
661
|
+
@errors = 0
|
|
662
|
+
@queryable = session.queryable(@service_key, depth, complete: true)
|
|
663
|
+
@token = session.liveliness(@token_key)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Answers the requests waiting now with the block given to
|
|
667
|
+
# node.service. A request that cannot be decoded, or has no attachment
|
|
668
|
+
# (rmw_zenoh clients always send one), is counted in errors and
|
|
669
|
+
# finished without an answer. An exception from the block finishes
|
|
670
|
+
# that request without an answer and is raised from here. Returns the
|
|
671
|
+
# number answered.
|
|
672
|
+
def handle_pending
|
|
673
|
+
return 0 if @token.nil?
|
|
674
|
+
n = 0
|
|
675
|
+
# The Array form: no block called from C (stack depth, see README).
|
|
676
|
+
qs = @queryable.each_pending
|
|
677
|
+
begin
|
|
678
|
+
qs.each do |q|
|
|
679
|
+
n += 1 if answer(q)
|
|
680
|
+
q.finish
|
|
681
|
+
end
|
|
682
|
+
ensure
|
|
683
|
+
# Also the ones after a request whose block raised (finish is
|
|
684
|
+
# idempotent; an unanswered query just ends for the client).
|
|
685
|
+
qs.each { |q| q.finish }
|
|
686
|
+
end
|
|
687
|
+
n
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def close
|
|
691
|
+
@queryable.close
|
|
692
|
+
@token.close if @token
|
|
693
|
+
@token = nil
|
|
694
|
+
nil
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def closed?
|
|
698
|
+
@token.nil?
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
private
|
|
702
|
+
|
|
703
|
+
def answer(q)
|
|
704
|
+
info = ::Asterism::ROS::Attachment.decode(q.attachment)
|
|
705
|
+
req = nil
|
|
706
|
+
begin
|
|
707
|
+
req = @type::Request.decode(q.payload) if info
|
|
708
|
+
rescue ::Asterism::CDR::DecodeError
|
|
709
|
+
req = nil
|
|
710
|
+
end
|
|
711
|
+
if req.nil?
|
|
712
|
+
@errors += 1
|
|
713
|
+
return false
|
|
714
|
+
end
|
|
715
|
+
res = @handler.call(req)
|
|
716
|
+
# The client's sequence number and GID go back with the reply.
|
|
717
|
+
att = ::Asterism::ROS::Attachment.new(info.sequence, ::Asterism::ROS.now_ns, info.gid)
|
|
718
|
+
q.reply(@service_key, @type::Response.encode(res), attachment: att.encode)
|
|
719
|
+
@handled += 1
|
|
720
|
+
true
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# A call on its way (client.call_async). done? never waits (node.poll
|
|
725
|
+
# moves it on); value waits, polling the node's session.
|
|
726
|
+
class Call
|
|
727
|
+
attr_reader :sequence, :response, :took_ms
|
|
728
|
+
|
|
729
|
+
def initialize(client, get, sequence, timeout_ms)
|
|
730
|
+
@client = client
|
|
731
|
+
@get = get
|
|
732
|
+
@sequence = sequence
|
|
733
|
+
@timeout_ms = timeout_ms
|
|
734
|
+
@started = ::Asterism::ROS.now_ms
|
|
735
|
+
@deadline = @started + timeout_ms
|
|
736
|
+
@response = nil
|
|
737
|
+
@finished = false
|
|
738
|
+
@took_ms = nil
|
|
739
|
+
@nobody = false
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# True once the response came, the time ran out, or no server answered.
|
|
743
|
+
def done?
|
|
744
|
+
collect
|
|
745
|
+
@finished
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# The response (type::Response); waits until done. Raises
|
|
749
|
+
# Asterism::ROS::Timeout when there was none.
|
|
750
|
+
def value
|
|
751
|
+
@client.wait_for(self) unless done?
|
|
752
|
+
if @response.nil?
|
|
753
|
+
raise ::Asterism::Zenoh::Error, "session is closed" if @client.session_closed?
|
|
754
|
+
if @nobody
|
|
755
|
+
raise ::Asterism::ROS::Timeout, "no answer from #{@client.service_name} (nobody serves it)"
|
|
756
|
+
end
|
|
757
|
+
raise ::Asterism::ROS::Timeout, "no answer from #{@client.service_name} within #{@timeout_ms} ms"
|
|
758
|
+
end
|
|
759
|
+
@response
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def collect
|
|
763
|
+
return if @finished
|
|
764
|
+
g = @get
|
|
765
|
+
# done? before taking the replies, as in Asterism::Future#collect.
|
|
766
|
+
ended = g.done?
|
|
767
|
+
# The Array form, as in Asterism::Future (no block called from C).
|
|
768
|
+
g.each_reply.each do |r|
|
|
769
|
+
next unless @response.nil?
|
|
770
|
+
info = ::Asterism::ROS::Attachment.decode(r[2])
|
|
771
|
+
# rmw_zenoh pairs the reply by the sequence number it carries back.
|
|
772
|
+
next if info.nil? || info.sequence != @sequence
|
|
773
|
+
begin
|
|
774
|
+
@response = @client.decode_response(r[1])
|
|
775
|
+
rescue ::Asterism::CDR::DecodeError
|
|
776
|
+
@response = nil
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
now = ::Asterism::ROS.now_ms
|
|
780
|
+
if !@response.nil? || ended || now >= @deadline
|
|
781
|
+
@nobody = @response.nil? && now < @deadline
|
|
782
|
+
@finished = true
|
|
783
|
+
@took_ms = now - @started
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# The client side of a service (node.client). Sends each request as a
|
|
789
|
+
# get on the service key, plus the SC token.
|
|
790
|
+
class Client
|
|
791
|
+
DEFAULT_TIMEOUT_MS = 2000
|
|
792
|
+
# Pause between polls while a call waits (ms).
|
|
793
|
+
WAIT_STEP_MS = 2
|
|
794
|
+
|
|
795
|
+
attr_reader :service_key, :token_key, :gid, :sequence, :service_name
|
|
796
|
+
|
|
797
|
+
def initialize(node, session, service, type, qos)
|
|
798
|
+
@node = node
|
|
799
|
+
@session = session
|
|
800
|
+
@type = type
|
|
801
|
+
@service_name = ::Asterism::ROS.resolve(service, node.namespace)
|
|
802
|
+
@service_key, @token_key = node.entity_keys("SC", service, type, qos)
|
|
803
|
+
@gid = ::Asterism::ROS.gid_for(@token_key)
|
|
804
|
+
@sequence = 0
|
|
805
|
+
@token = session.liveliness(@token_key)
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# Sends the request and returns a Call at once. request: a
|
|
809
|
+
# type::Request, a Hash, or the fields as keywords.
|
|
810
|
+
def call_async(request = nil, timeout_ms: DEFAULT_TIMEOUT_MS, **fields)
|
|
811
|
+
raise ::Asterism::Zenoh::Error, "client closed" if @token.nil?
|
|
812
|
+
req = request.nil? ? fields : request
|
|
813
|
+
payload = @type::Request.encode(req)
|
|
814
|
+
# Read once into a local: the request and its Call carry the same
|
|
815
|
+
# number even when another thread (CRuby) calls through this client
|
|
816
|
+
# meanwhile.
|
|
817
|
+
seq = @sequence + 1
|
|
818
|
+
@sequence = seq
|
|
819
|
+
att = ::Asterism::ROS::Attachment.new(seq, ::Asterism::ROS.now_ns, @gid)
|
|
820
|
+
g = @session.get(@service_key, timeout_ms, nil, payload, attachment: att.encode,
|
|
821
|
+
target: :all_complete, consolidation: :none)
|
|
822
|
+
::Asterism::ROS::Call.new(self, g, seq, timeout_ms)
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Sends the request and waits for the response (polling the node, so
|
|
826
|
+
# this node's services keep answering meanwhile). Raises
|
|
827
|
+
# Asterism::ROS::Timeout when no response came in time.
|
|
828
|
+
def call(request = nil, timeout_ms: DEFAULT_TIMEOUT_MS, **fields)
|
|
829
|
+
call_async(request, timeout_ms: timeout_ms, **fields).value
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
# Polls the node's session and services (not its subscribe / every
|
|
833
|
+
# blocks) until the call is done.
|
|
834
|
+
def wait_for(c)
|
|
835
|
+
until c.done?
|
|
836
|
+
break unless @node.pump
|
|
837
|
+
break if c.done?
|
|
838
|
+
::Asterism::ROS.pause(WAIT_STEP_MS)
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def session_closed?
|
|
843
|
+
@session.closed?
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def decode_response(bytes)
|
|
847
|
+
@type::Response.decode(bytes)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def close
|
|
851
|
+
@token.close if @token
|
|
852
|
+
@token = nil
|
|
853
|
+
nil
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def closed?
|
|
857
|
+
@token.nil?
|
|
858
|
+
end
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
end
|