Create AOS MCAP logger for testing Foxglove
Doesn't add indexing yet, so still limited in size.
https://github.com/foxglove/studio/issues/2909 tracks support for
flatbuffers in foxglove studio
References: PRO-13587
Change-Id: I7c8c15c765395ade979eb8a011cfdae65451b526
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/BUILD b/aos/BUILD
index 025e78c..aaf0ec0 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -265,6 +265,7 @@
srcs = ["flatbuffer_utils.cc"],
hdrs = ["flatbuffer_utils.h"],
target_compatible_with = ["@platforms//os:linux"],
+ visibility = ["//visibility:public"],
deps = [
"@com_github_google_flatbuffers//:flatbuffers",
"@com_github_google_glog//:glog",
@@ -514,6 +515,7 @@
"fast_string_builder.h",
],
target_compatible_with = ["@platforms//os:linux"],
+ visibility = ["//visibility:public"],
deps = [
"@com_github_google_glog//:glog",
"@com_google_absl//absl/strings",
diff --git a/aos/util/BUILD b/aos/util/BUILD
index 8df16e2..3b96cfd 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -34,6 +34,42 @@
],
)
+cc_binary(
+ name = "log_to_mcap",
+ srcs = ["log_to_mcap.cc"],
+ deps = [
+ ":mcap_logger",
+ "//aos:init",
+ "//aos/events/logging:log_reader",
+ "//frc971/control_loops:control_loops_fbs",
+ ],
+)
+
+cc_library(
+ name = "mcap_logger",
+ srcs = ["mcap_logger.cc"],
+ hdrs = ["mcap_logger.h"],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = [
+ "//aos:configuration_fbs",
+ "//aos:fast_string_builder",
+ "//aos:flatbuffer_utils",
+ "//aos/events:event_loop",
+ "@com_github_nlohmann_json//:json",
+ ],
+)
+
+cc_test(
+ name = "mcap_logger_test",
+ srcs = ["mcap_logger_test.cc"],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = [
+ ":mcap_logger",
+ "//aos/testing:googletest",
+ "@com_github_nlohmann_json//:json",
+ ],
+)
+
cc_library(
name = "math",
hdrs = ["math.h"],
diff --git a/aos/util/log_to_mcap.cc b/aos/util/log_to_mcap.cc
new file mode 100644
index 0000000..17555a4
--- /dev/null
+++ b/aos/util/log_to_mcap.cc
@@ -0,0 +1,34 @@
+#include "aos/events/event_loop_generated.h"
+#include "aos/events/logging/log_reader.h"
+#include "aos/init.h"
+#include "aos/util/mcap_logger.h"
+
+DEFINE_string(node, "", "Node to replay from the perspective of.");
+DEFINE_string(output_path, "/tmp/log.mcap", "Log to output.");
+
+// Converts an AOS log to an MCAP log that can be fed into Foxglove. To try this
+// out, run:
+// bazel run -c opt //aos/util:log_to_mcap -- --node NODE_NAME /path/to/logfile
+//
+// Then navigate to http://studio.foxglove.dev (or spin up your own instance
+// locally), and use it to open the file (this doesn't upload the file to
+// foxglove's servers or anything).
+int main(int argc, char *argv[]) {
+ aos::InitGoogle(&argc, &argv);
+
+ const std::vector<aos::logger::LogFile> logfiles =
+ aos::logger::SortParts(aos::logger::FindLogs(argc, argv));
+
+ aos::logger::LogReader reader(logfiles);
+
+ reader.Register();
+
+ const aos::Node *node =
+ aos::configuration::GetNode(reader.configuration(), FLAGS_node);
+ CHECK_NOTNULL(node);
+ std::unique_ptr<aos::EventLoop> mcap_event_loop =
+ reader.event_loop_factory()->MakeEventLoop("mcap", node);
+ CHECK(!FLAGS_output_path.empty());
+ aos::McapLogger relogger(mcap_event_loop.get(), FLAGS_output_path);
+ reader.event_loop_factory()->Run();
+}
diff --git a/aos/util/mcap_logger.cc b/aos/util/mcap_logger.cc
new file mode 100644
index 0000000..9ab4012
--- /dev/null
+++ b/aos/util/mcap_logger.cc
@@ -0,0 +1,232 @@
+#include "aos/util/mcap_logger.h"
+
+#include "absl/strings/str_replace.h"
+#include "single_include/nlohmann/json.hpp"
+
+namespace aos {
+nlohmann::json JsonSchemaForFlatbuffer(const FlatbufferType &type,
+ JsonSchemaRecursion recursion_level) {
+ nlohmann::json schema;
+ if (recursion_level == JsonSchemaRecursion::kTopLevel) {
+ schema["$schema"] = "https://json-schema.org/draft/2020-12/schema";
+ }
+ schema["type"] = "object";
+ nlohmann::json properties;
+ for (int index = 0; index < type.NumberFields(); ++index) {
+ nlohmann::json field;
+ const bool is_array = type.FieldIsRepeating(index);
+ if (type.FieldIsSequence(index)) {
+ // For sub-tables/structs, just recurse.
+ nlohmann::json subtype = JsonSchemaForFlatbuffer(
+ type.FieldType(index), JsonSchemaRecursion::kNested);
+ if (is_array) {
+ field["type"] = "array";
+ field["items"] = subtype;
+ } else {
+ field = subtype;
+ }
+ } else {
+ std::string elementary_type;
+ switch (type.FieldElementaryType(index)) {
+ case flatbuffers::ET_UTYPE:
+ case flatbuffers::ET_CHAR:
+ case flatbuffers::ET_UCHAR:
+ case flatbuffers::ET_SHORT:
+ case flatbuffers::ET_USHORT:
+ case flatbuffers::ET_INT:
+ case flatbuffers::ET_UINT:
+ case flatbuffers::ET_LONG:
+ case flatbuffers::ET_ULONG:
+ case flatbuffers::ET_FLOAT:
+ case flatbuffers::ET_DOUBLE:
+ elementary_type = "number";
+ break;
+ case flatbuffers::ET_BOOL:
+ elementary_type = "boolean";
+ break;
+ case flatbuffers::ET_STRING:
+ elementary_type = "string";
+ break;
+ case flatbuffers::ET_SEQUENCE:
+ if (type.FieldIsEnum(index)) {
+ elementary_type = "string";
+ } else {
+ LOG(FATAL) << "Should not encounter any sequence fields here.";
+ }
+ break;
+ }
+ if (is_array) {
+ field["type"] = "array";
+ field["items"]["type"] = elementary_type;
+ } else {
+ field["type"] = elementary_type;
+ }
+ }
+ // the nlohmann::json [] operator needs an actual string, not just a
+ // string_view :(.
+ properties[std::string(type.FieldName(index))] = field;
+ }
+ schema["properties"] = properties;
+ return schema;
+}
+
+McapLogger::McapLogger(EventLoop *event_loop, const std::string &output_path)
+ : output_(output_path) {
+ event_loop->SkipTimingReport();
+ event_loop->SkipAosLog();
+ CHECK(output_);
+ WriteMagic();
+ WriteHeader();
+ uint16_t id = 1;
+ for (const Channel *channel : *event_loop->configuration()->channels()) {
+ if (!configuration::ChannelIsReadableOnNode(channel, event_loop->node())) {
+ continue;
+ }
+
+ WriteSchema(id, channel);
+
+ // Write out the channel entry that uses the schema (we just re-use the
+ // chema ID for the channel ID, since we aren't deduplicating schemas for
+ // channels that are of the same type).
+ WriteChannel(id, id, channel);
+
+ event_loop->MakeRawWatcher(
+ channel, [this, id, channel](const Context &context, const void *) {
+ WriteMessage(id, channel, context);
+ });
+ ++id;
+ }
+}
+
+McapLogger::~McapLogger() {
+ WriteDataEnd();
+ WriteFooter();
+ WriteMagic();
+}
+
+void McapLogger::WriteMagic() { output_ << "\x89MCAP0\r\n"; }
+
+void McapLogger::WriteHeader() {
+ string_builder_.Reset();
+ // "profile"
+ AppendString(&string_builder_, "x-aos");
+ // "library"
+ AppendString(&string_builder_, "AOS MCAP converter");
+ WriteRecord(OpCode::kHeader, string_builder_.Result());
+}
+
+void McapLogger::WriteFooter() {
+ string_builder_.Reset();
+ // Offsets and CRC32 for summary section, which we don't populate.
+ AppendInt64(&string_builder_, 0);
+ AppendInt64(&string_builder_, 0);
+ AppendInt32(&string_builder_, 0);
+ WriteRecord(OpCode::kFooter, string_builder_.Result());
+}
+
+void McapLogger::WriteDataEnd() {
+ string_builder_.Reset();
+ // CRC32 for the data, which we are too lazy to calculate.
+ AppendInt32(&string_builder_, 0);
+ WriteRecord(OpCode::kDataEnd, string_builder_.Result());
+}
+
+void McapLogger::WriteSchema(const uint16_t id, const aos::Channel *channel) {
+ CHECK(channel->has_schema());
+ std::string schema = JsonSchemaForFlatbuffer({channel->schema()}).dump();
+
+ // Write out the schema (we don't bother deduplicating schema types):
+ string_builder_.Reset();
+ // Schema ID
+ AppendInt16(&string_builder_, id);
+ // Type name
+ AppendString(&string_builder_, channel->type()->string_view());
+ // Encoding
+ AppendString(&string_builder_, "jsonschema");
+ // Actual schema itself
+ AppendString(&string_builder_, schema);
+ WriteRecord(OpCode::kSchema, string_builder_.Result());
+}
+
+void McapLogger::WriteChannel(const uint16_t id, const uint16_t schema_id,
+ const aos::Channel *channel) {
+ string_builder_.Reset();
+ // Channel ID
+ AppendInt16(&string_builder_, id);
+ // Schema ID
+ AppendInt16(&string_builder_, schema_id);
+ // Topic name
+ AppendString(&string_builder_,
+ absl::StrCat(channel->name()->string_view(), " ",
+ channel->type()->string_view()));
+ // Encoding
+ AppendString(&string_builder_, "json");
+ // Metadata (technically supposed to be a Map<string, string>)
+ AppendString(&string_builder_, "");
+ WriteRecord(OpCode::kChannel, string_builder_.Result());
+}
+
+void McapLogger::WriteMessage(uint16_t channel_id, const Channel *channel,
+ const Context &context) {
+ CHECK_NOTNULL(context.data);
+
+ string_builder_.Reset();
+ // Channel ID
+ AppendInt16(&string_builder_, channel_id);
+ // Queue Index
+ AppendInt32(&string_builder_, context.queue_index);
+ // Log time, and publish time. Since we don't log a logged time, just use
+ // published time.
+ // TODO(james): If we use this for multi-node logfiles, use distributed clock.
+ AppendInt64(&string_builder_,
+ context.monotonic_event_time.time_since_epoch().count());
+ AppendInt64(&string_builder_,
+ context.monotonic_event_time.time_since_epoch().count());
+
+ CHECK(flatbuffers::Verify(*channel->schema(),
+ *channel->schema()->root_table(),
+ static_cast<const uint8_t *>(context.data),
+ static_cast<size_t>(context.size)))
+ << ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
+ << channel->type()->c_str();
+
+ aos::FlatbufferToJson(&string_builder_, channel->schema(),
+ static_cast<const uint8_t *>(context.data));
+
+ WriteRecord(OpCode::kMessage, string_builder_.Result());
+}
+
+void McapLogger::WriteRecord(OpCode op, std::string_view record) {
+ output_.put(static_cast<char>(op));
+ uint64_t record_length = record.size();
+ output_.write(reinterpret_cast<const char *>(&record_length),
+ sizeof(record_length));
+ output_ << record;
+}
+
+void McapLogger::AppendString(FastStringBuilder *builder,
+ std::string_view string) {
+ AppendInt32(builder, string.size());
+ builder->Append(string);
+}
+
+namespace {
+template <typename T>
+static void AppendInt(FastStringBuilder *builder, T val) {
+ builder->Append(
+ std::string_view(reinterpret_cast<const char *>(&val), sizeof(T)));
+}
+} // namespace
+
+void McapLogger::AppendInt16(FastStringBuilder *builder, uint16_t val) {
+ AppendInt(builder, val);
+}
+
+void McapLogger::AppendInt32(FastStringBuilder *builder, uint32_t val) {
+ AppendInt(builder, val);
+}
+
+void McapLogger::AppendInt64(FastStringBuilder *builder, uint64_t val) {
+ AppendInt(builder, val);
+}
+} // namespace aos
diff --git a/aos/util/mcap_logger.h b/aos/util/mcap_logger.h
new file mode 100644
index 0000000..15d11db
--- /dev/null
+++ b/aos/util/mcap_logger.h
@@ -0,0 +1,67 @@
+#ifndef AOS_UTIL_MCAP_LOGGER_H_
+#define AOS_UTIL_MCAP_LOGGER_H_
+
+#include "aos/configuration_generated.h"
+#include "aos/events/event_loop.h"
+#include "aos/fast_string_builder.h"
+#include "aos/flatbuffer_utils.h"
+#include "single_include/nlohmann/json.hpp"
+
+namespace aos {
+
+// Produces a JSON Schema (https://json-schema.org/) for a given flatbuffer
+// type. If recursion_level is set, will include a $schema attribute indicating
+// the schema definition being used (this is used to allow for recursion).
+//
+// Note that this is pretty bare-bones, so, e.g., we don't distinguish between
+// structs and tables when generating the JSON schema, so we don't bother to
+// mark struct fields as required.
+enum class JsonSchemaRecursion {
+ kTopLevel,
+ kNested,
+};
+nlohmann::json JsonSchemaForFlatbuffer(
+ const FlatbufferType &type,
+ JsonSchemaRecursion recursion_level = JsonSchemaRecursion::kTopLevel);
+
+// Generates an MCAP file, per the specification at
+// https://github.com/foxglove/mcap/tree/main/docs/specification
+class McapLogger {
+ public:
+ McapLogger(EventLoop *event_loop, const std::string &output_path);
+ ~McapLogger();
+
+ private:
+ enum class OpCode {
+ kHeader = 0x01,
+ kFooter = 0x02,
+ kSchema = 0x03,
+ kChannel = 0x04,
+ kMessage = 0x05,
+ kDataEnd = 0x0F,
+ };
+ // Helpers to write each type of relevant record.
+ void WriteMagic();
+ void WriteHeader();
+ void WriteFooter();
+ void WriteDataEnd();
+ void WriteSchema(const uint16_t id, const aos::Channel *channel);
+ void WriteChannel(const uint16_t id, const uint16_t schema_id,
+ const aos::Channel *channel);
+ void WriteMessage(uint16_t channel_id, const Channel *channel,
+ const Context &context);
+ void WriteConfig();
+
+ // Writes an MCAP record to the output file.
+ void WriteRecord(OpCode op, std::string_view record);
+ // Adds an MCAP-spec string/fixed-size integer to a buffer.
+ static void AppendString(FastStringBuilder *builder, std::string_view string);
+ static void AppendInt16(FastStringBuilder *builder, uint16_t val);
+ static void AppendInt32(FastStringBuilder *builder, uint32_t val);
+ static void AppendInt64(FastStringBuilder *builder, uint64_t val);
+
+ std::ofstream output_;
+ FastStringBuilder string_builder_;
+};
+} // namespace aos
+#endif // AOS_UTIL_MCAP_LOGGER_H_
diff --git a/aos/util/mcap_logger_test.cc b/aos/util/mcap_logger_test.cc
new file mode 100644
index 0000000..6e82a53
--- /dev/null
+++ b/aos/util/mcap_logger_test.cc
@@ -0,0 +1,829 @@
+#include "aos/util/mcap_logger.h"
+
+#include <iostream>
+#include "flatbuffers/reflection_generated.h"
+#include "gtest/gtest.h"
+
+namespace aos::testing {
+// TODO(james): Write a proper test for the McapLogger itself. However, that
+// will require writing an MCAP reader (or importing an existing one).
+
+// Confirm that the schema for the reflection.Schema table itself hasn't
+// changed. reflection.Schema should be a very stable type, so this should need
+// updating except when we change the JSON schema generation itself.
+TEST(JsonSchemaTest, ReflectionSchema) {
+ std::string schema_json =
+ JsonSchemaForFlatbuffer({reflection::Schema::MiniReflectTypeTable()})
+ .dump(4);
+ EXPECT_EQ(R"json({
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "properties": {
+ "advanced_features": {
+ "type": "number"
+ },
+ "enums": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "is_union": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "underlying_type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ },
+ "values": {
+ "items": {
+ "properties": {
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ },
+ "object": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bytesize": {
+ "type": "number"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "fields": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "default_integer": {
+ "type": "number"
+ },
+ "default_real": {
+ "type": "number"
+ },
+ "deprecated": {
+ "type": "boolean"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "id": {
+ "type": "number"
+ },
+ "key": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "number"
+ },
+ "optional": {
+ "type": "boolean"
+ },
+ "padding": {
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "is_struct": {
+ "type": "boolean"
+ },
+ "minalign": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "union_type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ },
+ "value": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "fbs_files": {
+ "items": {
+ "properties": {
+ "filename": {
+ "type": "string"
+ },
+ "included_filenames": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "file_ext": {
+ "type": "string"
+ },
+ "file_ident": {
+ "type": "string"
+ },
+ "objects": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bytesize": {
+ "type": "number"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "fields": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "default_integer": {
+ "type": "number"
+ },
+ "default_real": {
+ "type": "number"
+ },
+ "deprecated": {
+ "type": "boolean"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "id": {
+ "type": "number"
+ },
+ "key": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "number"
+ },
+ "optional": {
+ "type": "boolean"
+ },
+ "padding": {
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "is_struct": {
+ "type": "boolean"
+ },
+ "minalign": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "root_table": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bytesize": {
+ "type": "number"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "fields": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "default_integer": {
+ "type": "number"
+ },
+ "default_real": {
+ "type": "number"
+ },
+ "deprecated": {
+ "type": "boolean"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "id": {
+ "type": "number"
+ },
+ "key": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "number"
+ },
+ "optional": {
+ "type": "boolean"
+ },
+ "padding": {
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "is_struct": {
+ "type": "boolean"
+ },
+ "minalign": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "services": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "calls": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ },
+ "request": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bytesize": {
+ "type": "number"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "fields": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "default_integer": {
+ "type": "number"
+ },
+ "default_real": {
+ "type": "number"
+ },
+ "deprecated": {
+ "type": "boolean"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "id": {
+ "type": "number"
+ },
+ "key": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "number"
+ },
+ "optional": {
+ "type": "boolean"
+ },
+ "padding": {
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "is_struct": {
+ "type": "boolean"
+ },
+ "minalign": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "response": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bytesize": {
+ "type": "number"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "fields": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "default_integer": {
+ "type": "number"
+ },
+ "default_real": {
+ "type": "number"
+ },
+ "deprecated": {
+ "type": "boolean"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "id": {
+ "type": "number"
+ },
+ "key": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "number"
+ },
+ "optional": {
+ "type": "boolean"
+ },
+ "padding": {
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "type": {
+ "properties": {
+ "base_size": {
+ "type": "number"
+ },
+ "base_type": {
+ "type": "number"
+ },
+ "element": {
+ "type": "number"
+ },
+ "element_size": {
+ "type": "number"
+ },
+ "fixed_length": {
+ "type": "number"
+ },
+ "index": {
+ "type": "number"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "is_struct": {
+ "type": "boolean"
+ },
+ "minalign": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "declaration_file": {
+ "type": "string"
+ },
+ "documentation": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+})json",
+ schema_json);
+}
+
+} // namespace aos::testing