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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/NOTICE +27 -0
  4. data/README.md +581 -3
  5. data/data/msgs/LICENSE-Apache-2.0.txt +202 -0
  6. data/data/msgs/NOTICE +17 -0
  7. data/data/msgs/builtin_interfaces/msg/Duration.rb +33 -0
  8. data/data/msgs/builtin_interfaces/msg/Time.rb +33 -0
  9. data/data/msgs/example_interfaces/srv/AddTwoInts.rb +64 -0
  10. data/data/msgs/geometry_msgs/msg/Accel.rb +35 -0
  11. data/data/msgs/geometry_msgs/msg/AccelStamped.rb +36 -0
  12. data/data/msgs/geometry_msgs/msg/Point.rb +36 -0
  13. data/data/msgs/geometry_msgs/msg/Point32.rb +36 -0
  14. data/data/msgs/geometry_msgs/msg/PointStamped.rb +36 -0
  15. data/data/msgs/geometry_msgs/msg/Pose.rb +36 -0
  16. data/data/msgs/geometry_msgs/msg/Pose2D.rb +36 -0
  17. data/data/msgs/geometry_msgs/msg/PoseStamped.rb +36 -0
  18. data/data/msgs/geometry_msgs/msg/Quaternion.rb +39 -0
  19. data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +36 -0
  20. data/data/msgs/geometry_msgs/msg/Transform.rb +36 -0
  21. data/data/msgs/geometry_msgs/msg/TransformStamped.rb +39 -0
  22. data/data/msgs/geometry_msgs/msg/Twist.rb +35 -0
  23. data/data/msgs/geometry_msgs/msg/TwistStamped.rb +36 -0
  24. data/data/msgs/geometry_msgs/msg/Vector3.rb +36 -0
  25. data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +36 -0
  26. data/data/msgs/geometry_msgs/msg/Wrench.rb +35 -0
  27. data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +36 -0
  28. data/data/msgs/sensor_msgs/msg/BatteryState.rb +100 -0
  29. data/data/msgs/sensor_msgs/msg/FluidPressure.rb +38 -0
  30. data/data/msgs/sensor_msgs/msg/Illuminance.rb +38 -0
  31. data/data/msgs/sensor_msgs/msg/Imu.rb +52 -0
  32. data/data/msgs/sensor_msgs/msg/JointState.rb +44 -0
  33. data/data/msgs/sensor_msgs/msg/MagneticField.rb +39 -0
  34. data/data/msgs/sensor_msgs/msg/NavSatFix.rb +55 -0
  35. data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +43 -0
  36. data/data/msgs/sensor_msgs/msg/Range.rb +52 -0
  37. data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +38 -0
  38. data/data/msgs/sensor_msgs/msg/Temperature.rb +38 -0
  39. data/data/msgs/std_msgs/msg/Bool.rb +30 -0
  40. data/data/msgs/std_msgs/msg/Byte.rb +30 -0
  41. data/data/msgs/std_msgs/msg/ByteMultiArray.rb +35 -0
  42. data/data/msgs/std_msgs/msg/Char.rb +30 -0
  43. data/data/msgs/std_msgs/msg/ColorRGBA.rb +39 -0
  44. data/data/msgs/std_msgs/msg/Empty.rb +26 -0
  45. data/data/msgs/std_msgs/msg/Float32.rb +30 -0
  46. data/data/msgs/std_msgs/msg/Float32MultiArray.rb +35 -0
  47. data/data/msgs/std_msgs/msg/Float64.rb +30 -0
  48. data/data/msgs/std_msgs/msg/Float64MultiArray.rb +35 -0
  49. data/data/msgs/std_msgs/msg/Header.rb +35 -0
  50. data/data/msgs/std_msgs/msg/Int16.rb +30 -0
  51. data/data/msgs/std_msgs/msg/Int16MultiArray.rb +35 -0
  52. data/data/msgs/std_msgs/msg/Int32.rb +30 -0
  53. data/data/msgs/std_msgs/msg/Int32MultiArray.rb +35 -0
  54. data/data/msgs/std_msgs/msg/Int64.rb +30 -0
  55. data/data/msgs/std_msgs/msg/Int64MultiArray.rb +35 -0
  56. data/data/msgs/std_msgs/msg/Int8.rb +30 -0
  57. data/data/msgs/std_msgs/msg/Int8MultiArray.rb +35 -0
  58. data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +36 -0
  59. data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +35 -0
  60. data/data/msgs/std_msgs/msg/String.rb +30 -0
  61. data/data/msgs/std_msgs/msg/UInt16.rb +30 -0
  62. data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +35 -0
  63. data/data/msgs/std_msgs/msg/UInt32.rb +30 -0
  64. data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +35 -0
  65. data/data/msgs/std_msgs/msg/UInt64.rb +30 -0
  66. data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +35 -0
  67. data/data/msgs/std_msgs/msg/UInt8.rb +30 -0
  68. data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +35 -0
  69. data/lib/asterism/cruby/objects.rb +224 -0
  70. data/lib/asterism/cruby/ros.rb +305 -0
  71. data/lib/asterism/cruby/runner.rb +253 -0
  72. data/lib/asterism/cruby/zenoh.rb +422 -0
  73. data/lib/asterism/cruby.rb +30 -0
  74. data/lib/asterism/version.rb +5 -0
  75. data/lib/asterism.rb +22 -4
  76. data/mrblib/asterism.rb +617 -0
  77. data/mrblib/cdr.rb +512 -0
  78. data/mrblib/future.rb +88 -0
  79. data/mrblib/proxy.rb +97 -0
  80. data/mrblib/ros.rb +861 -0
  81. data/tools/asterism_msggen.rb +826 -0
  82. 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
@@ -0,0 +1,5 @@
1
+ module Asterism
2
+ # The gem's version (the CRuby gem asterism; it depends on asterism-zenoh
3
+ # of the same version).
4
+ VERSION = "0.2.0"
5
+ end