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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# The receiving loop behind the block API of the CRuby layer (CRuby only;
|
|
2
|
+
# the boards poll from their update loop instead).
|
|
3
|
+
#
|
|
4
|
+
# A Runner belongs to one Zenoh session. It holds tasks: small procs that
|
|
5
|
+
# take what has arrived (each_pending of the shared API) and hand it to the
|
|
6
|
+
# application's blocks. Every round of tasks (a tick) runs with the runner's
|
|
7
|
+
# lock held, so the shared, single-threaded layers are never entered by two
|
|
8
|
+
# threads at once. zenoh-c's own threads never call Ruby: they only fill the
|
|
9
|
+
# channels the tasks empty.
|
|
10
|
+
#
|
|
11
|
+
# runner.start ticks on a thread of its own (until stop or close)
|
|
12
|
+
# runner.run ticks on the calling thread (until stop, close or Ctrl-C)
|
|
13
|
+
# runner.stop ends either; joins the thread unless called from it
|
|
14
|
+
#
|
|
15
|
+
# A thread that waits for an answer while the runner ticks (proxy calls,
|
|
16
|
+
# ROS service calls) does not poll by itself: it waits on the runner's
|
|
17
|
+
# condition variable, and checks after each tick whether its answer came
|
|
18
|
+
# (wait_until). The runner's own thread, and any thread while the runner
|
|
19
|
+
# does not tick, poll as the shared layers always do.
|
|
20
|
+
require "monitor"
|
|
21
|
+
|
|
22
|
+
module Asterism
|
|
23
|
+
class Runner
|
|
24
|
+
# Pause between ticks when nothing arrived (seconds).
|
|
25
|
+
DEFAULT_INTERVAL = 0.002
|
|
26
|
+
# Longest a waiting thread sleeps before it checks again on its own.
|
|
27
|
+
WAIT_SLICE = 0.05
|
|
28
|
+
|
|
29
|
+
@registry = {}.compare_by_identity
|
|
30
|
+
@registry_lock = Mutex.new
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# The runner of a Zenoh session (nil when it has none).
|
|
34
|
+
def for(session)
|
|
35
|
+
@registry_lock.synchronize { @registry[session] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register(session, runner)
|
|
39
|
+
@registry_lock.synchronize { @registry[session] = runner }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def unregister(session, runner)
|
|
43
|
+
@registry_lock.synchronize { @registry.delete(session) if @registry[session].equal?(runner) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :lock, :session
|
|
48
|
+
|
|
49
|
+
# session: the Asterism::Zenoh::Session the tasks read from. lock: the
|
|
50
|
+
# Monitor guarding the layer the tasks call into (the object layer has a
|
|
51
|
+
# process-wide one). on_closed is called once, on the ticking thread,
|
|
52
|
+
# when the session is found closed.
|
|
53
|
+
def initialize(session, lock: Monitor.new, interval: DEFAULT_INTERVAL, name: "asterism")
|
|
54
|
+
@session = session
|
|
55
|
+
@lock = lock
|
|
56
|
+
@cond = @lock.new_cond
|
|
57
|
+
@interval = interval
|
|
58
|
+
@name = name
|
|
59
|
+
@tasks = []
|
|
60
|
+
@error_handler = nil
|
|
61
|
+
@thread = nil
|
|
62
|
+
@ticking = nil # the thread inside start / run
|
|
63
|
+
@stop = false
|
|
64
|
+
@closed = false
|
|
65
|
+
self.class.register(session, self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Adds a task: a proc called every tick with the lock held. It returns
|
|
69
|
+
# the number of things it handled (0 or nil when nothing came), and
|
|
70
|
+
# false once the session is found closed. Returns the task, for remove.
|
|
71
|
+
def add(task = nil, &blk)
|
|
72
|
+
t = task || blk
|
|
73
|
+
@lock.synchronize { @tasks << t }
|
|
74
|
+
t
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def remove(task)
|
|
78
|
+
@lock.synchronize { @tasks.delete(task) }
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# on_error { |error, where| }: errors raised by the application's blocks
|
|
83
|
+
# (StandardError) go here instead of stopping the runner. Without a
|
|
84
|
+
# handler they are printed with warn. where: what the block was for
|
|
85
|
+
# (e.g. "subscribe demo/**").
|
|
86
|
+
def on_error(&blk)
|
|
87
|
+
@error_handler = blk
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Runs the application's block; reports what it raises instead of
|
|
92
|
+
# letting it out. Returns the block's value, or nil when it raised.
|
|
93
|
+
def guard(where)
|
|
94
|
+
yield
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
report(e, where)
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def report(error, where)
|
|
101
|
+
if @error_handler
|
|
102
|
+
begin
|
|
103
|
+
@error_handler.call(error, where)
|
|
104
|
+
rescue StandardError => e2
|
|
105
|
+
warn "asterism: on_error raised #{e2.class}: #{e2.message}"
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
warn "asterism: #{where}: #{error.class}: #{error.message}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# One round of the tasks. Returns the number of things handled, or nil
|
|
113
|
+
# once the session is closed.
|
|
114
|
+
def tick
|
|
115
|
+
n = 0
|
|
116
|
+
closed = false
|
|
117
|
+
@lock.synchronize do
|
|
118
|
+
begin
|
|
119
|
+
@tasks.dup.each do |t|
|
|
120
|
+
r = begin
|
|
121
|
+
t.call
|
|
122
|
+
rescue ::Asterism::Zenoh::Error
|
|
123
|
+
false # the session went away under the task
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
report(e, "receiver")
|
|
126
|
+
0
|
|
127
|
+
end
|
|
128
|
+
if r == false
|
|
129
|
+
closed = true
|
|
130
|
+
break
|
|
131
|
+
end
|
|
132
|
+
n += r if r.is_a?(Integer)
|
|
133
|
+
end
|
|
134
|
+
ensure
|
|
135
|
+
@cond.broadcast
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
if closed
|
|
139
|
+
@closed = true
|
|
140
|
+
@stop = true
|
|
141
|
+
return nil
|
|
142
|
+
end
|
|
143
|
+
n
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Ticks on a thread of its own. Returns self. Does nothing when already
|
|
147
|
+
# ticking.
|
|
148
|
+
def start
|
|
149
|
+
@lock.synchronize do
|
|
150
|
+
return self if @ticking
|
|
151
|
+
@stop = false
|
|
152
|
+
@thread = Thread.new { loop_ticks }
|
|
153
|
+
@thread.name = "#{@name} receiver" if @thread.respond_to?(:name=)
|
|
154
|
+
@ticking = @thread
|
|
155
|
+
end
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Ticks on the calling thread until stop, the session closing, or
|
|
160
|
+
# Ctrl-C (which returns nil instead of raising). Raises Error when the
|
|
161
|
+
# runner already ticks on another thread.
|
|
162
|
+
def run
|
|
163
|
+
@lock.synchronize do
|
|
164
|
+
raise ::Asterism::Error, "already running (stop it first)" if @ticking
|
|
165
|
+
@stop = false
|
|
166
|
+
@ticking = Thread.current
|
|
167
|
+
end
|
|
168
|
+
begin
|
|
169
|
+
loop_ticks
|
|
170
|
+
rescue Interrupt
|
|
171
|
+
nil
|
|
172
|
+
ensure
|
|
173
|
+
@lock.synchronize do
|
|
174
|
+
@ticking = nil if @ticking.equal?(Thread.current)
|
|
175
|
+
@cond.broadcast
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Ends start / run after the current tick. From another thread it waits
|
|
182
|
+
# for the receiving thread to end (unless that thread is inside a tick
|
|
183
|
+
# this thread is waiting for). Idempotent.
|
|
184
|
+
def stop
|
|
185
|
+
@stop = true
|
|
186
|
+
th = @thread
|
|
187
|
+
if th && !th.equal?(Thread.current) && !@lock.mon_owned?
|
|
188
|
+
th.join
|
|
189
|
+
end
|
|
190
|
+
@lock.synchronize do
|
|
191
|
+
@thread = nil if @thread.equal?(th) && !(th && th.alive?)
|
|
192
|
+
@cond.broadcast
|
|
193
|
+
end
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Stops and forgets the session. The caller closes the session after.
|
|
198
|
+
def close
|
|
199
|
+
stop
|
|
200
|
+
self.class.unregister(@session, self)
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# True while start or run ticks.
|
|
205
|
+
def running?
|
|
206
|
+
t = @ticking
|
|
207
|
+
!t.nil? && t.alive? && !@stop
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# True on the thread that ticks.
|
|
211
|
+
def current?
|
|
212
|
+
@ticking.equal?(Thread.current)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def closed?
|
|
216
|
+
@closed
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# For a thread other than the ticking one: waits until the block is
|
|
220
|
+
# true, checking it after each tick with the lock held. Returns when the
|
|
221
|
+
# block is true, or when the runner stops (the caller then looks at its
|
|
222
|
+
# own state, e.g. a time limit or a closed session). The block also sees
|
|
223
|
+
# its own deadline: it is checked at least every WAIT_SLICE seconds.
|
|
224
|
+
def wait_until
|
|
225
|
+
@lock.synchronize do
|
|
226
|
+
until yield
|
|
227
|
+
break unless running?
|
|
228
|
+
@cond.wait(WAIT_SLICE)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def inspect
|
|
234
|
+
"#<Asterism::Runner #{@name} #{running? ? 'running' : 'stopped'}>"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def loop_ticks
|
|
240
|
+
until @stop
|
|
241
|
+
n = tick
|
|
242
|
+
break if n.nil?
|
|
243
|
+
sleep(@interval) if n == 0 && !@stop
|
|
244
|
+
end
|
|
245
|
+
ensure
|
|
246
|
+
@lock.synchronize do
|
|
247
|
+
@ticking = nil if @ticking.equal?(Thread.current)
|
|
248
|
+
@thread = nil if @thread.equal?(Thread.current)
|
|
249
|
+
@cond.broadcast
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Asterism::Zenoh with blocks, threads and Enumerators (CRuby only). The
|
|
2
|
+
# polled API of asterism-zenoh (Session, each_pending, each_reply) is the
|
|
3
|
+
# same on the boards and stays as it is; this is a layer on top of it.
|
|
4
|
+
#
|
|
5
|
+
# Asterism::Zenoh.open("tcp/192.0.2.2:7447") do |s|
|
|
6
|
+
# s.subscribe("home/**") { |sample| puts "#{sample.key} = #{sample.payload}" }
|
|
7
|
+
# s.queryable("home/pc/status") { |q| q.reply("ok") }
|
|
8
|
+
# s.put("home/pc/hello", "hi")
|
|
9
|
+
# s.get("home/**/status").each { |reply| p reply }
|
|
10
|
+
# s.subscribe("sensor/temp").each.lazy.map { _1.payload.to_f }.first(3)
|
|
11
|
+
# s.run # until Ctrl-C (or s.start / s.stop)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Blocks given to subscribe, queryable and liveliness_watch run on the
|
|
15
|
+
# session's receiving thread (start) or inside run; Enumerators (each, get)
|
|
16
|
+
# wait on the thread that iterates them. See Asterism::Runner.
|
|
17
|
+
module Asterism
|
|
18
|
+
module Zenoh
|
|
19
|
+
# Pause between looks while an Enumerator waits for data (seconds).
|
|
20
|
+
ENUM_STEP = 0.002
|
|
21
|
+
|
|
22
|
+
# A received sample. payload and attachment are binary Strings (the
|
|
23
|
+
# attachment nil when there was none); text is the payload as UTF-8.
|
|
24
|
+
Sample = Data.define(:key, :payload, :attachment) do
|
|
25
|
+
def text
|
|
26
|
+
payload.dup.force_encoding(Encoding::UTF_8)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
"#{key}: #{payload}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A reply to a get: the key it was sent with, payload and attachment.
|
|
35
|
+
Reply = Data.define(:key, :payload, :attachment) do
|
|
36
|
+
def text
|
|
37
|
+
payload.dup.force_encoding(Encoding::UTF_8)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
"#{key}: #{payload}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A liveliness change: key of the token, and whether it appeared.
|
|
46
|
+
Liveliness = Data.define(:key, :alive) do
|
|
47
|
+
alias_method :alive?, :alive
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Added to the queries a Connection's queryable hands out: reply with
|
|
51
|
+
# only a payload answers on the queryable's own key (when it has no
|
|
52
|
+
# wildcard) instead of the query's, which may be a pattern; payloads
|
|
53
|
+
# that are not Strings are sent as to_s.
|
|
54
|
+
module QueryReply
|
|
55
|
+
def reply(*args, **kw)
|
|
56
|
+
args[-1] = args[-1].to_s unless args.empty? || args[-1].is_a?(String)
|
|
57
|
+
args.unshift(@asterism_key) if args.size == 1 && @asterism_key
|
|
58
|
+
super(*args, **kw)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The queryable's key when it names one key (no wildcard).
|
|
63
|
+
def self.plain_key(key)
|
|
64
|
+
key.include?("*") || key.include?("$") ? nil : key
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.prepare_query(q, key)
|
|
68
|
+
q.extend(QueryReply)
|
|
69
|
+
q.instance_variable_set(:@asterism_key, key)
|
|
70
|
+
q
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Opens a session (the arguments of Session.open) and wraps it in a
|
|
74
|
+
# Connection. With a block: yields it, closes it when the block ends
|
|
75
|
+
# (also on an exception) and returns the block's value.
|
|
76
|
+
def self.open(locator = nil, interval: Runner::DEFAULT_INTERVAL, **opts)
|
|
77
|
+
conn = Connection.new(Session.open(locator, **opts), interval: interval)
|
|
78
|
+
return conn unless block_given?
|
|
79
|
+
begin
|
|
80
|
+
yield conn
|
|
81
|
+
ensure
|
|
82
|
+
conn.close
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Seconds (Float) or nil -> milliseconds for the polled API.
|
|
87
|
+
def self.ms(seconds, default_ms)
|
|
88
|
+
return default_ms if seconds.nil?
|
|
89
|
+
(seconds * 1000).round
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Waits on the calling thread until a sample comes into sub (an
|
|
93
|
+
# Asterism::Zenoh::Subscriber or LivelinessWatch), yielding each.
|
|
94
|
+
# Ends when sub or the session closes, or after timeout seconds.
|
|
95
|
+
def self.drain(sub, session, timeout)
|
|
96
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
97
|
+
loop do
|
|
98
|
+
got = sub.each_pending
|
|
99
|
+
got.each { |e| yield e }
|
|
100
|
+
break if sub.closed? || session.closed?
|
|
101
|
+
break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
102
|
+
sleep(ENUM_STEP) if got.empty?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A session with the block API. The polled calls of the session are
|
|
107
|
+
# still there (session, or the methods without a block).
|
|
108
|
+
class Connection
|
|
109
|
+
attr_reader :session, :runner
|
|
110
|
+
|
|
111
|
+
def initialize(session, interval: Runner::DEFAULT_INTERVAL)
|
|
112
|
+
@session = session
|
|
113
|
+
@runner = Runner.new(session, interval: interval, name: "zenoh")
|
|
114
|
+
# The first task: false (the runner stops) once the session closed.
|
|
115
|
+
@runner.add { @session.poll ? 0 : false }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def put(key, payload, attachment: nil)
|
|
119
|
+
@session.put(key, payload.is_a?(String) ? payload : payload.to_s, attachment: attachment)
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# With a block: every sample is given to it (as a Sample) on the
|
|
124
|
+
# receiving thread; returns the Subscription (close it to stop).
|
|
125
|
+
# Without: a Subscription to take from (each, each_pending).
|
|
126
|
+
def subscribe(key, depth: 16, &blk)
|
|
127
|
+
sub = Subscription.new(self, @session.subscribe(key, depth), key)
|
|
128
|
+
sub.handle(&blk) if blk
|
|
129
|
+
sub
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# With a block: each query is given to it on the receiving thread and
|
|
133
|
+
# finished when it returns (reply in the block with q.reply). Without:
|
|
134
|
+
# a Queryable whose each yields the queries (finished after each).
|
|
135
|
+
def queryable(key, depth: 16, complete: false, &blk)
|
|
136
|
+
qa = Queryable.new(self, @session.queryable(key, depth, complete: complete), key)
|
|
137
|
+
qa.handle(&blk) if blk
|
|
138
|
+
qa
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The replies to a get: yields each Reply until every answer came or
|
|
142
|
+
# timeout (seconds) ran out, and returns their number. Without a
|
|
143
|
+
# block, an Enumerator (each iteration sends the get again).
|
|
144
|
+
def get(key, timeout: 2.0, params: nil, payload: nil, attachment: nil, target: :all,
|
|
145
|
+
consolidation: :none, &blk)
|
|
146
|
+
args = [key, timeout, params, payload, attachment, target, consolidation]
|
|
147
|
+
return enum_for(:get_each, *args) unless blk
|
|
148
|
+
get_each(*args, &blk)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A liveliness token (close it to withdraw).
|
|
152
|
+
def liveliness(key)
|
|
153
|
+
@session.liveliness(key)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# With a block: yields key and alive (true when the token appeared) on
|
|
157
|
+
# the receiving thread for each change; the tokens alive now come
|
|
158
|
+
# first. Without: a Watch whose each yields Liveliness values.
|
|
159
|
+
def liveliness_watch(key, depth: 16, &blk)
|
|
160
|
+
w = Watch.new(self, @session.liveliness_watch(key, depth), key)
|
|
161
|
+
w.handle(&blk) if blk
|
|
162
|
+
w
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The keys of the liveliness tokens alive now (an Array; waits for
|
|
166
|
+
# the answers, at most timeout seconds).
|
|
167
|
+
def liveliness_get(key, timeout: 2.0)
|
|
168
|
+
g = @session.liveliness_get(key, Zenoh.ms(timeout, 2000))
|
|
169
|
+
keys = []
|
|
170
|
+
loop do
|
|
171
|
+
g.each_reply.each { |r| keys << r[0] }
|
|
172
|
+
break if g.done?
|
|
173
|
+
sleep(ENUM_STEP)
|
|
174
|
+
end
|
|
175
|
+
g.each_reply.each { |r| keys << r[0] }
|
|
176
|
+
keys
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Receives on a thread of its own (blocks run there). Returns self.
|
|
180
|
+
def start
|
|
181
|
+
@runner.start
|
|
182
|
+
self
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Stops the receiving thread (or ends run).
|
|
186
|
+
def stop
|
|
187
|
+
@runner.stop
|
|
188
|
+
self
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Receives on this thread until stop, the connection closing, or
|
|
192
|
+
# Ctrl-C. Returns nil.
|
|
193
|
+
def run
|
|
194
|
+
@runner.run
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def running?
|
|
198
|
+
@runner.running?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# on_error { |error, where| }: what the blocks raise. Without a
|
|
202
|
+
# handler it is printed (warn); the receiving goes on either way.
|
|
203
|
+
def on_error(&blk)
|
|
204
|
+
@runner.on_error(&blk)
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def poll
|
|
209
|
+
@session.poll
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def closed?
|
|
213
|
+
@session.closed?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def zid
|
|
217
|
+
@session.zid
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def peers
|
|
221
|
+
@session.peers
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Stops receiving and closes the session (with everything declared on
|
|
225
|
+
# it). Idempotent.
|
|
226
|
+
def close
|
|
227
|
+
@runner.close
|
|
228
|
+
@session.close
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def inspect
|
|
233
|
+
"#<Asterism::Zenoh::Connection #{closed? ? 'closed' : zid}#{running? ? ' running' : ''}>"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def get_each(key, timeout, params, payload, attachment, target, consolidation)
|
|
239
|
+
g = @session.get(key, Zenoh.ms(timeout, 2000), params, payload,
|
|
240
|
+
attachment: attachment, target: target, consolidation: consolidation)
|
|
241
|
+
n = 0
|
|
242
|
+
loop do
|
|
243
|
+
got = g.each_reply
|
|
244
|
+
got.each do |r|
|
|
245
|
+
n += 1
|
|
246
|
+
yield Reply.new(r[0], r[1], r[2])
|
|
247
|
+
end
|
|
248
|
+
break if g.done? && g.pending == 0
|
|
249
|
+
sleep(ENUM_STEP) if got.empty?
|
|
250
|
+
end
|
|
251
|
+
n
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A subscription of a Connection. Either its block gets the samples
|
|
255
|
+
# (on the receiving thread), or the application takes them: each (an
|
|
256
|
+
# Enumerator that waits for samples) or each_pending (what is there
|
|
257
|
+
# now, as the polled API).
|
|
258
|
+
class Subscription
|
|
259
|
+
include Enumerable
|
|
260
|
+
|
|
261
|
+
attr_reader :key, :subscriber
|
|
262
|
+
|
|
263
|
+
def initialize(conn, subscriber, key)
|
|
264
|
+
@conn = conn
|
|
265
|
+
@subscriber = subscriber
|
|
266
|
+
@key = key
|
|
267
|
+
@task = nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def handle(&blk)
|
|
271
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
272
|
+
where = "subscribe #{@key}"
|
|
273
|
+
r = @conn.runner
|
|
274
|
+
@task = r.add do
|
|
275
|
+
got = @subscriber.each_pending
|
|
276
|
+
got.each { |e| r.guard(where) { blk.call(Sample.new(e[0], e[1], e[2])) } }
|
|
277
|
+
got.size
|
|
278
|
+
end
|
|
279
|
+
self
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Yields each Sample as it comes, waiting for the next; ends when the
|
|
283
|
+
# subscription or the session closes, or after timeout seconds.
|
|
284
|
+
# Without a block, an Enumerator (each.lazy.map { ... }.first(3)).
|
|
285
|
+
def each(timeout: nil, &blk)
|
|
286
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
287
|
+
raise ::Asterism::Error, "this subscription has a block; its samples go there" if @task
|
|
288
|
+
Zenoh.drain(@subscriber, @conn.session, timeout) { |e| blk.call(Sample.new(e[0], e[1], e[2])) }
|
|
289
|
+
self
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# The samples there now as Samples (an Array), or yields them.
|
|
293
|
+
def each_pending
|
|
294
|
+
got = @subscriber.each_pending.map { |e| Sample.new(e[0], e[1], e[2]) }
|
|
295
|
+
return got unless block_given?
|
|
296
|
+
got.each { |s| yield s }
|
|
297
|
+
got.size
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def pending = @subscriber.pending
|
|
301
|
+
def received = @subscriber.received
|
|
302
|
+
def dropped = @subscriber.dropped
|
|
303
|
+
def closed? = @subscriber.closed?
|
|
304
|
+
|
|
305
|
+
def close
|
|
306
|
+
@conn.runner.remove(@task) if @task
|
|
307
|
+
@task = nil
|
|
308
|
+
@subscriber.close
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def inspect
|
|
313
|
+
"#<Asterism::Zenoh::Connection::Subscription #{@key}#{@task ? ' (block)' : ''}>"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A queryable of a Connection: its block answers on the receiving
|
|
318
|
+
# thread, or each yields the queries (each finished after the block).
|
|
319
|
+
class Queryable
|
|
320
|
+
include Enumerable
|
|
321
|
+
|
|
322
|
+
attr_reader :key, :queryable
|
|
323
|
+
|
|
324
|
+
def initialize(conn, queryable, key)
|
|
325
|
+
@conn = conn
|
|
326
|
+
@queryable = queryable
|
|
327
|
+
@key = key
|
|
328
|
+
@task = nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def handle(&blk)
|
|
332
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
333
|
+
where = "queryable #{@key}"
|
|
334
|
+
own = Zenoh.plain_key(@key)
|
|
335
|
+
r = @conn.runner
|
|
336
|
+
@task = r.add do
|
|
337
|
+
qs = @queryable.each_pending
|
|
338
|
+
qs.each do |q|
|
|
339
|
+
r.guard(where) { blk.call(Zenoh.prepare_query(q, own)) }
|
|
340
|
+
ensure
|
|
341
|
+
q.finish
|
|
342
|
+
end
|
|
343
|
+
qs.size
|
|
344
|
+
end
|
|
345
|
+
self
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Yields each query (Asterism::Zenoh::Query: key, params, payload,
|
|
349
|
+
# attachment, reply; reply(payload) answers on the queryable's own
|
|
350
|
+
# key) as it comes and finishes it after the block.
|
|
351
|
+
# Ends when the queryable or the session closes, or after timeout.
|
|
352
|
+
def each(timeout: nil, &blk)
|
|
353
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
354
|
+
raise ::Asterism::Error, "this queryable has a block; its queries go there" if @task
|
|
355
|
+
own = Zenoh.plain_key(@key)
|
|
356
|
+
Zenoh.drain(@queryable, @conn.session, timeout) do |q|
|
|
357
|
+
blk.call(Zenoh.prepare_query(q, own))
|
|
358
|
+
ensure
|
|
359
|
+
q.finish
|
|
360
|
+
end
|
|
361
|
+
self
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def pending = @queryable.pending
|
|
365
|
+
def received = @queryable.received
|
|
366
|
+
def dropped = @queryable.dropped
|
|
367
|
+
def closed? = @queryable.closed?
|
|
368
|
+
|
|
369
|
+
def close
|
|
370
|
+
@conn.runner.remove(@task) if @task
|
|
371
|
+
@task = nil
|
|
372
|
+
@queryable.close
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# A liveliness watch of a Connection.
|
|
378
|
+
class Watch
|
|
379
|
+
include Enumerable
|
|
380
|
+
|
|
381
|
+
attr_reader :key, :watch
|
|
382
|
+
|
|
383
|
+
def initialize(conn, watch, key)
|
|
384
|
+
@conn = conn
|
|
385
|
+
@watch = watch
|
|
386
|
+
@key = key
|
|
387
|
+
@task = nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def handle(&blk)
|
|
391
|
+
raise ArgumentError, "a block is needed" unless blk
|
|
392
|
+
where = "liveliness_watch #{@key}"
|
|
393
|
+
r = @conn.runner
|
|
394
|
+
@task = r.add do
|
|
395
|
+
got = @watch.each_pending
|
|
396
|
+
got.each { |e| r.guard(where) { blk.call(e[0], e[1]) } }
|
|
397
|
+
got.size
|
|
398
|
+
end
|
|
399
|
+
self
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Yields each change as a Liveliness (key, alive), waiting for the
|
|
403
|
+
# next; the tokens alive now come first.
|
|
404
|
+
def each(timeout: nil, &blk)
|
|
405
|
+
return enum_for(:each, timeout: timeout) unless blk
|
|
406
|
+
raise ::Asterism::Error, "this watch has a block; its changes go there" if @task
|
|
407
|
+
Zenoh.drain(@watch, @conn.session, timeout) { |e| blk.call(Liveliness.new(e[0], e[1])) }
|
|
408
|
+
self
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def closed? = @watch.closed?
|
|
412
|
+
|
|
413
|
+
def close
|
|
414
|
+
@conn.runner.remove(@task) if @task
|
|
415
|
+
@task = nil
|
|
416
|
+
@watch.close
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# What CRuby needs on top of the pure Ruby layers in mrblib/ (shared with the
|
|
2
|
+
# mruby / PicoRuby boards). Kept small: anything that is also right on mruby
|
|
3
|
+
# belongs in mrblib/ instead.
|
|
4
|
+
module Asterism
|
|
5
|
+
# The message and service types bundled with the gem (the same files the
|
|
6
|
+
# boards keep in /usr/share/asterism/msgs). The application may add its
|
|
7
|
+
# own directories (tools/asterism_msggen.rb -o <dir>).
|
|
8
|
+
MSGS_DIR = File.expand_path("../../data/msgs", __dir__)
|
|
9
|
+
ROS::TYPE_PATH.replace([MSGS_DIR])
|
|
10
|
+
|
|
11
|
+
module Codec
|
|
12
|
+
# CRuby's msgpack packs binary (ASCII-8BIT) Strings, such as payloads
|
|
13
|
+
# that came from Zenoh, as the bin type. The mruby MessagePack of the
|
|
14
|
+
# boards reads only the str type, so every String goes as str
|
|
15
|
+
# (compatibility mode), which is what the boards send too.
|
|
16
|
+
def self.pack(v)
|
|
17
|
+
::MessagePack.pack(check(v), compatibility_mode: true)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A monotonic clock (deadlines and node.every must not jump with the
|
|
22
|
+
# wall clock).
|
|
23
|
+
def self.now_ms
|
|
24
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.error_reply(klass, message)
|
|
28
|
+
::MessagePack.pack(["error", klass.to_s, message.to_s], compatibility_mode: true)
|
|
29
|
+
end
|
|
30
|
+
end
|