asterism 0.4.0 → 0.5.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -4
  3. data/NOTICE +12 -7
  4. data/README.md +148 -17
  5. data/data/icon.png +0 -0
  6. data/data/msgs/LICENSE-BSD-3-Clause-tf2_msgs.txt +29 -0
  7. data/data/msgs/NOTICE +19 -13
  8. data/data/msgs/builtin_interfaces/msg/Duration.rb +4 -0
  9. data/data/msgs/builtin_interfaces/msg/Time.rb +4 -0
  10. data/data/msgs/diagnostic_msgs/msg/DiagnosticArray.rb +40 -0
  11. data/data/msgs/diagnostic_msgs/msg/DiagnosticStatus.rb +55 -0
  12. data/data/msgs/diagnostic_msgs/msg/KeyValue.rb +37 -0
  13. data/data/msgs/example_interfaces/srv/AddTwoInts.rb +7 -0
  14. data/data/msgs/geometry_msgs/msg/Accel.rb +4 -0
  15. data/data/msgs/geometry_msgs/msg/AccelStamped.rb +4 -0
  16. data/data/msgs/geometry_msgs/msg/Point.rb +5 -0
  17. data/data/msgs/geometry_msgs/msg/Point32.rb +5 -0
  18. data/data/msgs/geometry_msgs/msg/PointStamped.rb +4 -0
  19. data/data/msgs/geometry_msgs/msg/Pose.rb +4 -0
  20. data/data/msgs/geometry_msgs/msg/Pose2D.rb +5 -0
  21. data/data/msgs/geometry_msgs/msg/PoseStamped.rb +4 -0
  22. data/data/msgs/geometry_msgs/msg/PoseWithCovariance.rb +39 -0
  23. data/data/msgs/geometry_msgs/msg/Quaternion.rb +6 -0
  24. data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +4 -0
  25. data/data/msgs/geometry_msgs/msg/Transform.rb +4 -0
  26. data/data/msgs/geometry_msgs/msg/TransformStamped.rb +5 -0
  27. data/data/msgs/geometry_msgs/msg/Twist.rb +4 -0
  28. data/data/msgs/geometry_msgs/msg/TwistStamped.rb +4 -0
  29. data/data/msgs/geometry_msgs/msg/TwistWithCovariance.rb +39 -0
  30. data/data/msgs/geometry_msgs/msg/Vector3.rb +5 -0
  31. data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +4 -0
  32. data/data/msgs/geometry_msgs/msg/Wrench.rb +4 -0
  33. data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +4 -0
  34. data/data/msgs/nav_msgs/msg/MapMetaData.rb +52 -0
  35. data/data/msgs/nav_msgs/msg/OccupancyGrid.rb +44 -0
  36. data/data/msgs/nav_msgs/msg/Odometry.rb +49 -0
  37. data/data/msgs/nav_msgs/msg/Path.rb +40 -0
  38. data/data/msgs/rcl_interfaces/msg/Log.rb +64 -0
  39. data/data/msgs/rcl_interfaces/msg/Parameter.rb +39 -0
  40. data/data/msgs/rcl_interfaces/msg/ParameterEvent.rb +52 -0
  41. data/data/msgs/rcl_interfaces/msg/ParameterValue.rb +69 -0
  42. data/data/msgs/sensor_msgs/msg/BatteryState.rb +18 -0
  43. data/data/msgs/sensor_msgs/msg/CompressedImage.rb +43 -0
  44. data/data/msgs/sensor_msgs/msg/FluidPressure.rb +5 -0
  45. data/data/msgs/sensor_msgs/msg/Illuminance.rb +5 -0
  46. data/data/msgs/sensor_msgs/msg/Imu.rb +9 -0
  47. data/data/msgs/sensor_msgs/msg/JointState.rb +7 -0
  48. data/data/msgs/sensor_msgs/msg/LaserScan.rb +71 -0
  49. data/data/msgs/sensor_msgs/msg/MagneticField.rb +5 -0
  50. data/data/msgs/sensor_msgs/msg/NavSatFix.rb +9 -0
  51. data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +4 -0
  52. data/data/msgs/sensor_msgs/msg/PointCloud2.rb +68 -0
  53. data/data/msgs/sensor_msgs/msg/PointField.rb +53 -0
  54. data/data/msgs/sensor_msgs/msg/Range.rb +9 -0
  55. data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +5 -0
  56. data/data/msgs/sensor_msgs/msg/Temperature.rb +5 -0
  57. data/data/msgs/std_msgs/msg/Bool.rb +3 -0
  58. data/data/msgs/std_msgs/msg/Byte.rb +3 -0
  59. data/data/msgs/std_msgs/msg/ByteMultiArray.rb +4 -0
  60. data/data/msgs/std_msgs/msg/Char.rb +3 -0
  61. data/data/msgs/std_msgs/msg/ColorRGBA.rb +6 -0
  62. data/data/msgs/std_msgs/msg/Empty.rb +1 -0
  63. data/data/msgs/std_msgs/msg/Float32.rb +3 -0
  64. data/data/msgs/std_msgs/msg/Float32MultiArray.rb +4 -0
  65. data/data/msgs/std_msgs/msg/Float64.rb +3 -0
  66. data/data/msgs/std_msgs/msg/Float64MultiArray.rb +4 -0
  67. data/data/msgs/std_msgs/msg/Header.rb +4 -0
  68. data/data/msgs/std_msgs/msg/Int16.rb +3 -0
  69. data/data/msgs/std_msgs/msg/Int16MultiArray.rb +4 -0
  70. data/data/msgs/std_msgs/msg/Int32.rb +3 -0
  71. data/data/msgs/std_msgs/msg/Int32MultiArray.rb +4 -0
  72. data/data/msgs/std_msgs/msg/Int64.rb +3 -0
  73. data/data/msgs/std_msgs/msg/Int64MultiArray.rb +4 -0
  74. data/data/msgs/std_msgs/msg/Int8.rb +3 -0
  75. data/data/msgs/std_msgs/msg/Int8MultiArray.rb +4 -0
  76. data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +5 -0
  77. data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +4 -0
  78. data/data/msgs/std_msgs/msg/String.rb +3 -0
  79. data/data/msgs/std_msgs/msg/UInt16.rb +3 -0
  80. data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +4 -0
  81. data/data/msgs/std_msgs/msg/UInt32.rb +3 -0
  82. data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +4 -0
  83. data/data/msgs/std_msgs/msg/UInt64.rb +3 -0
  84. data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +4 -0
  85. data/data/msgs/std_msgs/msg/UInt8.rb +3 -0
  86. data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +4 -0
  87. data/data/msgs/tf2_msgs/msg/TFMessage.rb +35 -0
  88. data/data/msgs/visualization_msgs/msg/Marker.rb +132 -0
  89. data/data/msgs/visualization_msgs/msg/MarkerArray.rb +35 -0
  90. data/data/msgs/visualization_msgs/msg/MeshFile.rb +37 -0
  91. data/data/msgs/visualization_msgs/msg/UVCoordinate.rb +37 -0
  92. data/lib/asterism/cruby/objects.rb +13 -1
  93. data/lib/asterism/cruby.rb +38 -0
  94. data/lib/asterism/version.rb +4 -3
  95. data/lib/asterism.rb +1 -1
  96. data/mrblib/asterism.rb +5 -0
  97. data/mrblib/profile.rb +362 -0
  98. data/mrblib/ros.rb +71 -6
  99. data/tools/asterism_msggen.rb +32 -5
  100. metadata +28 -2
data/mrblib/profile.rb ADDED
@@ -0,0 +1,362 @@
1
+ # A node's profile: what it runs on, where, a display name and an icon,
2
+ # told by the node itself (0.5.0).
3
+ #
4
+ # Asterism.profile(name: "Rover", icon: png) # this node's; returns it
5
+ # Asterism.profile # => {"kind" => "picoruby", "name" => "Rover", ...}
6
+ # Asterism.profile_of("fmruby-aaaaaa") # another node's, or nil
7
+ #
8
+ # Key: asterism/<node>/@profile (a queryable of every application connected
9
+ # as that node). The reply is a MessagePack Hash:
10
+ # "kind" what Ruby runs the node: "family_mruby", "picoruby",
11
+ # "cruby", "console" (others are allowed)
12
+ # "host" where it runs, optional ("atoms3r", "tab5", "sim", ...)
13
+ # "name" a display name, optional
14
+ # "icon" PNG bytes, optional (8 KB and 256x256 at most)
15
+ # "icon_hash" the icon's summary (Profile.png_info): a reader that has
16
+ # that icon already asks again with the parameter icon=0 and
17
+ # gets the rest without it
18
+ # Each connect, and each change while connected, also puts the profile
19
+ # without its icon on the same key, for those who follow it (a console).
20
+ #
21
+ # Room left for later: an application's own profile would be
22
+ # asterism/<node>/<app>/@profile, with the same Hash.
23
+ module Asterism
24
+ module Profile
25
+ # The kinds this version knows. Others pass: a kind is any lower-case
26
+ # word (a-z, 0-9, _; up to KIND_MAX bytes) starting with a letter.
27
+ KINDS = ["family_mruby", "picoruby", "cruby", "console"]
28
+ KEY = "@profile"
29
+ ICON_MAX = 8192 # bytes
30
+ ICON_SIDE_MAX = 256 # pixels, each side
31
+ KIND_MAX = 32
32
+ HOST_MAX = 32
33
+ NAME_MAX = 64
34
+ PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10]
35
+
36
+ # [width, height, icon_hash] of a PNG that a profile may carry, or nil.
37
+ # Checks the signature, the size (ICON_MAX bytes), that the chunks tile
38
+ # the bytes from IHDR to IEND, and the image's sides (ICON_SIDE_MAX).
39
+ #
40
+ # icon_hash is the Adler-32 (8 hex digits) of the chunks' CRC-32 fields,
41
+ # in order: each CRC already sums up its chunk, so the summary costs a
42
+ # few bytes of arithmetic instead of a pass over the whole file (small
43
+ # integers only, the same on every Ruby). On CRuby it equals
44
+ # Zlib.adler32 of those fields joined.
45
+ def self.png_info(b)
46
+ return nil unless b.is_a?(String)
47
+ size = b.bytesize
48
+ return nil if size < 8 + 25 + 12 || size > ICON_MAX
49
+ i = 0
50
+ while i < 8
51
+ return nil unless b.getbyte(i) == PNG_SIGNATURE[i]
52
+ i += 1
53
+ end
54
+ pos = 8
55
+ first = true
56
+ w = 0
57
+ h = 0
58
+ a = 1
59
+ s = 0
60
+ while pos + 12 <= size
61
+ # The length is 4 bytes; anything past 16 bits is larger than ICON_MAX.
62
+ return nil unless b.getbyte(pos) == 0 && b.getbyte(pos + 1) == 0
63
+ len = b.getbyte(pos + 2) * 256 + b.getbyte(pos + 3)
64
+ nxt = pos + 12 + len
65
+ return nil if nxt > size
66
+ if first
67
+ return nil unless type?(b, pos, 73, 72, 68, 82) && len == 13 # IHDR
68
+ return nil unless b.getbyte(pos + 8) == 0 && b.getbyte(pos + 9) == 0
69
+ return nil unless b.getbyte(pos + 12) == 0 && b.getbyte(pos + 13) == 0
70
+ w = b.getbyte(pos + 10) * 256 + b.getbyte(pos + 11)
71
+ h = b.getbyte(pos + 14) * 256 + b.getbyte(pos + 15)
72
+ return nil if w < 1 || h < 1 || w > ICON_SIDE_MAX || h > ICON_SIDE_MAX
73
+ first = false
74
+ end
75
+ k = nxt - 4
76
+ while k < nxt
77
+ a = (a + b.getbyte(k)) % 65521
78
+ s = (s + a) % 65521
79
+ k += 1
80
+ end
81
+ if type?(b, pos, 73, 69, 78, 68) # IEND
82
+ return nil unless nxt == size
83
+ return [w, h, hex4(s) + hex4(a)]
84
+ end
85
+ pos = nxt
86
+ end
87
+ nil
88
+ end
89
+
90
+ # @api private
91
+ def self.type?(b, pos, c0, c1, c2, c3)
92
+ b.getbyte(pos + 4) == c0 && b.getbyte(pos + 5) == c1 && b.getbyte(pos + 6) == c2 && b.getbyte(pos + 7) == c3
93
+ end
94
+
95
+ # @api private
96
+ def self.hex4(n)
97
+ t = n.to_s(16)
98
+ t = "0" + t while t.length < 4
99
+ t
100
+ end
101
+
102
+ # A received profile, cleaned: the known fields that pass their checks
103
+ # (a field that does not is left out; an icon that is not a valid PNG
104
+ # goes with its hash). nil when it is not a profile (no valid kind).
105
+ def self.check(h)
106
+ return nil unless h.is_a?(Hash)
107
+ kind = h["kind"]
108
+ return nil unless kind.is_a?(String) && kind?(kind)
109
+ out = { "kind" => kind }
110
+ host = h["host"]
111
+ out["host"] = host if host.is_a?(String) && !host.empty? && host.bytesize <= HOST_MAX
112
+ name = h["name"]
113
+ out["name"] = name if name.is_a?(String) && !name.empty? && name.bytesize <= NAME_MAX
114
+ icon = h["icon"]
115
+ if icon.is_a?(String)
116
+ info = png_info(icon)
117
+ if info
118
+ out["icon"] = icon
119
+ out["icon_hash"] = info[2]
120
+ end
121
+ elsif hash_text?(h["icon_hash"])
122
+ out["icon_hash"] = h["icon_hash"]
123
+ end
124
+ out
125
+ end
126
+
127
+ # @api private
128
+ def self.kind?(s)
129
+ n = s.bytesize
130
+ return false if n < 1 || n > KIND_MAX
131
+ i = 0
132
+ while i < n
133
+ c = s.getbyte(i)
134
+ lower = c >= 97 && c <= 122
135
+ return false unless lower || (i > 0 && ((c >= 48 && c <= 57) || c == 95))
136
+ i += 1
137
+ end
138
+ true
139
+ end
140
+
141
+ # @api private
142
+ def self.hash_text?(s)
143
+ return false unless s.is_a?(String) && s.bytesize == 8
144
+ i = 0
145
+ while i < 8
146
+ c = s.getbyte(i)
147
+ return false unless (c >= 48 && c <= 57) || (c >= 97 && c <= 102)
148
+ i += 1
149
+ end
150
+ true
151
+ end
152
+
153
+ # The checks of Asterism.profile (ArgumentError, with the reason).
154
+ # @api private
155
+ def self.kind!(v)
156
+ s = v.is_a?(Symbol) ? v.to_s : v
157
+ unless s.is_a?(String) && kind?(s)
158
+ raise ArgumentError, "profile kind: #{v.inspect} is not a lower-case word (a-z, 0-9, _; " \
159
+ "#{KIND_MAX} bytes at most), e.g. :cruby"
160
+ end
161
+ s
162
+ end
163
+
164
+ # @api private
165
+ def self.text!(what, v, max)
166
+ s = v.is_a?(Symbol) ? v.to_s : v
167
+ raise ArgumentError, "profile #{what}: expected a String, got #{v.class}" unless s.is_a?(String)
168
+ raise ArgumentError, "profile #{what}: empty (pass nil to leave it out)" if s.empty?
169
+ raise ArgumentError, "profile #{what}: #{s.bytesize} bytes (#{max} at most)" if s.bytesize > max
170
+ s
171
+ end
172
+
173
+ # @api private
174
+ def self.icon!(v)
175
+ raise ArgumentError, "profile icon: expected the bytes of a PNG (a String), got #{v.class}" unless v.is_a?(String)
176
+ if v.bytesize > ICON_MAX
177
+ raise ArgumentError, "profile icon: #{v.bytesize} bytes (#{ICON_MAX} at most)"
178
+ end
179
+ unless png_info(v)
180
+ raise ArgumentError, "profile icon: not a PNG, or not one this accepts (up to " \
181
+ "#{ICON_SIDE_MAX}x#{ICON_SIDE_MAX}, chunks from IHDR to IEND)"
182
+ end
183
+ v
184
+ end
185
+
186
+ # Whether a query's parameters ask for the icon (icon=0 says no).
187
+ # @api private
188
+ def self.wants_icon?(params)
189
+ return true if params.nil? || params.empty?
190
+ params.split(";").each do |pair|
191
+ kv = pair.split("=")
192
+ next unless kv[0] == "icon"
193
+ v = kv[1].to_s
194
+ return false if v == "0" || v == "no" || v == "false"
195
+ end
196
+ true
197
+ end
198
+ end
199
+
200
+ # This node's profile. With keywords, sets those fields and returns the
201
+ # profile; without, returns it. The fields: kind: (a Symbol or String;
202
+ # nil goes back to the default), name:, host: (Strings; nil leaves them
203
+ # out), icon: (the bytes of a PNG; nil for no icon). The fields not given
204
+ # stay as they are. Raises ArgumentError for a field it does not accept
205
+ # (see Asterism::Profile).
206
+ #
207
+ # The defaults: kind "picoruby" on mruby / PicoRuby, "cruby" on CRuby; no
208
+ # host, no name; the icon is the file at Asterism.default_icon_path when
209
+ # there is one (the boards: /usr/share/asterism/icon.png; CRuby: the gem's
210
+ # data/icon.png). A board's firmware may set its own defaults with this
211
+ # same call before the application runs (Family mruby does).
212
+ #
213
+ # Kept across connect and close (it belongs to the process, or the VM on
214
+ # a board). While connected, a change is also put on the profile's key.
215
+ def self.profile(**fields)
216
+ return profile_hash(true) if fields.empty?
217
+ changes = {}
218
+ fields.each do |k, v|
219
+ key = k.to_s
220
+ if key == "kind"
221
+ changes[key] = v.nil? ? nil : Profile.kind!(v)
222
+ elsif key == "name"
223
+ changes[key] = v.nil? ? nil : Profile.text!("name", v, Profile::NAME_MAX)
224
+ elsif key == "host"
225
+ changes[key] = v.nil? ? nil : Profile.text!("host", v, Profile::HOST_MAX)
226
+ elsif key == "icon"
227
+ changes[key] = v.nil? ? nil : Profile.icon!(v)
228
+ else
229
+ raise ArgumentError, "unknown profile field #{key} (kind, name, host, icon)"
230
+ end
231
+ end
232
+ set = (@profile_set ||= {})
233
+ changes.each do |k, v|
234
+ if v.nil? && k != "icon"
235
+ set.delete(k)
236
+ else
237
+ set[k] = v # icon: nil stays, so the default file is not used
238
+ end
239
+ end
240
+ @profile_icon = nil if changes.key?("icon")
241
+ tell_profile
242
+ profile_hash(true)
243
+ end
244
+
245
+ # Another node's profile (Asterism::Profile.check has cleaned it), or nil
246
+ # when it does not answer in time (an older version of Asterism, a ROS 2
247
+ # node, a node that is gone). icon: false asks for it without the icon
248
+ # bytes (icon_hash still says which icon it is). timeout: in seconds (or
249
+ # timeout_ms:), 2.0 by default. Waits like a call (polling, answering
250
+ # calls meanwhile); raises Disconnected when not connected. This node's
251
+ # own profile comes without the network.
252
+ def self.profile_of(node, icon: true, timeout: nil, timeout_ms: nil)
253
+ f = profile_async(node, icon, timeout, timeout_ms)
254
+ begin
255
+ raw = f.raw_value
256
+ rescue Disconnected
257
+ raise
258
+ rescue Error
259
+ return nil # no answer in time, or not MessagePack
260
+ end
261
+ Profile.check(raw)
262
+ end
263
+
264
+ # @api private
265
+ def self.profile_async(node, icon, timeout, timeout_ms)
266
+ node = check_name("node", node)
267
+ ms = time_ms("Asterism.profile_of", timeout, timeout_ms, nil, DEFAULT_TIMEOUT_MS)
268
+ path = "#{node}/#{Profile::KEY}"
269
+ return Future.answered(path, "profile", Codec.pack(profile_hash(icon))) if @session && node == @node
270
+ begin
271
+ g = session!.get("#{ROOT}/#{path}", timeout_ms: ms, params: icon ? nil : "icon=0")
272
+ rescue Asterism::Zenoh::Error => e
273
+ lost(e.message)
274
+ raise Disconnected, e.message
275
+ end
276
+ Future.new(path, "profile", g, now_ms + ms, ms)
277
+ end
278
+
279
+ # The kind when the application did not set one.
280
+ # @api private
281
+ def self.default_profile_kind
282
+ "picoruby"
283
+ end
284
+
285
+ # Where the default icon is read from (when the application gave none);
286
+ # nil for none. CRuby: the gem's data/icon.png.
287
+ def self.default_icon_path
288
+ "/usr/share/asterism/icon.png"
289
+ end
290
+
291
+ # The bytes of a file, nil when it cannot be read.
292
+ # @api private
293
+ def self.read_icon_file(path)
294
+ return nil unless Object.const_defined?(:File) && File.exist?(path)
295
+ File.open(path, "r") { |f| f.read }
296
+ rescue
297
+ nil
298
+ end
299
+
300
+ # The profile as sent: String keys, the icon only when with_icon.
301
+ # @api private
302
+ def self.profile_hash(with_icon)
303
+ set = @profile_set || {}
304
+ h = { "kind" => set["kind"] || default_profile_kind }
305
+ h["host"] = set["host"] if set["host"]
306
+ h["name"] = set["name"] if set["name"]
307
+ icon = profile_icon
308
+ if icon
309
+ h["icon"] = icon[0] if with_icon
310
+ h["icon_hash"] = icon[1]
311
+ end
312
+ h
313
+ end
314
+
315
+ # [bytes, icon_hash] of this node's icon, or nil. The default file is
316
+ # read once, when the profile is first needed.
317
+ # @api private
318
+ def self.profile_icon
319
+ if @profile_icon.nil?
320
+ set = @profile_set || {}
321
+ bytes = set.key?("icon") ? set["icon"] : nil
322
+ unless set.key?("icon")
323
+ path = default_icon_path
324
+ bytes = path ? read_icon_file(path) : nil
325
+ end
326
+ info = bytes ? Profile.png_info(bytes) : nil
327
+ @profile_icon = info ? [bytes, info[2]] : false
328
+ end
329
+ @profile_icon ? @profile_icon : nil
330
+ end
331
+
332
+ # Puts the profile without its icon on its key (while connected).
333
+ # @api private
334
+ def self.tell_profile
335
+ s = @session
336
+ return unless s
337
+ s.put("#{ROOT}/#{@node}/#{Profile::KEY}", Codec.pack(profile_hash(false)))
338
+ rescue Asterism::Zenoh::Error
339
+ nil # the next poll notices a lost connection
340
+ end
341
+
342
+ # @api private
343
+ def self.answer_profile
344
+ qa = @profile_queryable
345
+ return unless qa
346
+ queries = qa.each_pending
347
+ i = 0
348
+ while i < queries.size
349
+ q = queries[i]
350
+ i += 1
351
+ begin
352
+ q.reply("#{ROOT}/#{@node}/#{Profile::KEY}", Codec.pack(profile_hash(Profile.wants_icon?(q.params))))
353
+ ensure
354
+ begin
355
+ q.finish
356
+ rescue Asterism::Zenoh::Error
357
+ # the session went away meanwhile
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
data/mrblib/ros.rb CHANGED
@@ -209,11 +209,14 @@ module Asterism
209
209
  # returns it (Asterism::ROS::GeometryMsgs::Twist). The file loads the
210
210
  # types it is made of first. A type already defined (loaded before, or
211
211
  # written in Ruby by the application) is returned without a file.
212
+ #
213
+ # The name is checked before it reaches the file system (it may come
214
+ # from the network): pkg/msg/Name or pkg/srv/Name, the package of
215
+ # a-z, 0-9 and _ starting with a letter, the type of letters and digits
216
+ # starting with a capital (the rules of ROS 2's interface names).
217
+ # Anything else raises ArgumentError.
212
218
  def self.require_type(name)
213
- parts = name.to_s.split("/")
214
- if parts.size != 3 || (parts[1] != "msg" && parts[1] != "srv")
215
- raise ArgumentError, "bad type name #{name.inspect} (want pkg/msg/Name or pkg/srv/Name)"
216
- end
219
+ parts = check_type_name(name)
217
220
  t = type_constant(parts[0], parts[2])
218
221
  return t if t
219
222
  found = nil
@@ -227,6 +230,68 @@ module Asterism
227
230
  t
228
231
  end
229
232
 
233
+ # "pkg/msg/Name" -> ["pkg", "msg", "Name"], or ArgumentError. By hand,
234
+ # byte by byte: the boards have no Regexp.
235
+ # @api private
236
+ def self.check_type_name(name)
237
+ s = name.to_s
238
+ parts = s.split("/")
239
+ # (split drops a trailing "/": the parts must give the name back)
240
+ ok = parts.size == 3 && parts.join("/") == s && (parts[1] == "msg" || parts[1] == "srv") &&
241
+ ros_name_part?(parts[0], false) && ros_name_part?(parts[2], true)
242
+ raise ArgumentError, "bad type name #{name.inspect} (want pkg/msg/Name or pkg/srv/Name)" unless ok
243
+ parts
244
+ end
245
+
246
+ # A package name ([a-z][a-z0-9_]*) or, with type true, a type name
247
+ # ([A-Z][A-Za-z0-9]*).
248
+ # @api private
249
+ def self.ros_name_part?(s, type)
250
+ n = s.bytesize
251
+ return false if n == 0
252
+ c = s.getbyte(0)
253
+ return false unless type ? (c >= 65 && c <= 90) : (c >= 97 && c <= 122)
254
+ i = 1
255
+ while i < n
256
+ c = s.getbyte(i)
257
+ ok = (c >= 97 && c <= 122) || (c >= 48 && c <= 57) ||
258
+ (type ? (c >= 65 && c <= 90) : c == 95)
259
+ return false unless ok
260
+ i += 1
261
+ end
262
+ true
263
+ end
264
+
265
+ # What the fields of a type and of every message type it is made of
266
+ # are: {ROS name => FIELD_TYPES}, the type first, each type once. The
267
+ # type is a generated type or its ROS name (the types are loaded with
268
+ # require_type); for a service, its Request and Response. Each entry of
269
+ # FIELD_TYPES is [name, base, kind, nested, capacity, string_capacity]
270
+ # (plain Strings, Integers and nil; see tools/asterism_msggen.rb):
271
+ # Asterism::ROS.field_types("sensor_msgs/msg/JointState")
272
+ # # => {"sensor_msgs/msg/JointState" => [["header", "std_msgs/msg/Header",
273
+ # # "scalar", "std_msgs/msg/Header", nil, nil],
274
+ # # ["name", "string", "sequence", nil, nil, nil], ...],
275
+ # # "std_msgs/msg/Header" => [...], "builtin_interfaces/msg/Time" => [...]}
276
+ # A type without FIELD_TYPES (written by hand, or generated before
277
+ # 0.4.1) raises UnknownType.
278
+ def self.field_types(type)
279
+ t = type_of(type)
280
+ queue = t.is_a?(::Class) ? [t] : [t::Request, t::Response]
281
+ out = {}
282
+ until queue.empty?
283
+ c = queue.shift
284
+ next if out.key?(c::ROS_NAME)
285
+ unless c.const_defined?(:FIELD_TYPES, false)
286
+ raise UnknownType, "#{c::ROS_NAME} has no FIELD_TYPES (generate it with asterism_msggen.rb of 0.4.1 or later)"
287
+ end
288
+ fts = c::FIELD_TYPES
289
+ out[c::ROS_NAME] = fts
290
+ fts.each { |f| queue << require_type(f[3]) if f[3] }
291
+ end
292
+ out
293
+ end
294
+
230
295
  # On mruby the file is evaluated in this VM (Kernel#eval): unlike
231
296
  # require, which runs each file in a Sandbox task of its own that stays
232
297
  # for the life of the VM, this keeps only the compiled code (measured in
@@ -270,8 +335,8 @@ module Asterism
270
335
  end
271
336
 
272
337
  # The base of the generated message types (Request / Response of a
273
- # service too). A type adds ROS_NAME, TYPE_NAME, TYPE_HASH, FIELDS, an
274
- # accessor per field, initialize(**fields) and the CDR steps
338
+ # service too). A type adds ROS_NAME, TYPE_NAME, TYPE_HASH, FIELDS,
339
+ # FIELD_TYPES (0.4.1 and later), an accessor per field, initialize(**fields) and the CDR steps
275
340
  # write(w, msg) / read(r); the rest is here.
276
341
  class Message
277
342
  # A message from nil (all defaults), a Hash of fields (Symbol or
@@ -11,11 +11,12 @@
11
11
  #
12
12
  # Each generated file defines one type (a message, or a service with its
13
13
  # Request and Response) under Asterism::ROS::<PackageInCamelCase>, with its
14
- # fields and defaults, ROS_NAME, TYPE_NAME (the DDS name rmw_zenoh uses),
15
- # TYPE_HASH (RIHS01, computed here) and the CDR conversion in both directions
16
- # on top of Asterism::CDR. A file loads the types it is made of through
17
- # Asterism::ROS.require_type first. The output layout mirrors the ROS names:
18
- # <out>/<pkg>/msg/<Name>.rb and <out>/<pkg>/srv/<Name>.rb.
14
+ # fields and defaults, FIELD_TYPES (what each field is), ROS_NAME,
15
+ # TYPE_NAME (the DDS name rmw_zenoh uses), TYPE_HASH (RIHS01, computed here)
16
+ # and the CDR conversion in both directions on top of Asterism::CDR. A file
17
+ # loads the types it is made of through Asterism::ROS.require_type first.
18
+ # The output layout mirrors the ROS names: <out>/<pkg>/msg/<Name>.rb and
19
+ # <out>/<pkg>/srv/<Name>.rb.
19
20
  #
20
21
  # The type hash follows the type description rules of ROS 2 Jazzy
21
22
  # (rosidl_generator_type_description, REP 2011): the type and every type it
@@ -578,6 +579,7 @@ module AsterismMsgGen
578
579
  out << " TYPE_NAME = #{dds_name(m).inspect}\n"
579
580
  out << " TYPE_HASH = #{hash.inspect}\n"
580
581
  out << " FIELDS = [#{fs.map { |f| ":#{f.name}" }.join(', ')}]\n"
582
+ out << field_types_lit(fs)
581
583
  m.constants.each do |c|
582
584
  out << " #{c.name} = #{lit(c.value, c.base)}\n"
583
585
  end
@@ -616,6 +618,31 @@ module AsterismMsgGen
616
618
  PRIMITIVES.fetch(f.base)[1]
617
619
  end
618
620
 
621
+ # What a field is, for tools that look at a type without its .msg (the
622
+ # same plain values on mruby and CRuby: Arrays, Strings, Integers, nil):
623
+ # [name, base, kind, nested, capacity, string_capacity]
624
+ # base: the .msg type ("float64", "string", "uint8", ...) or, for a
625
+ # message, its full name; kind: "scalar", "array" (T[N]),
626
+ # "bounded_sequence" (T[<=N]) or "sequence" (T[]); nested: the full name
627
+ # of the message type, or nil; capacity: N of an array or a bounded
628
+ # sequence, else nil; string_capacity: N of string<=N, else nil.
629
+ FIELD_KINDS = { nil => "scalar", fixed: "array", bounded: "bounded_sequence", unbounded: "sequence" }.freeze
630
+
631
+ def self.field_type(f)
632
+ cap = f.array == :fixed || f.array == :bounded ? f.size : nil
633
+ [f.name, f.base, FIELD_KINDS.fetch(f.array), f.nested, cap, f.string_max]
634
+ end
635
+
636
+ def field_types_lit(fs)
637
+ return " FIELD_TYPES = []\n" if fs.empty?
638
+ out = +" FIELD_TYPES = [\n"
639
+ fs.each_with_index do |f, i|
640
+ items = Emitter.field_type(f).map { |v| v.nil? ? "nil" : v.inspect }
641
+ out << " [#{items.join(', ')}]#{i < fs.size - 1 ? ',' : ''}\n"
642
+ end
643
+ out << " ]\n"
644
+ end
645
+
619
646
  def byte_kind?(f)
620
647
  BYTE_KINDS.include?(f.base)
621
648
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asterism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katsuhiko Kageyama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-09 00:00:00.000000000 Z
11
+ date: 2026-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asterism-zenoh
@@ -50,10 +50,15 @@ files:
50
50
  - LICENSE
51
51
  - NOTICE
52
52
  - README.md
53
+ - data/icon.png
53
54
  - data/msgs/LICENSE-Apache-2.0.txt
55
+ - data/msgs/LICENSE-BSD-3-Clause-tf2_msgs.txt
54
56
  - data/msgs/NOTICE
55
57
  - data/msgs/builtin_interfaces/msg/Duration.rb
56
58
  - data/msgs/builtin_interfaces/msg/Time.rb
59
+ - data/msgs/diagnostic_msgs/msg/DiagnosticArray.rb
60
+ - data/msgs/diagnostic_msgs/msg/DiagnosticStatus.rb
61
+ - data/msgs/diagnostic_msgs/msg/KeyValue.rb
57
62
  - data/msgs/example_interfaces/srv/AddTwoInts.rb
58
63
  - data/msgs/geometry_msgs/msg/Accel.rb
59
64
  - data/msgs/geometry_msgs/msg/AccelStamped.rb
@@ -63,24 +68,38 @@ files:
63
68
  - data/msgs/geometry_msgs/msg/Pose.rb
64
69
  - data/msgs/geometry_msgs/msg/Pose2D.rb
65
70
  - data/msgs/geometry_msgs/msg/PoseStamped.rb
71
+ - data/msgs/geometry_msgs/msg/PoseWithCovariance.rb
66
72
  - data/msgs/geometry_msgs/msg/Quaternion.rb
67
73
  - data/msgs/geometry_msgs/msg/QuaternionStamped.rb
68
74
  - data/msgs/geometry_msgs/msg/Transform.rb
69
75
  - data/msgs/geometry_msgs/msg/TransformStamped.rb
70
76
  - data/msgs/geometry_msgs/msg/Twist.rb
71
77
  - data/msgs/geometry_msgs/msg/TwistStamped.rb
78
+ - data/msgs/geometry_msgs/msg/TwistWithCovariance.rb
72
79
  - data/msgs/geometry_msgs/msg/Vector3.rb
73
80
  - data/msgs/geometry_msgs/msg/Vector3Stamped.rb
74
81
  - data/msgs/geometry_msgs/msg/Wrench.rb
75
82
  - data/msgs/geometry_msgs/msg/WrenchStamped.rb
83
+ - data/msgs/nav_msgs/msg/MapMetaData.rb
84
+ - data/msgs/nav_msgs/msg/OccupancyGrid.rb
85
+ - data/msgs/nav_msgs/msg/Odometry.rb
86
+ - data/msgs/nav_msgs/msg/Path.rb
87
+ - data/msgs/rcl_interfaces/msg/Log.rb
88
+ - data/msgs/rcl_interfaces/msg/Parameter.rb
89
+ - data/msgs/rcl_interfaces/msg/ParameterEvent.rb
90
+ - data/msgs/rcl_interfaces/msg/ParameterValue.rb
76
91
  - data/msgs/sensor_msgs/msg/BatteryState.rb
92
+ - data/msgs/sensor_msgs/msg/CompressedImage.rb
77
93
  - data/msgs/sensor_msgs/msg/FluidPressure.rb
78
94
  - data/msgs/sensor_msgs/msg/Illuminance.rb
79
95
  - data/msgs/sensor_msgs/msg/Imu.rb
80
96
  - data/msgs/sensor_msgs/msg/JointState.rb
97
+ - data/msgs/sensor_msgs/msg/LaserScan.rb
81
98
  - data/msgs/sensor_msgs/msg/MagneticField.rb
82
99
  - data/msgs/sensor_msgs/msg/NavSatFix.rb
83
100
  - data/msgs/sensor_msgs/msg/NavSatStatus.rb
101
+ - data/msgs/sensor_msgs/msg/PointCloud2.rb
102
+ - data/msgs/sensor_msgs/msg/PointField.rb
84
103
  - data/msgs/sensor_msgs/msg/Range.rb
85
104
  - data/msgs/sensor_msgs/msg/RelativeHumidity.rb
86
105
  - data/msgs/sensor_msgs/msg/Temperature.rb
@@ -114,6 +133,11 @@ files:
114
133
  - data/msgs/std_msgs/msg/UInt64MultiArray.rb
115
134
  - data/msgs/std_msgs/msg/UInt8.rb
116
135
  - data/msgs/std_msgs/msg/UInt8MultiArray.rb
136
+ - data/msgs/tf2_msgs/msg/TFMessage.rb
137
+ - data/msgs/visualization_msgs/msg/Marker.rb
138
+ - data/msgs/visualization_msgs/msg/MarkerArray.rb
139
+ - data/msgs/visualization_msgs/msg/MeshFile.rb
140
+ - data/msgs/visualization_msgs/msg/UVCoordinate.rb
117
141
  - lib/asterism.rb
118
142
  - lib/asterism/cruby.rb
119
143
  - lib/asterism/cruby/objects.rb
@@ -124,6 +148,7 @@ files:
124
148
  - mrblib/asterism.rb
125
149
  - mrblib/cdr.rb
126
150
  - mrblib/future.rb
151
+ - mrblib/profile.rb
127
152
  - mrblib/proxy.rb
128
153
  - mrblib/ros.rb
129
154
  - tools/asterism_msggen.rb
@@ -131,6 +156,7 @@ homepage: https://github.com/ruby-asterism/asterism
131
156
  licenses:
132
157
  - MIT
133
158
  - Apache-2.0
159
+ - BSD-3-Clause
134
160
  metadata:
135
161
  homepage_uri: https://github.com/ruby-asterism/asterism
136
162
  source_code_uri: https://github.com/ruby-asterism/asterism