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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -4
- data/NOTICE +12 -7
- data/README.md +148 -17
- data/data/icon.png +0 -0
- data/data/msgs/LICENSE-BSD-3-Clause-tf2_msgs.txt +29 -0
- data/data/msgs/NOTICE +19 -13
- data/data/msgs/builtin_interfaces/msg/Duration.rb +4 -0
- data/data/msgs/builtin_interfaces/msg/Time.rb +4 -0
- data/data/msgs/diagnostic_msgs/msg/DiagnosticArray.rb +40 -0
- data/data/msgs/diagnostic_msgs/msg/DiagnosticStatus.rb +55 -0
- data/data/msgs/diagnostic_msgs/msg/KeyValue.rb +37 -0
- data/data/msgs/example_interfaces/srv/AddTwoInts.rb +7 -0
- data/data/msgs/geometry_msgs/msg/Accel.rb +4 -0
- data/data/msgs/geometry_msgs/msg/AccelStamped.rb +4 -0
- data/data/msgs/geometry_msgs/msg/Point.rb +5 -0
- data/data/msgs/geometry_msgs/msg/Point32.rb +5 -0
- data/data/msgs/geometry_msgs/msg/PointStamped.rb +4 -0
- data/data/msgs/geometry_msgs/msg/Pose.rb +4 -0
- data/data/msgs/geometry_msgs/msg/Pose2D.rb +5 -0
- data/data/msgs/geometry_msgs/msg/PoseStamped.rb +4 -0
- data/data/msgs/geometry_msgs/msg/PoseWithCovariance.rb +39 -0
- data/data/msgs/geometry_msgs/msg/Quaternion.rb +6 -0
- data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +4 -0
- data/data/msgs/geometry_msgs/msg/Transform.rb +4 -0
- data/data/msgs/geometry_msgs/msg/TransformStamped.rb +5 -0
- data/data/msgs/geometry_msgs/msg/Twist.rb +4 -0
- data/data/msgs/geometry_msgs/msg/TwistStamped.rb +4 -0
- data/data/msgs/geometry_msgs/msg/TwistWithCovariance.rb +39 -0
- data/data/msgs/geometry_msgs/msg/Vector3.rb +5 -0
- data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +4 -0
- data/data/msgs/geometry_msgs/msg/Wrench.rb +4 -0
- data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +4 -0
- data/data/msgs/nav_msgs/msg/MapMetaData.rb +52 -0
- data/data/msgs/nav_msgs/msg/OccupancyGrid.rb +44 -0
- data/data/msgs/nav_msgs/msg/Odometry.rb +49 -0
- data/data/msgs/nav_msgs/msg/Path.rb +40 -0
- data/data/msgs/rcl_interfaces/msg/Log.rb +64 -0
- data/data/msgs/rcl_interfaces/msg/Parameter.rb +39 -0
- data/data/msgs/rcl_interfaces/msg/ParameterEvent.rb +52 -0
- data/data/msgs/rcl_interfaces/msg/ParameterValue.rb +69 -0
- data/data/msgs/sensor_msgs/msg/BatteryState.rb +18 -0
- data/data/msgs/sensor_msgs/msg/CompressedImage.rb +43 -0
- data/data/msgs/sensor_msgs/msg/FluidPressure.rb +5 -0
- data/data/msgs/sensor_msgs/msg/Illuminance.rb +5 -0
- data/data/msgs/sensor_msgs/msg/Imu.rb +9 -0
- data/data/msgs/sensor_msgs/msg/JointState.rb +7 -0
- data/data/msgs/sensor_msgs/msg/LaserScan.rb +71 -0
- data/data/msgs/sensor_msgs/msg/MagneticField.rb +5 -0
- data/data/msgs/sensor_msgs/msg/NavSatFix.rb +9 -0
- data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +4 -0
- data/data/msgs/sensor_msgs/msg/PointCloud2.rb +68 -0
- data/data/msgs/sensor_msgs/msg/PointField.rb +53 -0
- data/data/msgs/sensor_msgs/msg/Range.rb +9 -0
- data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +5 -0
- data/data/msgs/sensor_msgs/msg/Temperature.rb +5 -0
- data/data/msgs/std_msgs/msg/Bool.rb +3 -0
- data/data/msgs/std_msgs/msg/Byte.rb +3 -0
- data/data/msgs/std_msgs/msg/ByteMultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/Char.rb +3 -0
- data/data/msgs/std_msgs/msg/ColorRGBA.rb +6 -0
- data/data/msgs/std_msgs/msg/Empty.rb +1 -0
- data/data/msgs/std_msgs/msg/Float32.rb +3 -0
- data/data/msgs/std_msgs/msg/Float32MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/Float64.rb +3 -0
- data/data/msgs/std_msgs/msg/Float64MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/Header.rb +4 -0
- data/data/msgs/std_msgs/msg/Int16.rb +3 -0
- data/data/msgs/std_msgs/msg/Int16MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/Int32.rb +3 -0
- data/data/msgs/std_msgs/msg/Int32MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/Int64.rb +3 -0
- data/data/msgs/std_msgs/msg/Int64MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/Int8.rb +3 -0
- data/data/msgs/std_msgs/msg/Int8MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +5 -0
- data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +4 -0
- data/data/msgs/std_msgs/msg/String.rb +3 -0
- data/data/msgs/std_msgs/msg/UInt16.rb +3 -0
- data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/UInt32.rb +3 -0
- data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/UInt64.rb +3 -0
- data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +4 -0
- data/data/msgs/std_msgs/msg/UInt8.rb +3 -0
- data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +4 -0
- data/data/msgs/tf2_msgs/msg/TFMessage.rb +35 -0
- data/data/msgs/visualization_msgs/msg/Marker.rb +132 -0
- data/data/msgs/visualization_msgs/msg/MarkerArray.rb +35 -0
- data/data/msgs/visualization_msgs/msg/MeshFile.rb +37 -0
- data/data/msgs/visualization_msgs/msg/UVCoordinate.rb +37 -0
- data/lib/asterism/cruby/objects.rb +13 -1
- data/lib/asterism/cruby.rb +38 -0
- data/lib/asterism/version.rb +4 -3
- data/lib/asterism.rb +1 -1
- data/mrblib/asterism.rb +5 -0
- data/mrblib/profile.rb +362 -0
- data/mrblib/ros.rb +71 -6
- data/tools/asterism_msggen.rb +32 -5
- 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
|
|
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,
|
|
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
|
data/tools/asterism_msggen.rb
CHANGED
|
@@ -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,
|
|
15
|
-
# TYPE_HASH (RIHS01, computed here)
|
|
16
|
-
# on top of Asterism::CDR. A file
|
|
17
|
-
#
|
|
18
|
-
# <out>/<pkg>/msg/<Name>.rb and
|
|
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
|
+
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-
|
|
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
|