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,224 @@
1
+ # The object layer (Asterism.connect / expose / []) with blocks and a
2
+ # receiving thread (CRuby only). Asterism.connect without a block,
3
+ # Asterism.poll and the rest of the polled API work as on the boards.
4
+ #
5
+ # Asterism.connect("tcp/192.0.2.2:7447", node: "mypc", app: "demo") do |net|
6
+ # net.expose("screen", Screen.new, methods: [:say])
7
+ # net.on_join { |node| puts "joined #{node}" }
8
+ # net.on_leave { |node| puts "left #{node}" }
9
+ # net["fmruby-aaaaaa/demo/screen"].say("hello")
10
+ # net.each("*/demo/info").map(&:status)
11
+ # net.run # answer calls until Ctrl-C
12
+ # end
13
+ #
14
+ # Locking: the object layer is one connection per process with its state in
15
+ # the Asterism module, written for one thread. Every entry into it (polling,
16
+ # answering, calling, exposing, listing) holds Asterism::LOCK, so the
17
+ # receiving thread and the application's threads take turns. A call that
18
+ # waits for its answer while the receiving thread runs does not poll; it
19
+ # waits for the receiving thread's ticks (Asterism::Runner#wait_until).
20
+ require "monitor"
21
+
22
+ module Asterism
23
+ # Held by every entry into the object layer (reentrant).
24
+ LOCK = Monitor.new
25
+
26
+ # The polled API, entered under LOCK. Prepended to the Asterism module's
27
+ # singleton class, so the shared methods themselves are not changed.
28
+ module Locked
29
+ %i[expose unexpose poll nodes [] call_async meta connected? exposed on_join on_leave].each do |m|
30
+ define_method(m) do |*args, **kw, &blk|
31
+ ::Asterism::LOCK.synchronize { super(*args, **kw, &blk) }
32
+ end
33
+ end
34
+
35
+ # With a block: yields an Asterism::Net and closes when the block ends
36
+ # (also on an exception); returns the block's value. Without a block,
37
+ # as before (returns Asterism).
38
+ def connect(locator, node:, app:, mode: nil, listen: nil, &blk)
39
+ ::Asterism::LOCK.synchronize do
40
+ super(locator, node: node, app: app, mode: mode, listen: listen)
41
+ # The Net of a connection that was lost (not closed) belongs to the
42
+ # old session.
43
+ old = @net
44
+ @net = nil
45
+ old&.release
46
+ end
47
+ return self unless blk
48
+ net = ::Asterism.net
49
+ begin
50
+ blk.call(net)
51
+ ensure
52
+ net.close
53
+ end
54
+ end
55
+
56
+ # Stops the receiving thread first (outside the lock: it may be inside
57
+ # a tick), then closes.
58
+ def close
59
+ net = @net
60
+ @net = nil
61
+ net&.release
62
+ ::Asterism::LOCK.synchronize { super }
63
+ end
64
+
65
+ # The on_join / on_leave blocks (called from Asterism.poll): with a Net,
66
+ # what they raise goes to its on_error and the other blocks still run.
67
+ def tell(where, blk, node)
68
+ net = @net
69
+ return super unless net
70
+ net.runner.guard(where) { super }
71
+ end
72
+
73
+ # Lists under the lock, yields outside it (the block may make calls).
74
+ def each(pattern = "**", &blk)
75
+ list = ::Asterism::LOCK.synchronize { super(pattern, &nil) }
76
+ return list unless blk
77
+ list.each(&blk)
78
+ list.size
79
+ end
80
+
81
+ # A call waiting for its answer. On the receiving thread, or when it
82
+ # does not run: polls, as on the boards. On another thread while it
83
+ # runs: waits for its ticks to bring the answer.
84
+ def wait_until(&cond)
85
+ loop do
86
+ r = @net&.runner
87
+ if r && r.running? && !r.current?
88
+ r.wait_until(&cond)
89
+ return if ::Asterism::LOCK.synchronize(&cond)
90
+ # The receiving thread stopped before the answer: poll here.
91
+ else
92
+ return ::Asterism::LOCK.synchronize { super(&cond) }
93
+ end
94
+ end
95
+ end
96
+ end
97
+ singleton_class.prepend(Locked)
98
+
99
+ class Future
100
+ # Prepended: done? reads the connection state, under the lock.
101
+ module Locked
102
+ def done?
103
+ ::Asterism::LOCK.synchronize { super }
104
+ end
105
+ end
106
+ prepend Locked
107
+ end
108
+
109
+ # The connection of this process (Asterism is one per process) as an
110
+ # object, with a receiving thread. nil when not connected.
111
+ def self.net
112
+ LOCK.synchronize do
113
+ return nil unless connected?
114
+ @net ||= Net.new
115
+ end
116
+ end
117
+
118
+ # The block API of the object layer. Made by Asterism.connect with a
119
+ # block, or Asterism.net after a connect without one.
120
+ class Net
121
+ attr_reader :runner
122
+
123
+ def initialize
124
+ @runner = Runner.new(::Asterism.instance_variable_get(:@session), lock: ::Asterism::LOCK, name: "asterism")
125
+ # One tick: Asterism.poll (answers the calls, tells on_join /
126
+ # on_leave); false once the connection is gone.
127
+ @runner.add { ::Asterism.poll ? 0 : false }
128
+ end
129
+
130
+ def node_id = ::Asterism.node_id
131
+ def app = ::Asterism.app
132
+ def connected? = ::Asterism.connected?
133
+ def lost_reason = ::Asterism.lost_reason
134
+ def nodes = ::Asterism.nodes
135
+ def exposed = ::Asterism.exposed
136
+
137
+ # Asterism.expose: methods: an Array of names or a Hash name => number
138
+ # of arguments. The object's methods run on the receiving thread (or in
139
+ # run, or in whatever polls).
140
+ def expose(name, obj, methods:)
141
+ ::Asterism.expose(name, obj, methods: methods)
142
+ end
143
+
144
+ def unexpose(name)
145
+ ::Asterism.unexpose(name)
146
+ end
147
+
148
+ # A proxy for <node>/<app>/<object>; timeout in seconds.
149
+ def [](path, timeout: nil)
150
+ ::Asterism[path, Zenoh.ms(timeout, ::Asterism::DEFAULT_TIMEOUT_MS)]
151
+ end
152
+ alias proxy []
153
+
154
+ # The exposed objects alive now matching the pattern, as proxies. With
155
+ # a block, yields each and returns their number; without, an
156
+ # Enumerator (net.each("*/demo/info").map(&:status)).
157
+ def each(pattern = "**", &blk)
158
+ return enum_for(:each, pattern) unless blk
159
+ ::Asterism.each(pattern, &blk)
160
+ end
161
+ include Enumerable
162
+
163
+ # on_join { |node| } / on_leave { |node| }: Asterism.on_join /
164
+ # on_leave, the same as on the boards (called from Asterism.poll), here
165
+ # on the receiving thread. What they raise goes to on_error.
166
+ def on_join(&blk)
167
+ ::Asterism.on_join(&blk)
168
+ self
169
+ end
170
+
171
+ def on_leave(&blk)
172
+ ::Asterism.on_leave(&blk)
173
+ self
174
+ end
175
+
176
+ # on_error { |error, where| }: what on_join / on_leave blocks raise.
177
+ # (An exposed method that raises is answered to its caller as a
178
+ # RemoteError, as before; it does not come here.)
179
+ def on_error(&blk)
180
+ @runner.on_error(&blk)
181
+ self
182
+ end
183
+
184
+ # Answers calls on a thread of its own. Returns self.
185
+ def start
186
+ @runner.start
187
+ self
188
+ end
189
+
190
+ def stop
191
+ @runner.stop
192
+ self
193
+ end
194
+
195
+ # Answers calls on this thread until stop, the connection closing, or
196
+ # Ctrl-C. Returns nil.
197
+ def run
198
+ @runner.run
199
+ end
200
+
201
+ def running?
202
+ @runner.running?
203
+ end
204
+
205
+ def poll
206
+ ::Asterism.poll
207
+ end
208
+
209
+ # Stops the receiving thread and closes the connection (Asterism.close).
210
+ def close
211
+ ::Asterism.close
212
+ nil
213
+ end
214
+
215
+ # For Asterism.close: stop receiving and forget the session.
216
+ def release
217
+ @runner.close
218
+ end
219
+
220
+ def inspect
221
+ "#<Asterism::Net #{node_id}/#{app}#{running? ? ' running' : ''}>"
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,305 @@
1
+ # Asterism::ROS with a receiving thread and Enumerators (CRuby only).
2
+ # Asterism::ROS::Node, node.poll, the subscribe / every blocks and pattern
3
+ # matching on messages are the shared ones (mrblib/ros.rb), as on the
4
+ # boards; this layer runs node.poll on a receiving thread, routes what the
5
+ # blocks raise to on_error, and adds the Enumerators.
6
+ #
7
+ # Asterism::ROS.connect("tcp/192.0.2.2:7447", domain: 0) do |ros|
8
+ # node = ros.node("ruby_talker")
9
+ # chatter = node.publisher("/chatter", "std_msgs/msg/String")
10
+ # chatter << { data: "hello from Ruby" }
11
+ # node.subscribe("/odom", "nav_msgs/msg/Odometry") { |odom| p odom.pose.pose.position.x }
12
+ # node.every(0.1) { chatter << { data: Time.now.to_s } }
13
+ # node.service("/add_ruby", "example_interfaces/srv/AddTwoInts") { |req| { sum: req.a + req.b } }
14
+ # p node.call("/add_two_ints", "example_interfaces/srv/AddTwoInts", a: 1, b: 2).sum
15
+ # node.topic("/scan", "sensor_msgs/msg/LaserScan").each.lazy.first(1)
16
+ # ros.spin
17
+ # end
18
+ #
19
+ # Locking: each connection has one lock (its Runner's). A tick (node.poll:
20
+ # services answered, subscription blocks, timers) holds it; so do the calls
21
+ # that change a node (making publishers, subscriptions, services, clients,
22
+ # timers) and publishing. A service call made on another thread while the connection
23
+ # spins waits for the ticks (Asterism::Runner#wait_until) instead of
24
+ # polling by itself.
25
+ module Asterism
26
+ module ROS
27
+ # Opens a Zenoh session and makes a Connection for nodes on it. domain:
28
+ # the ROS domain of the nodes made by ros.node. type_path: directories
29
+ # with generated types to add to Asterism::ROS::TYPE_PATH. The other
30
+ # options go to Asterism::Zenoh::Session.open. With a block: yields the
31
+ # Connection, closes it when the block ends and returns its value.
32
+ def self.connect(locator = nil, domain: 0, type_path: nil, interval: Runner::DEFAULT_INTERVAL, **opts)
33
+ Array(type_path).each { |d| TYPE_PATH << File.expand_path(d) unless TYPE_PATH.include?(File.expand_path(d)) }
34
+ zconn = ::Asterism::Zenoh.open(locator, interval: interval, **opts)
35
+ conn = Connection.new(zconn, domain: domain)
36
+ return conn unless block_given?
37
+ begin
38
+ yield conn
39
+ ensure
40
+ conn.close
41
+ end
42
+ end
43
+
44
+ # The ROS side of a Zenoh connection: makes nodes and spins them.
45
+ class Connection
46
+ attr_reader :zenoh, :domain
47
+
48
+ def initialize(zconn, domain: 0)
49
+ @zenoh = zconn
50
+ @domain = domain.to_i
51
+ @nodes = []
52
+ end
53
+
54
+ def session = @zenoh.session
55
+ def runner = @zenoh.runner
56
+
57
+ # An Asterism::ROS::Node on this connection, with the block API
58
+ # (subscribe, every, topic; services answered while it spins).
59
+ def node(name, namespace: "/", enclave: "/")
60
+ runner.lock.synchronize do
61
+ n = Node.new(session, name, namespace: namespace, domain: @domain, enclave: enclave)
62
+ n.extend(Spinning)
63
+ n.asterism_spin_on(runner)
64
+ @nodes << n
65
+ n
66
+ end
67
+ end
68
+
69
+ def nodes = @nodes.dup
70
+
71
+ # Spins on this thread (services, subscription blocks, timers) until
72
+ # stop, the connection closing, or Ctrl-C. Returns nil.
73
+ def spin
74
+ runner.run
75
+ end
76
+ alias run spin
77
+
78
+ # Spins on a thread of its own. Returns self.
79
+ def start
80
+ runner.start
81
+ self
82
+ end
83
+
84
+ def stop
85
+ runner.stop
86
+ self
87
+ end
88
+
89
+ def running? = runner.running?
90
+
91
+ # on_error { |error, where| }: what subscription, timer and service
92
+ # blocks raise (the service request then gets no answer).
93
+ def on_error(&blk)
94
+ runner.on_error(&blk)
95
+ self
96
+ end
97
+
98
+ # Stops spinning, withdraws the nodes and closes the session.
99
+ def close
100
+ runner.stop
101
+ @nodes.each { |n| n.close rescue ::Asterism::Zenoh::Error }
102
+ @nodes = []
103
+ @zenoh.close
104
+ nil
105
+ end
106
+
107
+ def inspect
108
+ "#<Asterism::ROS::Connection domain #{@domain}#{running? ? ' spinning' : ''}>"
109
+ end
110
+ end
111
+
112
+ # A service block raised; it was reported through on_error already.
113
+ class ServiceFailed < ::StandardError; end
114
+
115
+ # Added to the nodes a Connection makes (Node#extend). The methods of
116
+ # Node keep their meaning; these come on top.
117
+ module Spinning
118
+ def asterism_spin_on(runner)
119
+ @asterism_runner = runner
120
+ node = self
121
+ @asterism_task = runner.add { node.poll ? 0 : false }
122
+ end
123
+
124
+ # As Node#pump (node.poll and waiting service calls go through it). A
125
+ # service block that raised was reported (on_error) and its request
126
+ # left unanswered; polling goes on.
127
+ def pump(steps = 8)
128
+ super
129
+ rescue ::Asterism::ROS::ServiceFailed
130
+ !@session.closed?
131
+ end
132
+
133
+ # The subscribe and every blocks (called from node.poll): what they
134
+ # raise goes to on_error, and the others still run.
135
+ def handle(sub, msg, info)
136
+ @asterism_runner.guard("subscribe #{sub.topic_key}") { super }
137
+ end
138
+
139
+ def fire(timer, now)
140
+ @asterism_runner.guard("every #{timer.period}") { super }
141
+ end
142
+
143
+ def asterism_runner = @asterism_runner
144
+
145
+ # The calls that change the node take the connection's lock.
146
+ %i[subscription client close every cancel_timer].each do |m|
147
+ define_method(m) do |*args, **kw, &blk|
148
+ @asterism_runner.lock.synchronize { super(*args, **kw, &blk) }
149
+ end
150
+ end
151
+
152
+ # As Node#publisher; publishing takes the lock too (a timer on the
153
+ # receiving thread and the application may publish at once).
154
+ def publisher(topic, type, qos: DEFAULT_QOS)
155
+ r = @asterism_runner
156
+ r.lock.synchronize do
157
+ pub = super
158
+ pub.extend(LockedPublisher)
159
+ pub.instance_variable_set(:@asterism_lock, r.lock)
160
+ pub
161
+ end
162
+ end
163
+
164
+ # As Node#service: the block answers while the connection spins (or
165
+ # from node.poll). What it raises goes to on_error and that request
166
+ # gets no answer.
167
+ def service(name, type, qos: DEFAULT_QOS, depth: 8, &handler)
168
+ raise ArgumentError, "service needs a block" unless handler
169
+ r = @asterism_runner
170
+ where = "service #{name}"
171
+ wrapped = proc do |req|
172
+ handler.call(req)
173
+ rescue StandardError => e
174
+ r.report(e, where)
175
+ raise ::Asterism::ROS::ServiceFailed, e.message
176
+ end
177
+ r.lock.synchronize { super(name, type, qos: qos, depth: depth, &wrapped) }
178
+ end
179
+
180
+ # With a block: as on the boards (Node#subscribe), the block gets each
181
+ # message (and its Attachment, or nil) from node.poll, here while the
182
+ # connection spins; returns the Subscription. Without: a Subscription
183
+ # whose each waits for messages (an Enumerator).
184
+ def subscribe(topic, type, qos: DEFAULT_QOS, depth: 16, &blk)
185
+ r = @asterism_runner
186
+ r.lock.synchronize do
187
+ sub = blk ? super : subscription(topic, type, qos: qos, depth: depth)
188
+ sub.extend(Stream)
189
+ sub.asterism_stream_on(session)
190
+ sub
191
+ end
192
+ end
193
+
194
+ # A topic to iterate: each subscribes for the iteration and withdraws
195
+ # after it (node.topic("/scan", "sensor_msgs/msg/LaserScan").each.lazy.first(1)).
196
+ def topic(topic, type, qos: DEFAULT_QOS, depth: 16)
197
+ Topic.new(self, topic, type, qos, depth)
198
+ end
199
+
200
+ # Node#call with the time limit in seconds as well: timeout: 2.0.
201
+ def call(service, type, request = nil, timeout: nil, timeout_ms: nil, **fields)
202
+ ms = timeout_ms || ::Asterism::Zenoh.ms(timeout, Client::DEFAULT_TIMEOUT_MS)
203
+ c = @asterism_runner.lock.synchronize do
204
+ @clients ||= {}
205
+ cl = @clients[service]
206
+ if cl.nil? || cl.closed?
207
+ cl = client(service, type)
208
+ @clients[service] = cl
209
+ end
210
+ cl
211
+ end
212
+ c.call(request, timeout_ms: ms, **fields)
213
+ end
214
+ end
215
+
216
+ module LockedPublisher
217
+ def publish(msg)
218
+ @asterism_lock.synchronize { super }
219
+ end
220
+ end
221
+
222
+ # Added to the subscriptions node.subscribe makes.
223
+ module Stream
224
+ include Enumerable
225
+
226
+ def asterism_stream_on(session)
227
+ @asterism_session = session
228
+ end
229
+
230
+ # Yields each message (and its Attachment) as it comes, waiting for
231
+ # the next; ends when the subscription or the session closes, or after
232
+ # timeout seconds. Without a block, an Enumerator.
233
+ def each(timeout: nil, &blk)
234
+ return enum_for(:each, timeout: timeout) unless blk
235
+ raise ::Asterism::Error, "this subscription has a block; its messages go there" if handler
236
+ deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
237
+ loop do
238
+ got = each_pending
239
+ got.each { |m| blk.call(m[0], m[1]) }
240
+ break if closed? || @asterism_session.closed?
241
+ break if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
242
+ sleep(::Asterism::Zenoh::ENUM_STEP) if got.empty?
243
+ end
244
+ self
245
+ end
246
+
247
+ end
248
+
249
+ # node.topic: subscribes while it is iterated.
250
+ class Topic
251
+ include Enumerable
252
+
253
+ attr_reader :name, :type
254
+
255
+ def initialize(node, name, type, qos, depth)
256
+ @node = node
257
+ @name = name
258
+ @type = type
259
+ @qos = qos
260
+ @depth = depth
261
+ end
262
+
263
+ # Yields each message and its Attachment, from a subscription made
264
+ # for this iteration (withdrawn when it ends: break, first(n), the
265
+ # time running out). Without a block, an Enumerator.
266
+ def each(timeout: nil, &blk)
267
+ return enum_for(:each, timeout: timeout) unless blk
268
+ sub = @node.subscribe(@name, @type, qos: @qos, depth: @depth)
269
+ begin
270
+ sub.each(timeout: timeout, &blk)
271
+ ensure
272
+ sub.close
273
+ end
274
+ self
275
+ end
276
+
277
+ def inspect
278
+ "#<Asterism::ROS::Topic #{@name}>"
279
+ end
280
+ end
281
+
282
+ class Client
283
+ # Prepended: a call made on another thread while the connection
284
+ # spins waits for the ticks instead of polling the node by itself.
285
+ # (Calls from a board's update loop are on one thread; here the
286
+ # application may call from several.)
287
+ # (The sequence number is read once in the shared call_async, so
288
+ # several threads may call through one client without a lock.)
289
+ module Waiting
290
+ def wait_for(c)
291
+ loop do
292
+ r = ::Asterism::Runner.for(@session)
293
+ if r && r.running? && !r.current?
294
+ r.wait_until { c.done? }
295
+ return if c.done?
296
+ else
297
+ return r ? r.lock.synchronize { super } : super
298
+ end
299
+ end
300
+ end
301
+ end
302
+ prepend Waiting
303
+ end
304
+ end
305
+ end