Repository: foxglove/schemas (language: python)

Issue:
Add `frame_id` to image types
Image and CompressedImage need `frame_id` if we are going to display them in the 3D panel. CameraCalibration may need it as well if we want to use `frame_id` to confirm that the calibration topic corresponds to the image topic.


Discussion / hints:
CameraCalibration is an independently visualizable topic in the 3D panel, so it needs frame_id as well.

Fix patch:
diff --git a/ros_foxglove_msgs/ros1/CameraCalibration.msg b/ros_foxglove_msgs/ros1/CameraCalibration.msg
index eed8d50d..fad3d781 100644
--- a/ros_foxglove_msgs/ros1/CameraCalibration.msg
+++ b/ros_foxglove_msgs/ros1/CameraCalibration.msg
@@ -6,6 +6,9 @@
 # Timestamp of calibration data
 time timestamp
 
+# Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/ros_foxglove_msgs/ros1/CompressedImage.msg b/ros_foxglove_msgs/ros1/CompressedImage.msg
index f19cc78e..f049b777 100644
--- a/ros_foxglove_msgs/ros1/CompressedImage.msg
+++ b/ros_foxglove_msgs/ros1/CompressedImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Compressed image data
 uint8[] data
 
diff --git a/ros_foxglove_msgs/ros1/RawImage.msg b/ros_foxglove_msgs/ros1/RawImage.msg
index 7a85e17b..64b86aeb 100644
--- a/ros_foxglove_msgs/ros1/RawImage.msg
+++ b/ros_foxglove_msgs/ros1/RawImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/ros_foxglove_msgs/ros2/CameraCalibration.msg b/ros_foxglove_msgs/ros2/CameraCalibration.msg
index 38894b54..8dabdf13 100644
--- a/ros_foxglove_msgs/ros2/CameraCalibration.msg
+++ b/ros_foxglove_msgs/ros2/CameraCalibration.msg
@@ -6,6 +6,9 @@
 # Timestamp of calibration data
 builtin_interfaces/Time timestamp
 
+# Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/ros_foxglove_msgs/ros2/CompressedImage.msg b/ros_foxglove_msgs/ros2/CompressedImage.msg
index 8c0a6f18..afba021d 100644
--- a/ros_foxglove_msgs/ros2/CompressedImage.msg
+++ b/ros_foxglove_msgs/ros2/CompressedImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 builtin_interfaces/Time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Compressed image data
 uint8[] data
 
diff --git a/ros_foxglove_msgs/ros2/RawImage.msg b/ros_foxglove_msgs/ros2/RawImage.msg
index dd0343a2..f6310c11 100644
--- a/ros_foxglove_msgs/ros2/RawImage.msg
+++ b/ros_foxglove_msgs/ros2/RawImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 builtin_interfaces/Time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/schemas/README.md b/schemas/README.md
index 13ad6d9e..a15be44b 100644
--- a/schemas/README.md
+++ b/schemas/README.md
@@ -244,6 +244,19 @@ time
 
 Timestamp of calibration data
 
+</td>
+</tr>
+<tr>
+<td><code>frame_id</code></td>
+<td>
+
+string
+
+</td>
+<td>
+
+Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+
 </td>
 </tr>
 <tr>
@@ -552,6 +565,19 @@ time
 
 Timestamp of image
 
+</td>
+</tr>
+<tr>
+<td><code>frame_id</code></td>
+<td>
+
+string
+
+</td>
+<td>
+
+Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+
 </td>
 </tr>
 <tr>
@@ -2066,6 +2092,19 @@ time
 
 Timestamp of image
 
+</td>
+</tr>
+<tr>
+<td><code>frame_id</code></td>
+<td>
+
+string
+
+</td>
+<td>
+
+Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+
 </td>
 </tr>
 <tr>
diff --git a/schemas/jsonschema/CameraCalibration.json b/schemas/jsonschema/CameraCalibration.json
index 2f52f771..ebef038d 100644
--- a/schemas/jsonschema/CameraCalibration.json
+++ b/schemas/jsonschema/CameraCalibration.json
@@ -20,6 +20,10 @@
       },
       "description": "Timestamp of calibration data"
     },
+    "frame_id": {
+      "type": "string",
+      "description": "Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image."
+    },
     "width": {
       "type": "integer",
       "minimum": 0,
diff --git a/schemas/jsonschema/CompressedImage.json b/schemas/jsonschema/CompressedImage.json
index 09c6a694..cffb874d 100644
--- a/schemas/jsonschema/CompressedImage.json
+++ b/schemas/jsonschema/CompressedImage.json
@@ -20,6 +20,10 @@
       },
       "description": "Timestamp of image"
     },
+    "frame_id": {
+      "type": "string",
+      "description": "Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image."
+    },
     "data": {
       "type": "string",
       "contentEncoding": "base64",
diff --git a/schemas/jsonschema/RawImage.json b/schemas/jsonschema/RawImage.json
index f2eab54f..03ea61a9 100644
--- a/schemas/jsonschema/RawImage.json
+++ b/schemas/jsonschema/RawImage.json
@@ -20,6 +20,10 @@
       },
       "description": "Timestamp of image"
     },
+    "frame_id": {
+      "type": "string",
+      "description": "Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image."
+    },
     "width": {
       "type": "integer",
       "minimum": 0,
diff --git a/schemas/proto/foxglove/CameraCalibration.proto b/schemas/proto/foxglove/CameraCalibration.proto
index bbfdfc8b..d257f177 100644
--- a/schemas/proto/foxglove/CameraCalibration.proto
+++ b/schemas/proto/foxglove/CameraCalibration.proto
@@ -11,6 +11,9 @@ message CameraCalibration {
   // Timestamp of calibration data
   google.protobuf.Timestamp timestamp = 1;
 
+  // Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+  string frame_id = 9;
+
   // Image width
   fixed32 width = 2;
 
diff --git a/schemas/proto/foxglove/CompressedImage.proto b/schemas/proto/foxglove/CompressedImage.proto
index 10e533be..a30296e1 100644
--- a/schemas/proto/foxglove/CompressedImage.proto
+++ b/schemas/proto/foxglove/CompressedImage.proto
@@ -11,6 +11,9 @@ message CompressedImage {
   // Timestamp of image
   google.protobuf.Timestamp timestamp = 1;
 
+  // Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+  string frame_id = 4;
+
   // Compressed image data
   bytes data = 2;
 
diff --git a/schemas/proto/foxglove/RawImage.proto b/schemas/proto/foxglove/RawImage.proto
index 275d4424..40f4b89b 100644
--- a/schemas/proto/foxglove/RawImage.proto
+++ b/schemas/proto/foxglove/RawImage.proto
@@ -11,6 +11,9 @@ message RawImage {
   // Timestamp of image
   google.protobuf.Timestamp timestamp = 1;
 
+  // Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+  string frame_id = 7;
+
   // Image width
   fixed32 width = 2;
 
diff --git a/schemas/ros1/CameraCalibration.msg b/schemas/ros1/CameraCalibration.msg
index eed8d50d..fad3d781 100644
--- a/schemas/ros1/CameraCalibration.msg
+++ b/schemas/ros1/CameraCalibration.msg
@@ -6,6 +6,9 @@
 # Timestamp of calibration data
 time timestamp
 
+# Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/schemas/ros1/CompressedImage.msg b/schemas/ros1/CompressedImage.msg
index f19cc78e..f049b777 100644
--- a/schemas/ros1/CompressedImage.msg
+++ b/schemas/ros1/CompressedImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Compressed image data
 uint8[] data
 
diff --git a/schemas/ros1/RawImage.msg b/schemas/ros1/RawImage.msg
index 7a85e17b..64b86aeb 100644
--- a/schemas/ros1/RawImage.msg
+++ b/schemas/ros1/RawImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/schemas/ros2/CameraCalibration.msg b/schemas/ros2/CameraCalibration.msg
index 38894b54..8dabdf13 100644
--- a/schemas/ros2/CameraCalibration.msg
+++ b/schemas/ros2/CameraCalibration.msg
@@ -6,6 +6,9 @@
 # Timestamp of calibration data
 builtin_interfaces/Time timestamp
 
+# Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/schemas/ros2/CompressedImage.msg b/schemas/ros2/CompressedImage.msg
index 8c0a6f18..afba021d 100644
--- a/schemas/ros2/CompressedImage.msg
+++ b/schemas/ros2/CompressedImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 builtin_interfaces/Time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Compressed image data
 uint8[] data
 
diff --git a/schemas/ros2/RawImage.msg b/schemas/ros2/RawImage.msg
index dd0343a2..f6310c11 100644
--- a/schemas/ros2/RawImage.msg
+++ b/schemas/ros2/RawImage.msg
@@ -6,6 +6,9 @@
 # Timestamp of image
 builtin_interfaces/Time timestamp
 
+# Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.
+string frame_id
+
 # Image width
 uint32 width
 
diff --git a/schemas/typescript/CameraCalibration.ts b/schemas/typescript/CameraCalibration.ts
index 519c11c7..d70509db 100644
--- a/schemas/typescript/CameraCalibration.ts
+++ b/schemas/typescript/CameraCalibration.ts
@@ -7,6 +7,9 @@ export type CameraCalibration = {
   /** Timestamp of calibration data */
   timestamp: Time;
 
+  /** Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image. */
+  frame_id: string;
+
   /** Image width */
   width: number;
 
diff --git a/schemas/typescript/CompressedImage.ts b/schemas/typescript/CompressedImage.ts
index f23d91b4..cf3084bd 100644
--- a/schemas/typescript/CompressedImage.ts
+++ b/schemas/typescript/CompressedImage.ts
@@ -7,6 +7,9 @@ export type CompressedImage = {
   /** Timestamp of image */
   timestamp: Time;
 
+  /** Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image. */
+  frame_id: string;
+
   /** Compressed image data */
   data: Uint8Array;
 
diff --git a/schemas/typescript/RawImage.ts b/schemas/typescript/RawImage.ts
index e50350a7..690fcce8 100644
--- a/schemas/typescript/RawImage.ts
+++ b/schemas/typescript/RawImage.ts
@@ -7,6 +7,9 @@ export type RawImage = {
   /** Timestamp of image */
   timestamp: Time;
 
+  /** Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image. */
+  frame_id: string;
+
   /** Image width */
   width: number;
 
diff --git a/src/generateProto.ts b/src/generateProto.ts
index cf098101..5cf66800 100644
--- a/src/generateProto.ts
+++ b/src/generateProto.ts
@@ -1,4 +1,9 @@
-import { FoxgloveEnumSchema, FoxgloveMessageSchema, FoxglovePrimitive } from "./types";
+import {
+  FoxgloveEnumSchema,
+  FoxgloveMessageField,
+  FoxgloveMessageSchema,
+  FoxglovePrimitive,
+} from "./types";
 
 function primitiveToProto(type: Exclude<FoxglovePrimitive, "time" | "duration">) {
   switch (type) {
@@ -35,9 +40,33 @@ export function generateProto(
     );
   }
 
-  let fieldNumber = 1;
+  const explicitFieldNumbers = new Set<number>();
+  for (const field of schema.fields) {
+    if (field.protobufFieldNumber != undefined) {
+      if (explicitFieldNumbers.has(field.protobufFieldNumber)) {
+        throw new Error(
+          `More than one field with protobufFieldNumber ${field.protobufFieldNumber}`,
+        );
+      }
+      explicitFieldNumbers.add(field.protobufFieldNumber);
+    }
+  }
+
+  let nextFieldNumber = 1;
+  const numberedFields = schema.fields.map(
+    (field): FoxgloveMessageField & { protobufFieldNumber: number } => {
+      if (field.protobufFieldNumber != undefined) {
+        return { ...field, protobufFieldNumber: field.protobufFieldNumber };
+      }
+      while (explicitFieldNumbers.has(nextFieldNumber)) {
+        ++nextFieldNumber;
+      }
+      return { ...field, protobufFieldNumber: nextFieldNumber++ };
+    },
+  );
+
   const imports = new Set<string>();
-  const fields = schema.fields.map((field) => {
+  const fields = numberedFields.map((field) => {
     const lineComments: string[] = [];
     const qualifiers: string[] = [];
     if (field.array != undefined) {
@@ -70,7 +99,7 @@ export function generateProto(
       .trim()
       .split("\n")
       .map((line) => `  // ${line}\n`)
-      .join("")}  ${qualifiers.join(" ")} ${field.name} = ${fieldNumber++};${
+      .join("")}  ${qualifiers.join(" ")} ${field.name} = ${field.protobufFieldNumber};${
       lineComments.length > 0 ? " // " + lineComments.join(", ") : ""
     }`;
   });
diff --git a/src/schemas.ts b/src/schemas.ts
index 0863c2dd..ea9cdbc6 100644
--- a/src/schemas.ts
+++ b/src/schemas.ts
@@ -664,6 +664,13 @@ const CameraCalibration: FoxgloveMessageSchema = {
       type: { type: "primitive", name: "time" },
       description: "Timestamp of calibration data",
     },
+    {
+      name: "frame_id",
+      type: { type: "primitive", name: "string" },
+      description:
+        "Frame of reference for the camera. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.",
+      protobufFieldNumber: 9,
+    },
     {
       name: "width",
       type: { type: "primitive", name: "uint32" },
@@ -754,6 +761,13 @@ const CompressedImage: FoxgloveMessageSchema = {
       type: { type: "primitive", name: "time" },
       description: "Timestamp of image",
     },
+    {
+      name: "frame_id",
+      type: { type: "primitive", name: "string" },
+      description:
+        "Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.",
+      protobufFieldNumber: 4,
+    },
     {
       name: "data",
       type: { type: "primitive", name: "bytes" },
@@ -777,6 +791,13 @@ const RawImage: FoxgloveMessageSchema = {
       type: { type: "primitive", name: "time" },
       description: "Timestamp of image",
     },
+    {
+      name: "frame_id",
+      type: { type: "primitive", name: "string" },
+      description:
+        "Frame of reference for the image. The origin of the frame is the optical center of the camera. +x points to the right in the image, +y points down, and +z points into the plane of the image.",
+      protobufFieldNumber: 7,
+    },
     {
       name: "width",
       type: { type: "primitive", name: "uint32" },
diff --git a/src/types.ts b/src/types.ts
index aa35d4de..5f9973cd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -20,21 +20,24 @@ export type FoxgloveEnumSchema = {
   }>;
 };
 
+export type FoxgloveMessageField = {
+  name: string;
+  type:
+    | { type: "primitive"; name: FoxglovePrimitive }
+    | { type: "nested"; schema: FoxgloveMessageSchema }
+    | { type: "enum"; enum: FoxgloveEnumSchema };
+  array?: true | number;
+  required?: true;
+  description: string;
+  protobufFieldNumber?: number;
+};
+
 export type FoxgloveMessageSchema = {
   type: "message";
   name: string;
   description: string;
   rosEquivalent?: keyof typeof import("@foxglove/rosmsg-msgs-common").definitions;
-  fields: ReadonlyArray<{
-    name: string;
-    type:
-      | { type: "primitive"; name: FoxglovePrimitive }
-      | { type: "nested"; schema: FoxgloveMessageSchema }
-      | { type: "enum"; enum: FoxgloveEnumSchema };
-    array?: true | number;
-    required?: true;
-    description: string;
-  }>;
+  fields: ReadonlyArray<FoxgloveMessageField>;
 };
 
 export type FoxgloveSchema = FoxgloveMessageSchema | FoxgloveEnumSchema;


Test patch:
diff --git a/src/generateProto.test.ts b/src/generateProto.test.ts
index 58fbc4e0..99b7b78b 100644
--- a/src/generateProto.test.ts
+++ b/src/generateProto.test.ts
@@ -37,67 +37,67 @@ describe("generateProto", () => {
         bool field_boolean = 3;
 
         // bytes field
-        bytes field_bytes = 4;
+        bytes field_bytes = 5;
 
         // float64 field
-        double field_float64 = 5;
+        double field_float64 = 6;
 
         // uint32 field
-        fixed32 field_uint32 = 6;
+        fixed32 field_uint32 = 7;
 
         // string field
-        string field_string = 7;
+        string field_string = 8;
 
         // duration array field
-        repeated google.protobuf.Duration field_duration_array = 8;
+        repeated google.protobuf.Duration field_duration_array = 9;
 
         // time array field
-        repeated google.protobuf.Timestamp field_time_array = 9;
+        repeated google.protobuf.Timestamp field_time_array = 10;
 
         // boolean array field
-        repeated bool field_boolean_array = 10;
+        repeated bool field_boolean_array = 11;
 
         // bytes array field
-        repeated bytes field_bytes_array = 11;
+        repeated bytes field_bytes_array = 12;
 
         // float64 array field
-        repeated double field_float64_array = 12;
+        repeated double field_float64_array = 13;
 
         // uint32 array field
-        repeated fixed32 field_uint32_array = 13;
+        repeated fixed32 field_uint32_array = 14;
 
         // string array field
-        repeated string field_string_array = 14;
+        repeated string field_string_array = 15;
 
         // duration fixed-length array field
-        repeated google.protobuf.Duration field_duration_fixed_array = 15; // length 3
+        repeated google.protobuf.Duration field_duration_fixed_array = 16; // length 3
 
         // time fixed-length array field
-        repeated google.protobuf.Timestamp field_time_fixed_array = 16; // length 3
+        repeated google.protobuf.Timestamp field_time_fixed_array = 17; // length 3
 
         // boolean fixed-length array field
-        repeated bool field_boolean_fixed_array = 17; // length 3
+        repeated bool field_boolean_fixed_array = 18; // length 3
 
         // bytes fixed-length array field
-        repeated bytes field_bytes_fixed_array = 18; // length 3
+        repeated bytes field_bytes_fixed_array = 19; // length 3
 
         // float64 fixed-length array field
-        repeated double field_float64_fixed_array = 19; // length 3
+        repeated double field_float64_fixed_array = 20; // length 3
 
         // uint32 fixed-length array field
-        repeated fixed32 field_uint32_fixed_array = 20; // length 3
+        repeated fixed32 field_uint32_fixed_array = 21; // length 3
 
         // string fixed-length array field
-        repeated string field_string_fixed_array = 21; // length 3
+        repeated string field_string_fixed_array = 22; // length 3
 
         // An enum field
-        ExampleProtoEnum field_enum = 22;
+        ExampleProtoEnum field_enum = 23;
 
         // An enum array field
-        repeated ExampleProtoEnum field_enum_array = 23;
+        repeated ExampleProtoEnum field_enum_array = 24;
 
         // A nested field
-        foxglove.NestedMessage field_nested = 24;
+        foxglove.NestedMessage field_nested = 25;
 
         // A nested array field
         // With
@@ -105,7 +105,7 @@ describe("generateProto", () => {
         // very
         // long
         // description
-        repeated foxglove.NestedMessage field_nested_array = 25;
+        repeated foxglove.NestedMessage field_nested_array = 4;
       }
       "
     `);
diff --git a/src/testFixtures.ts b/src/testFixtures.ts
index fc969983..62de11d9 100644
--- a/src/testFixtures.ts
+++ b/src/testFixtures.ts
@@ -78,6 +78,7 @@ export const exampleMessage: FoxgloveMessageSchema = {
       description: "A nested array field\nWith\na\nvery\nlong\ndescription",
       type: { type: "nested", schema: exampleNestedMessage },
       array: true,
+      protobufFieldNumber: 4,
     },
   ],
 };