asterism 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/NOTICE +27 -0
- data/README.md +581 -3
- data/data/msgs/LICENSE-Apache-2.0.txt +202 -0
- data/data/msgs/NOTICE +17 -0
- data/data/msgs/builtin_interfaces/msg/Duration.rb +33 -0
- data/data/msgs/builtin_interfaces/msg/Time.rb +33 -0
- data/data/msgs/example_interfaces/srv/AddTwoInts.rb +64 -0
- data/data/msgs/geometry_msgs/msg/Accel.rb +35 -0
- data/data/msgs/geometry_msgs/msg/AccelStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Point.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Point32.rb +36 -0
- data/data/msgs/geometry_msgs/msg/PointStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Pose.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Pose2D.rb +36 -0
- data/data/msgs/geometry_msgs/msg/PoseStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Quaternion.rb +39 -0
- data/data/msgs/geometry_msgs/msg/QuaternionStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Transform.rb +36 -0
- data/data/msgs/geometry_msgs/msg/TransformStamped.rb +39 -0
- data/data/msgs/geometry_msgs/msg/Twist.rb +35 -0
- data/data/msgs/geometry_msgs/msg/TwistStamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Vector3.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Vector3Stamped.rb +36 -0
- data/data/msgs/geometry_msgs/msg/Wrench.rb +35 -0
- data/data/msgs/geometry_msgs/msg/WrenchStamped.rb +36 -0
- data/data/msgs/sensor_msgs/msg/BatteryState.rb +100 -0
- data/data/msgs/sensor_msgs/msg/FluidPressure.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Illuminance.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Imu.rb +52 -0
- data/data/msgs/sensor_msgs/msg/JointState.rb +44 -0
- data/data/msgs/sensor_msgs/msg/MagneticField.rb +39 -0
- data/data/msgs/sensor_msgs/msg/NavSatFix.rb +55 -0
- data/data/msgs/sensor_msgs/msg/NavSatStatus.rb +43 -0
- data/data/msgs/sensor_msgs/msg/Range.rb +52 -0
- data/data/msgs/sensor_msgs/msg/RelativeHumidity.rb +38 -0
- data/data/msgs/sensor_msgs/msg/Temperature.rb +38 -0
- data/data/msgs/std_msgs/msg/Bool.rb +30 -0
- data/data/msgs/std_msgs/msg/Byte.rb +30 -0
- data/data/msgs/std_msgs/msg/ByteMultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Char.rb +30 -0
- data/data/msgs/std_msgs/msg/ColorRGBA.rb +39 -0
- data/data/msgs/std_msgs/msg/Empty.rb +26 -0
- data/data/msgs/std_msgs/msg/Float32.rb +30 -0
- data/data/msgs/std_msgs/msg/Float32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Float64.rb +30 -0
- data/data/msgs/std_msgs/msg/Float64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Header.rb +35 -0
- data/data/msgs/std_msgs/msg/Int16.rb +30 -0
- data/data/msgs/std_msgs/msg/Int16MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int32.rb +30 -0
- data/data/msgs/std_msgs/msg/Int32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int64.rb +30 -0
- data/data/msgs/std_msgs/msg/Int64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/Int8.rb +30 -0
- data/data/msgs/std_msgs/msg/Int8MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/MultiArrayDimension.rb +36 -0
- data/data/msgs/std_msgs/msg/MultiArrayLayout.rb +35 -0
- data/data/msgs/std_msgs/msg/String.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt16.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt16MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt32.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt32MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt64.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt64MultiArray.rb +35 -0
- data/data/msgs/std_msgs/msg/UInt8.rb +30 -0
- data/data/msgs/std_msgs/msg/UInt8MultiArray.rb +35 -0
- data/lib/asterism/cruby/objects.rb +224 -0
- data/lib/asterism/cruby/ros.rb +305 -0
- data/lib/asterism/cruby/runner.rb +253 -0
- data/lib/asterism/cruby/zenoh.rb +422 -0
- data/lib/asterism/cruby.rb +30 -0
- data/lib/asterism/version.rb +5 -0
- data/lib/asterism.rb +22 -4
- data/mrblib/asterism.rb +617 -0
- data/mrblib/cdr.rb +512 -0
- data/mrblib/future.rb +88 -0
- data/mrblib/proxy.rb +97 -0
- data/mrblib/ros.rb +861 -0
- data/tools/asterism_msggen.rb +826 -0
- metadata +122 -9
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# asterism_msggen.rb: ROS 2 .msg / .srv -> pure Ruby message types for
|
|
5
|
+
# Asterism::ROS (doc/ruby_asterism, R3). CRuby with the standard library only.
|
|
6
|
+
#
|
|
7
|
+
# ruby asterism_msggen.rb -I /opt/ros/jazzy/share -o out geometry_msgs/msg/Twist
|
|
8
|
+
# ruby asterism_msggen.rb -I share -I my_ws/src -o out my_pkg/msg/Foo.msg
|
|
9
|
+
# ruby asterism_msggen.rb -I share --hash std_msgs/msg/String
|
|
10
|
+
# ruby asterism_msggen.rb -I share --check-json share --all
|
|
11
|
+
#
|
|
12
|
+
# Each generated file defines one type (a message, or a service with its
|
|
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.
|
|
19
|
+
#
|
|
20
|
+
# The type hash follows the type description rules of ROS 2 Jazzy
|
|
21
|
+
# (rosidl_generator_type_description, REP 2011): the type and every type it
|
|
22
|
+
# references, as {type_name, fields: [{name, type: {type_id, capacity,
|
|
23
|
+
# string_capacity, nested_type_name}}]}, without default values, referenced
|
|
24
|
+
# types sorted by name, dumped like Python's json.dumps with separators
|
|
25
|
+
# (", ", ": ") and ensure_ascii, then SHA-256.
|
|
26
|
+
require "digest"
|
|
27
|
+
require "fileutils"
|
|
28
|
+
require "json"
|
|
29
|
+
require "optparse"
|
|
30
|
+
|
|
31
|
+
module AsterismMsgGen
|
|
32
|
+
ROS_DISTRO = "jazzy"
|
|
33
|
+
|
|
34
|
+
# .msg primitive -> [type description id, CDR kind used by Asterism::CDR]
|
|
35
|
+
PRIMITIVES = {
|
|
36
|
+
"bool" => [15, :bool],
|
|
37
|
+
"byte" => [16, :uint8],
|
|
38
|
+
"char" => [3, :uint8], # .msg char is uint8 in the IDL
|
|
39
|
+
"int8" => [2, :int8],
|
|
40
|
+
"uint8" => [3, :uint8],
|
|
41
|
+
"int16" => [4, :int16],
|
|
42
|
+
"uint16" => [5, :uint16],
|
|
43
|
+
"int32" => [6, :int32],
|
|
44
|
+
"uint32" => [7, :uint32],
|
|
45
|
+
"int64" => [8, :int64],
|
|
46
|
+
"uint64" => [9, :uint64],
|
|
47
|
+
"float32" => [10, :float32],
|
|
48
|
+
"float64" => [11, :float64],
|
|
49
|
+
"string" => [17, :string],
|
|
50
|
+
"wstring" => [18, :wstring]
|
|
51
|
+
}.freeze
|
|
52
|
+
NESTED_ID = 1
|
|
53
|
+
BOUNDED_STRING_ID = 21
|
|
54
|
+
BOUNDED_WSTRING_ID = 22
|
|
55
|
+
ARRAY_OFFSET = { fixed: 48, bounded: 96, unbounded: 144 }.freeze
|
|
56
|
+
# Kinds that are Strings of bytes when they come as an array or sequence.
|
|
57
|
+
BYTE_KINDS = %w[byte char uint8].freeze
|
|
58
|
+
FLOAT_KINDS = %w[float32 float64].freeze
|
|
59
|
+
INT_KINDS = %w[int8 uint8 int16 uint16 int32 uint32 int64 uint64 byte char].freeze
|
|
60
|
+
|
|
61
|
+
RUBY_RESERVED = %w[
|
|
62
|
+
BEGIN END alias and begin break case class def defined? do else elsif end
|
|
63
|
+
ensure false for if in module next nil not or redo rescue retry return
|
|
64
|
+
self super then true undef unless until when while yield __FILE__ __LINE__
|
|
65
|
+
__ENCODING__ hash class object_id send method initialize
|
|
66
|
+
].freeze
|
|
67
|
+
|
|
68
|
+
class Error < StandardError; end
|
|
69
|
+
|
|
70
|
+
# One field: base is the .msg type word ("float64", "std_msgs/msg/Header"),
|
|
71
|
+
# nested is the full name of a message type (nil for primitives).
|
|
72
|
+
Field = Struct.new(:name, :base, :nested, :array, :size, :string_max, :default, keyword_init: true) do
|
|
73
|
+
def primitive?
|
|
74
|
+
nested.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def string?
|
|
78
|
+
base == "string" || base == "wstring"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Constant = Struct.new(:name, :base, :value, keyword_init: true)
|
|
83
|
+
|
|
84
|
+
# A message (also a service's Request / Response / Event).
|
|
85
|
+
Message = Struct.new(:full_name, :pkg, :name, :fields, :constants, :source, :service, keyword_init: true)
|
|
86
|
+
|
|
87
|
+
Service = Struct.new(:full_name, :pkg, :name, :request, :response, :event, :source, keyword_init: true)
|
|
88
|
+
|
|
89
|
+
# "pkg/msg/Name" or "pkg/srv/Name" -> [pkg, kind, name]
|
|
90
|
+
def self.split_name(full)
|
|
91
|
+
parts = full.split("/")
|
|
92
|
+
raise Error, "bad type name #{full.inspect} (want pkg/msg/Name)" unless parts.size == 3
|
|
93
|
+
parts
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.camel(pkg)
|
|
97
|
+
pkg.split("_").map { |w| w.empty? ? "" : w[0].upcase + w[1..] }.join
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ---- parsing -------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
class Parser
|
|
103
|
+
TYPE_RE = %r{\A([A-Za-z][A-Za-z0-9_]*(?:/[A-Za-z][A-Za-z0-9_]*)?)(?:<=(\d+))?(?:\[(<=)?(\d*)\])?\z}
|
|
104
|
+
FIELD_NAME_RE = /\A[a-z][a-z0-9_]*\z/
|
|
105
|
+
CONST_NAME_RE = /\A[A-Z][A-Z0-9_]*\z/
|
|
106
|
+
|
|
107
|
+
def initialize(pkg, source)
|
|
108
|
+
@pkg = pkg
|
|
109
|
+
@source = source
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Lines of a .msg body -> [fields, constants]
|
|
113
|
+
def parse_lines(lines)
|
|
114
|
+
fields = []
|
|
115
|
+
constants = []
|
|
116
|
+
lines.each_with_index do |raw, i|
|
|
117
|
+
line = strip_comment(raw).strip
|
|
118
|
+
next if line.empty?
|
|
119
|
+
begin
|
|
120
|
+
parse_line(line, fields, constants)
|
|
121
|
+
rescue Error => e
|
|
122
|
+
raise Error, "#{@source}:#{i + 1}: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
[fields, constants]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def strip_comment(line)
|
|
129
|
+
quote = nil
|
|
130
|
+
line.each_char.with_index do |c, i|
|
|
131
|
+
if quote
|
|
132
|
+
quote = nil if c == quote
|
|
133
|
+
elsif c == '"' || c == "'"
|
|
134
|
+
quote = c
|
|
135
|
+
elsif c == "#"
|
|
136
|
+
return line[0, i]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
line
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_line(line, fields, constants)
|
|
143
|
+
type_word, rest = line.split(/\s+/, 2)
|
|
144
|
+
raise Error, "no name after #{type_word}" if rest.nil? || rest.empty?
|
|
145
|
+
if (m = rest.match(/\A([A-Za-z][A-Za-z0-9_]*)\s*=\s*(.*)\z/)) && !rest.match?(/\A[a-z][a-z0-9_]*\s+\S/)
|
|
146
|
+
constants << parse_constant(type_word, m[1], m[2].strip)
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
name, default = rest.split(/\s+/, 2)
|
|
150
|
+
raise Error, "bad field name #{name.inspect}" unless FIELD_NAME_RE.match?(name)
|
|
151
|
+
raise Error, "field name #{name.inspect} clashes with Ruby" if RUBY_RESERVED.include?(name)
|
|
152
|
+
f = parse_type(type_word)
|
|
153
|
+
f.name = name
|
|
154
|
+
f.default = default.nil? || default.empty? ? nil : parse_default(f, default.strip)
|
|
155
|
+
fields << f
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_type(word)
|
|
159
|
+
m = TYPE_RE.match(word)
|
|
160
|
+
raise Error, "bad type #{word.inspect}" unless m
|
|
161
|
+
base = m[1]
|
|
162
|
+
f = Field.new
|
|
163
|
+
if m[2]
|
|
164
|
+
raise Error, "only strings can be bounded: #{word}" unless %w[string wstring].include?(base)
|
|
165
|
+
f.string_max = m[2].to_i
|
|
166
|
+
end
|
|
167
|
+
if word.include?("[")
|
|
168
|
+
if m[3]
|
|
169
|
+
f.array = :bounded
|
|
170
|
+
f.size = m[4].to_i
|
|
171
|
+
raise Error, "bounded sequence without a bound: #{word}" if m[4].empty?
|
|
172
|
+
elsif m[4].empty?
|
|
173
|
+
f.array = :unbounded
|
|
174
|
+
else
|
|
175
|
+
f.array = :fixed
|
|
176
|
+
f.size = m[4].to_i
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
if PRIMITIVES.key?(base)
|
|
180
|
+
f.base = base
|
|
181
|
+
else
|
|
182
|
+
f.nested = resolve(base)
|
|
183
|
+
f.base = f.nested
|
|
184
|
+
end
|
|
185
|
+
f
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# A nested type word of a .msg -> its full name.
|
|
189
|
+
def resolve(base)
|
|
190
|
+
if base.include?("/")
|
|
191
|
+
pkg, name = base.split("/")
|
|
192
|
+
"#{pkg}/msg/#{name}"
|
|
193
|
+
elsif base == "Header"
|
|
194
|
+
"std_msgs/msg/Header"
|
|
195
|
+
else
|
|
196
|
+
"#{@pkg}/msg/#{base}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def parse_constant(type_word, name, value)
|
|
201
|
+
raise Error, "bad constant name #{name.inspect}" unless CONST_NAME_RE.match?(name)
|
|
202
|
+
raise Error, "constants must be primitive: #{type_word}" unless PRIMITIVES.key?(type_word)
|
|
203
|
+
v = type_word == "string" || type_word == "wstring" ? value : scalar(type_word, value)
|
|
204
|
+
Constant.new(name: name, base: type_word, value: v)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def parse_default(f, text)
|
|
208
|
+
raise Error, "nested fields have no default" unless f.primitive?
|
|
209
|
+
if f.array
|
|
210
|
+
raise Error, "array default must be [..]: #{text}" unless text.start_with?("[") && text.end_with?("]")
|
|
211
|
+
items = split_list(text[1..-2])
|
|
212
|
+
items.map { |s| f.string? ? unquote(s) : scalar(f.base, s) }
|
|
213
|
+
elsif f.string?
|
|
214
|
+
unquote(text)
|
|
215
|
+
else
|
|
216
|
+
scalar(f.base, text)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def split_list(body)
|
|
221
|
+
out = []
|
|
222
|
+
cur = +""
|
|
223
|
+
quote = nil
|
|
224
|
+
body.each_char do |c|
|
|
225
|
+
if quote
|
|
226
|
+
cur << c
|
|
227
|
+
quote = nil if c == quote
|
|
228
|
+
elsif c == '"' || c == "'"
|
|
229
|
+
quote = c
|
|
230
|
+
cur << c
|
|
231
|
+
elsif c == ","
|
|
232
|
+
out << cur.strip
|
|
233
|
+
cur = +""
|
|
234
|
+
else
|
|
235
|
+
cur << c
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
out << cur.strip unless cur.strip.empty?
|
|
239
|
+
out
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def unquote(s)
|
|
243
|
+
if s.size >= 2 && (s[0] == '"' || s[0] == "'") && s[-1] == s[0]
|
|
244
|
+
s[1..-2].gsub("\\#{s[0]}", s[0])
|
|
245
|
+
else
|
|
246
|
+
s
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def scalar(base, s)
|
|
251
|
+
case base
|
|
252
|
+
when "bool"
|
|
253
|
+
return true if %w[true True 1].include?(s)
|
|
254
|
+
return false if %w[false False 0].include?(s)
|
|
255
|
+
raise Error, "bad bool #{s.inspect}"
|
|
256
|
+
when *FLOAT_KINDS
|
|
257
|
+
Float(s)
|
|
258
|
+
else
|
|
259
|
+
Integer(s)
|
|
260
|
+
end
|
|
261
|
+
rescue ArgumentError
|
|
262
|
+
raise Error, "bad #{base} value #{s.inspect}"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ---- the set of known types --------------------------------------------
|
|
267
|
+
|
|
268
|
+
class Registry
|
|
269
|
+
attr_reader :include_dirs
|
|
270
|
+
|
|
271
|
+
def initialize(include_dirs)
|
|
272
|
+
@include_dirs = include_dirs
|
|
273
|
+
@messages = {}
|
|
274
|
+
@services = {}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The .msg / .srv files read so far.
|
|
278
|
+
def sources
|
|
279
|
+
(@messages.values + @services.values).map(&:source).uniq.sort
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# A message by full name ("pkg/msg/Name", or a service part such as
|
|
283
|
+
# "pkg/srv/Name_Request"), parsed on first use.
|
|
284
|
+
def message(full)
|
|
285
|
+
return @messages[full] if @messages.key?(full)
|
|
286
|
+
pkg, kind, name = AsterismMsgGen.split_name(full)
|
|
287
|
+
if kind == "srv"
|
|
288
|
+
base = name.sub(/_(Request|Response|Event)\z/, "")
|
|
289
|
+
raise Error, "unknown service part #{full}" if base == name
|
|
290
|
+
service("#{pkg}/srv/#{base}")
|
|
291
|
+
return @messages.fetch(full)
|
|
292
|
+
end
|
|
293
|
+
raise Error, "not a message: #{full}" unless kind == "msg"
|
|
294
|
+
path = find("#{pkg}/msg/#{name}.msg")
|
|
295
|
+
load_msg_file(path, pkg, name)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def service(full)
|
|
299
|
+
return @services[full] if @services.key?(full)
|
|
300
|
+
pkg, kind, name = AsterismMsgGen.split_name(full)
|
|
301
|
+
raise Error, "not a service: #{full}" unless kind == "srv"
|
|
302
|
+
path = find("#{pkg}/srv/#{name}.srv")
|
|
303
|
+
load_srv_file(path, pkg, name)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def type(full)
|
|
307
|
+
_, kind, = AsterismMsgGen.split_name(full)
|
|
308
|
+
kind == "srv" && !full.match?(/_(Request|Response|Event)\z/) ? service(full) : message(full)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# The license a package declares in its package.xml (ROS layout:
|
|
312
|
+
# <dir>/<pkg>/package.xml), or nil when there is none. Several
|
|
313
|
+
# <license> tags are joined with ", ".
|
|
314
|
+
def license_of(pkg)
|
|
315
|
+
@licenses ||= {}
|
|
316
|
+
return @licenses[pkg] if @licenses.key?(pkg)
|
|
317
|
+
@licenses[pkg] = nil
|
|
318
|
+
@include_dirs.each do |d|
|
|
319
|
+
xml = File.join(d, pkg, "package.xml")
|
|
320
|
+
next unless File.file?(xml)
|
|
321
|
+
names = File.read(xml).scan(%r{<license(?:\s[^>]*)?>([^<]+)</license>}).map { |m| m[0].strip }
|
|
322
|
+
@licenses[pkg] = names.join(", ") unless names.empty?
|
|
323
|
+
break
|
|
324
|
+
end
|
|
325
|
+
@licenses[pkg]
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def find(rel)
|
|
329
|
+
@include_dirs.each do |d|
|
|
330
|
+
p = File.join(d, rel)
|
|
331
|
+
return p if File.file?(p)
|
|
332
|
+
end
|
|
333
|
+
raise Error, "#{rel} not found under #{@include_dirs.join(', ')}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# A .msg / .srv path -> full name. The package is the directory above
|
|
337
|
+
# msg/ or srv/ (the ROS layout).
|
|
338
|
+
def name_of_file(path)
|
|
339
|
+
dir = File.basename(File.dirname(path))
|
|
340
|
+
pkg = File.basename(File.dirname(File.dirname(path)))
|
|
341
|
+
ext = File.extname(path)
|
|
342
|
+
kind = ext == ".srv" ? "srv" : "msg"
|
|
343
|
+
raise Error, "#{path}: expected <pkg>/#{kind}/<Name>#{ext}" unless dir == kind
|
|
344
|
+
full = "#{pkg}/#{kind}/#{File.basename(path, ext)}"
|
|
345
|
+
if kind == "srv"
|
|
346
|
+
@services[full] ||= load_srv_file(path, pkg, File.basename(path, ext))
|
|
347
|
+
else
|
|
348
|
+
@messages[full] ||= load_msg_file(path, pkg, File.basename(path, ext))
|
|
349
|
+
end
|
|
350
|
+
full
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def load_msg_file(path, pkg, name)
|
|
354
|
+
full = "#{pkg}/msg/#{name}"
|
|
355
|
+
fields, constants = Parser.new(pkg, path).parse_lines(File.read(path).lines)
|
|
356
|
+
@messages[full] = Message.new(full_name: full, pkg: pkg, name: name, fields: fields,
|
|
357
|
+
constants: constants, source: path)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def load_srv_file(path, pkg, name)
|
|
361
|
+
full = "#{pkg}/srv/#{name}"
|
|
362
|
+
lines = File.read(path).lines
|
|
363
|
+
sep = lines.index { |l| l.strip == "---" }
|
|
364
|
+
raise Error, "#{path}: no --- line" unless sep
|
|
365
|
+
parser = Parser.new(pkg, path)
|
|
366
|
+
parts = {}
|
|
367
|
+
[["Request", lines[0...sep]], ["Response", lines[(sep + 1)..]]].each do |suffix, body|
|
|
368
|
+
fields, constants = parser.parse_lines(body)
|
|
369
|
+
mfull = "#{full}_#{suffix}"
|
|
370
|
+
parts[suffix] = @messages[mfull] = Message.new(full_name: mfull, pkg: pkg, name: "#{name}_#{suffix}",
|
|
371
|
+
fields: fields, constants: constants, source: path,
|
|
372
|
+
service: full)
|
|
373
|
+
end
|
|
374
|
+
# The event message every service has (REP 2011 / service introspection).
|
|
375
|
+
event_full = "#{full}_Event"
|
|
376
|
+
event_fields = [
|
|
377
|
+
Field.new(name: "info", base: "service_msgs/msg/ServiceEventInfo", nested: "service_msgs/msg/ServiceEventInfo"),
|
|
378
|
+
Field.new(name: "request", base: parts["Request"].full_name, nested: parts["Request"].full_name, array: :bounded, size: 1),
|
|
379
|
+
Field.new(name: "response", base: parts["Response"].full_name, nested: parts["Response"].full_name, array: :bounded, size: 1)
|
|
380
|
+
]
|
|
381
|
+
@messages[event_full] = Message.new(full_name: event_full, pkg: pkg, name: "#{name}_Event",
|
|
382
|
+
fields: event_fields, constants: [], source: path, service: full)
|
|
383
|
+
@services[full] = Service.new(full_name: full, pkg: pkg, name: name, request: parts["Request"],
|
|
384
|
+
response: parts["Response"], event: @messages[event_full], source: path)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# ---- type hashes ------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
class Hasher
|
|
391
|
+
def initialize(registry)
|
|
392
|
+
@reg = registry
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def field_type(f)
|
|
396
|
+
id = if f.nested
|
|
397
|
+
NESTED_ID
|
|
398
|
+
elsif f.string_max
|
|
399
|
+
f.base == "string" ? BOUNDED_STRING_ID : BOUNDED_WSTRING_ID
|
|
400
|
+
else
|
|
401
|
+
PRIMITIVES.fetch(f.base)[0]
|
|
402
|
+
end
|
|
403
|
+
id += ARRAY_OFFSET[f.array] if f.array
|
|
404
|
+
{
|
|
405
|
+
"type_id" => id,
|
|
406
|
+
"capacity" => f.array == :fixed || f.array == :bounded ? f.size : 0,
|
|
407
|
+
"string_capacity" => f.string_max || 0,
|
|
408
|
+
"nested_type_name" => f.nested || ""
|
|
409
|
+
}
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# The individual description of a type (no references), without defaults.
|
|
413
|
+
def individual(full)
|
|
414
|
+
t = @reg.type(full)
|
|
415
|
+
fields = if t.is_a?(Service)
|
|
416
|
+
[["request_message", t.request.full_name], ["response_message", t.response.full_name],
|
|
417
|
+
["event_message", t.event.full_name]].map do |n, nested|
|
|
418
|
+
{ "name" => n, "type" => field_type(Field.new(base: nested, nested: nested)) }
|
|
419
|
+
end
|
|
420
|
+
else
|
|
421
|
+
fs = t.fields
|
|
422
|
+
if fs.empty?
|
|
423
|
+
# rosidl gives an empty structure one placeholder member.
|
|
424
|
+
fs = [Field.new(name: "structure_needs_at_least_one_member", base: "uint8")]
|
|
425
|
+
end
|
|
426
|
+
fs.map { |f| { "name" => f.name, "type" => field_type(f) } }
|
|
427
|
+
end
|
|
428
|
+
{ "type_name" => full, "fields" => fields }
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def full_description(full)
|
|
432
|
+
top = individual(full)
|
|
433
|
+
refs = {}
|
|
434
|
+
queue = top["fields"].map { |f| f["type"]["nested_type_name"] }.reject(&:empty?)
|
|
435
|
+
until queue.empty?
|
|
436
|
+
n = queue.pop
|
|
437
|
+
next if refs.key?(n)
|
|
438
|
+
refs[n] = individual(n)
|
|
439
|
+
queue.concat(refs[n]["fields"].map { |f| f["type"]["nested_type_name"] }.reject(&:empty?))
|
|
440
|
+
end
|
|
441
|
+
{ "type_description" => top, "referenced_type_descriptions" => refs.keys.sort.map { |k| refs[k] } }
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def type_hash(full)
|
|
445
|
+
"RIHS01_" + Digest::SHA256.hexdigest(AsterismMsgGen.py_json(full_description(full)))
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Python's json.dumps(obj, ensure_ascii=True, separators=(", ", ": ")).
|
|
450
|
+
def self.py_json(o)
|
|
451
|
+
case o
|
|
452
|
+
when Hash then "{" + o.map { |k, v| "#{py_str(k.to_s)}: #{py_json(v)}" }.join(", ") + "}"
|
|
453
|
+
when Array then "[" + o.map { |v| py_json(v) }.join(", ") + "]"
|
|
454
|
+
when String then py_str(o)
|
|
455
|
+
when Integer then o.to_s
|
|
456
|
+
when true then "true"
|
|
457
|
+
when false then "false"
|
|
458
|
+
when nil then "null"
|
|
459
|
+
else raise Error, "cannot dump #{o.class}"
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def self.py_str(s)
|
|
464
|
+
out = +'"'
|
|
465
|
+
s.each_char do |c|
|
|
466
|
+
out << case c
|
|
467
|
+
when '"' then '\\"'
|
|
468
|
+
when "\\" then "\\\\"
|
|
469
|
+
when "\n" then "\\n"
|
|
470
|
+
when "\r" then "\\r"
|
|
471
|
+
when "\t" then "\\t"
|
|
472
|
+
when "\b" then "\\b"
|
|
473
|
+
when "\f" then "\\f"
|
|
474
|
+
else
|
|
475
|
+
cp = c.ord
|
|
476
|
+
if cp < 0x20 || cp > 0x7e
|
|
477
|
+
if cp > 0xffff
|
|
478
|
+
cp -= 0x10000
|
|
479
|
+
format("\\u%04x\\u%04x", 0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff))
|
|
480
|
+
else
|
|
481
|
+
format("\\u%04x", cp)
|
|
482
|
+
end
|
|
483
|
+
else
|
|
484
|
+
c
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
out << '"'
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# ---- Ruby code --------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
class Emitter
|
|
494
|
+
def initialize(registry, hasher)
|
|
495
|
+
@reg = registry
|
|
496
|
+
@hasher = hasher
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def self.rel_path(full)
|
|
500
|
+
"#{full}.rb"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# The Ruby constant path of a type, e.g. ::Asterism::ROS::GeometryMsgs::Twist
|
|
504
|
+
def const_of(full)
|
|
505
|
+
pkg, kind, name = AsterismMsgGen.split_name(full)
|
|
506
|
+
if kind == "srv" && (m = name.match(/\A(.+)_(Request|Response|Event)\z/))
|
|
507
|
+
"::Asterism::ROS::#{AsterismMsgGen.camel(pkg)}::#{m[1]}::#{m[2]}"
|
|
508
|
+
else
|
|
509
|
+
"::Asterism::ROS::#{AsterismMsgGen.camel(pkg)}::#{name}"
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# The types a file must load first (the messages its fields are made of,
|
|
514
|
+
# other than the ones it defines itself).
|
|
515
|
+
def runtime_deps(t)
|
|
516
|
+
msgs = t.is_a?(Service) ? [t.request, t.response] : [t]
|
|
517
|
+
own = msgs.map(&:full_name)
|
|
518
|
+
msgs.flat_map { |m| m.fields.map(&:nested).compact }.uniq.reject { |n| own.include?(n) }.sort
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def file(full)
|
|
522
|
+
t = @reg.type(full)
|
|
523
|
+
out = +""
|
|
524
|
+
out << "# #{full} (ROS 2 #{ROS_DISTRO}), generated by asterism_msggen.rb from\n"
|
|
525
|
+
out << "# #{File.basename(t.source)} of the package #{t.pkg}.\n"
|
|
526
|
+
# Where the definition came from and under which terms: the license
|
|
527
|
+
# its package.xml declares, which this file (made from it) shares.
|
|
528
|
+
lic = @reg.license_of(t.pkg)
|
|
529
|
+
out << "# License: #{lic}, as the definition (package.xml of #{t.pkg}).\n" if lic
|
|
530
|
+
out << "# Do not edit; regenerate instead.\n"
|
|
531
|
+
deps = runtime_deps(t)
|
|
532
|
+
unless deps.empty?
|
|
533
|
+
out << "\n"
|
|
534
|
+
deps.each { |d| out << "::Asterism::ROS.require_type(#{d.inspect})\n" }
|
|
535
|
+
end
|
|
536
|
+
mod = "::Asterism::ROS::#{AsterismMsgGen.camel(t.pkg)}"
|
|
537
|
+
# Full names, no nesting: on mruby each module body is more compiled
|
|
538
|
+
# code and a deeper compile (report/r3.md). "::" because the file is
|
|
539
|
+
# evaluated inside a method there (Asterism::ROS.require_type).
|
|
540
|
+
out << "\nmodule #{mod}\nend\n\n"
|
|
541
|
+
out << if t.is_a?(Service)
|
|
542
|
+
service_body(t, mod)
|
|
543
|
+
else
|
|
544
|
+
message_body(t, "#{mod}::#{t.name}", @hasher.type_hash(full))
|
|
545
|
+
end
|
|
546
|
+
out
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def service_body(s, mod)
|
|
550
|
+
dds = "#{s.pkg}::srv::dds_::#{s.name}_"
|
|
551
|
+
path = "#{mod}::#{s.name}"
|
|
552
|
+
out = +""
|
|
553
|
+
out << "# #{s.full_name}: a service. Request and Response are its messages;\n"
|
|
554
|
+
out << "# TYPE_NAME and TYPE_HASH are the service's (what rmw_zenoh puts in keys).\n"
|
|
555
|
+
out << "module #{path}\n"
|
|
556
|
+
out << " ROS_NAME = #{s.full_name.inspect}\n"
|
|
557
|
+
out << " TYPE_NAME = #{dds.inspect}\n"
|
|
558
|
+
out << " TYPE_HASH = #{@hasher.type_hash(s.full_name).inspect}\n"
|
|
559
|
+
out << "end\n"
|
|
560
|
+
[s.request, s.response].each do |m|
|
|
561
|
+
out << "\n"
|
|
562
|
+
suffix = m.name.sub("#{s.name}_", "")
|
|
563
|
+
out << message_body(m, "#{path}::#{suffix}", @hasher.type_hash(m.full_name))
|
|
564
|
+
end
|
|
565
|
+
out
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def dds_name(m)
|
|
569
|
+
_, kind, = AsterismMsgGen.split_name(m.full_name)
|
|
570
|
+
"#{m.pkg}::#{kind}::dds_::#{m.name}_"
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def message_body(m, class_path, hash)
|
|
574
|
+
fs = m.fields
|
|
575
|
+
out = +""
|
|
576
|
+
out << "class #{class_path} < ::Asterism::ROS::Message\n"
|
|
577
|
+
out << " ROS_NAME = #{m.full_name.inspect}\n"
|
|
578
|
+
out << " TYPE_NAME = #{dds_name(m).inspect}\n"
|
|
579
|
+
out << " TYPE_HASH = #{hash.inspect}\n"
|
|
580
|
+
out << " FIELDS = [#{fs.map { |f| ":#{f.name}" }.join(', ')}]\n"
|
|
581
|
+
m.constants.each do |c|
|
|
582
|
+
out << " #{c.name} = #{lit(c.value, c.base)}\n"
|
|
583
|
+
end
|
|
584
|
+
out << "\n"
|
|
585
|
+
out << " attr_accessor #{fs.map { |f| ":#{f.name}" }.join(', ')}\n" unless fs.empty?
|
|
586
|
+
out << "\n" unless fs.empty?
|
|
587
|
+
# initialize
|
|
588
|
+
if fs.empty?
|
|
589
|
+
out << " def initialize\n end\n"
|
|
590
|
+
else
|
|
591
|
+
out << " def initialize(#{fs.map { |f| "#{f.name}: nil" }.join(', ')})\n"
|
|
592
|
+
fs.each { |f| out << " @#{f.name} = #{init_expr(f)}\n" }
|
|
593
|
+
out << " end\n"
|
|
594
|
+
end
|
|
595
|
+
out << "\n"
|
|
596
|
+
out << " def self.write(w, m)\n"
|
|
597
|
+
if fs.empty?
|
|
598
|
+
out << " w.uint8(0)\n"
|
|
599
|
+
else
|
|
600
|
+
fs.each { |f| out << " #{write_stmt(f)}\n" }
|
|
601
|
+
end
|
|
602
|
+
out << " end\n\n"
|
|
603
|
+
out << " def self.read(r)\n"
|
|
604
|
+
if fs.empty?
|
|
605
|
+
out << " r.uint8\n new\n"
|
|
606
|
+
else
|
|
607
|
+
out << " m = new\n"
|
|
608
|
+
fs.each { |f| out << " m.#{f.name} = #{read_expr(f)}\n" }
|
|
609
|
+
out << " m\n"
|
|
610
|
+
end
|
|
611
|
+
out << " end\n"
|
|
612
|
+
out << "end\n"
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def kind(f)
|
|
616
|
+
PRIMITIVES.fetch(f.base)[1]
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def byte_kind?(f)
|
|
620
|
+
BYTE_KINDS.include?(f.base)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def lit(v, base)
|
|
624
|
+
case v
|
|
625
|
+
when Array then "[" + v.map { |x| lit(x, base) }.join(", ") + "]"
|
|
626
|
+
when String then v.inspect
|
|
627
|
+
when Float
|
|
628
|
+
if v.nan? then "::Float::NAN"
|
|
629
|
+
elsif v.infinite? then v > 0 ? "::Float::INFINITY" : "-::Float::INFINITY"
|
|
630
|
+
else v.to_s
|
|
631
|
+
end
|
|
632
|
+
when Integer
|
|
633
|
+
FLOAT_KINDS.include?(base) ? v.to_f.to_s : v.to_s
|
|
634
|
+
else v.inspect
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def zero(f)
|
|
639
|
+
case f.base
|
|
640
|
+
when "bool" then "false"
|
|
641
|
+
when "string", "wstring" then '""'
|
|
642
|
+
when *FLOAT_KINDS then "0.0"
|
|
643
|
+
else "0"
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# The value a field starts with when it is not given.
|
|
648
|
+
def default_expr(f)
|
|
649
|
+
if f.nested
|
|
650
|
+
c = const_of(f.nested)
|
|
651
|
+
case f.array
|
|
652
|
+
when nil then "#{c}.new"
|
|
653
|
+
when :fixed then "::Array.new(#{f.size}) { #{c}.new }"
|
|
654
|
+
else "[]"
|
|
655
|
+
end
|
|
656
|
+
elsif f.array
|
|
657
|
+
if f.default
|
|
658
|
+
if byte_kind?(f)
|
|
659
|
+
"::Asterism::CDR.byte_string(#{lit(f.default, f.base)})"
|
|
660
|
+
else
|
|
661
|
+
lit(f.default, f.base)
|
|
662
|
+
end
|
|
663
|
+
elsif byte_kind?(f)
|
|
664
|
+
f.array == :fixed ? "\"\\x00\" * #{f.size}" : '""'
|
|
665
|
+
elsif f.array == :fixed
|
|
666
|
+
"::Array.new(#{f.size}, #{zero(f)})"
|
|
667
|
+
else
|
|
668
|
+
"[]"
|
|
669
|
+
end
|
|
670
|
+
else
|
|
671
|
+
f.default.nil? ? zero(f) : lit(f.default, f.base)
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def init_expr(f)
|
|
676
|
+
d = default_expr(f)
|
|
677
|
+
if f.nested && f.array.nil?
|
|
678
|
+
"#{const_of(f.nested)}.from(#{f.name})"
|
|
679
|
+
elsif f.nested
|
|
680
|
+
"#{f.name}.nil? ? #{d} : #{f.name}.map { |e| #{const_of(f.nested)}.from(e) }"
|
|
681
|
+
else
|
|
682
|
+
"#{f.name}.nil? ? #{d} : #{f.name}"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def bound_args(f)
|
|
687
|
+
case f.array
|
|
688
|
+
when :fixed then "#{f.size}, nil"
|
|
689
|
+
when :bounded then "nil, #{f.size}"
|
|
690
|
+
else "nil, nil"
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def write_stmt(f)
|
|
695
|
+
v = "m.#{f.name}"
|
|
696
|
+
if f.nested
|
|
697
|
+
c = const_of(f.nested)
|
|
698
|
+
return "#{c}.write(w, #{c}.from(#{v}))" if f.array.nil?
|
|
699
|
+
return "w.structs(#{c}, #{v}, #{bound_args(f)})"
|
|
700
|
+
end
|
|
701
|
+
if f.array
|
|
702
|
+
return "w.bytes(#{v}, #{bound_args(f)})" if byte_kind?(f)
|
|
703
|
+
return "w.array(:#{kind(f)}, #{v}, #{bound_args(f)}, #{f.string_max.inspect})" if f.string?
|
|
704
|
+
return "w.array(:#{kind(f)}, #{v}, #{bound_args(f)})"
|
|
705
|
+
end
|
|
706
|
+
return "w.#{kind(f)}(#{v}, #{f.string_max})" if f.string? && f.string_max
|
|
707
|
+
"w.#{kind(f)}(#{v})"
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def read_expr(f)
|
|
711
|
+
if f.nested
|
|
712
|
+
c = const_of(f.nested)
|
|
713
|
+
return "#{c}.read(r)" if f.array.nil?
|
|
714
|
+
return "r.structs(#{c}, #{f.array == :fixed ? f.size : 'nil'})"
|
|
715
|
+
end
|
|
716
|
+
fixed = f.array == :fixed ? f.size : "nil"
|
|
717
|
+
if f.array
|
|
718
|
+
return "r.bytes(#{fixed})" if byte_kind?(f)
|
|
719
|
+
return "r.array(:#{kind(f)}, #{fixed})"
|
|
720
|
+
end
|
|
721
|
+
"r.#{kind(f)}"
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# ---- driver -----------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
# Every type full name a set of requested types needs in the output
|
|
728
|
+
# (the requested ones and the messages their fields use).
|
|
729
|
+
def self.closure(registry, names)
|
|
730
|
+
out = []
|
|
731
|
+
queue = names.dup
|
|
732
|
+
until queue.empty?
|
|
733
|
+
n = queue.shift
|
|
734
|
+
next if out.include?(n)
|
|
735
|
+
out << n
|
|
736
|
+
t = registry.type(n)
|
|
737
|
+
msgs = t.is_a?(Service) ? [t.request, t.response] : [t]
|
|
738
|
+
msgs.each { |m| m.fields.each { |f| queue << f.nested if f.nested } }
|
|
739
|
+
end
|
|
740
|
+
out
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# All the hash names of a type: itself and what it references, as in the
|
|
744
|
+
# type_hashes list of the ROS 2 type description JSON.
|
|
745
|
+
def self.hash_names(hasher, full)
|
|
746
|
+
d = hasher.full_description(full)
|
|
747
|
+
[full] + d["referenced_type_descriptions"].map { |r| r["type_name"] }
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Compares the hashes with <dir>/<pkg>/<msg|srv>/<Name>.json. Returns
|
|
751
|
+
# [checked, mismatches, missing].
|
|
752
|
+
def self.check_json(hasher, names, dirs)
|
|
753
|
+
checked = 0
|
|
754
|
+
bad = []
|
|
755
|
+
missing = []
|
|
756
|
+
names.each do |full|
|
|
757
|
+
pkg, kind, name = split_name(full)
|
|
758
|
+
path = dirs.map { |d| File.join(d, pkg, kind, "#{name}.json") }.find { |p| File.file?(p) }
|
|
759
|
+
if path.nil?
|
|
760
|
+
missing << full
|
|
761
|
+
next
|
|
762
|
+
end
|
|
763
|
+
JSON.parse(File.read(path))["type_hashes"].each do |th|
|
|
764
|
+
mine = hasher.type_hash(th["type_name"])
|
|
765
|
+
checked += 1
|
|
766
|
+
bad << [th["type_name"], mine, th["hash_string"]] unless mine == th["hash_string"]
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
[checked, bad, missing]
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def self.main(argv)
|
|
773
|
+
include_dirs = []
|
|
774
|
+
out_dir = nil
|
|
775
|
+
json_dirs = []
|
|
776
|
+
mode = :generate
|
|
777
|
+
deps = true
|
|
778
|
+
op = OptionParser.new do |o|
|
|
779
|
+
o.banner = "usage: asterism_msggen.rb [-I DIR]... (-o OUT | --hash | --check-json DIR) TYPE_OR_FILE..."
|
|
780
|
+
o.on("-I DIR", "where <pkg>/msg/*.msg and <pkg>/srv/*.srv are (repeatable)") { |d| include_dirs << d }
|
|
781
|
+
o.on("-o OUT", "write <OUT>/<pkg>/<msg|srv>/<Name>.rb") { |d| out_dir = d }
|
|
782
|
+
o.on("--no-deps", "write only the named types, not the types they use") { deps = false }
|
|
783
|
+
o.on("--hash", "print the RIHS01 hashes instead of writing files") { mode = :hash }
|
|
784
|
+
o.on("--check-json DIR", "compare the hashes with the ROS 2 type description JSON under DIR") do |d|
|
|
785
|
+
mode = :check
|
|
786
|
+
json_dirs << d
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
args = op.parse(argv)
|
|
790
|
+
abort op.banner if args.empty?
|
|
791
|
+
reg = Registry.new(include_dirs)
|
|
792
|
+
names = args.map do |a|
|
|
793
|
+
if a.end_with?(".msg") || a.end_with?(".srv")
|
|
794
|
+
reg.include_dirs << File.dirname(File.dirname(File.dirname(File.expand_path(a))))
|
|
795
|
+
reg.name_of_file(a)
|
|
796
|
+
else
|
|
797
|
+
a
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
hasher = Hasher.new(reg)
|
|
801
|
+
case mode
|
|
802
|
+
when :hash
|
|
803
|
+
names.each { |n| hash_names(hasher, n).each { |h| puts "#{h} #{hasher.type_hash(h)}" } }
|
|
804
|
+
when :check
|
|
805
|
+
all = closure(reg, names)
|
|
806
|
+
checked, bad, missing = check_json(hasher, all, json_dirs)
|
|
807
|
+
bad.each { |n, mine, theirs| warn "MISMATCH #{n}: computed #{mine}, json #{theirs}" }
|
|
808
|
+
missing.each { |n| warn "no JSON for #{n}" }
|
|
809
|
+
puts "type hashes: #{checked} checked, #{bad.size} mismatched, #{missing.size} without JSON"
|
|
810
|
+
exit(bad.empty? && missing.empty? ? 0 : 1)
|
|
811
|
+
else
|
|
812
|
+
abort "-o OUT is needed" unless out_dir
|
|
813
|
+
em = Emitter.new(reg, hasher)
|
|
814
|
+
(deps ? closure(reg, names) : names).each do |n|
|
|
815
|
+
path = File.join(out_dir, Emitter.rel_path(n))
|
|
816
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
817
|
+
File.write(path, em.file(n))
|
|
818
|
+
puts path
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
rescue Error => e
|
|
822
|
+
abort "asterism_msggen: #{e.message}"
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
AsterismMsgGen.main(ARGV) if $PROGRAM_NAME == __FILE__
|