diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 082eb0de..cf3329b4 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -17,6 +17,23 @@ jobs: with: configDirectory: . + # Messages already out keep their fields' numbers and types: a renumbered, + # retyped or removed field fails against master (buf.yaml). Additions pass. + buf-breaking: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Install buf + uses: bufbuild/buf-action@v1 + with: + setup_only: true + - name: Check for breaking changes against master + run: | + git fetch --no-tags --depth=1 origin master + git worktree add --detach "$RUNNER_TEMP/master" FETCH_HEAD + buf breaking --against "$RUNNER_TEMP/master/protobuf_definitions" + build-linux: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci-typescript.yaml b/.github/workflows/ci-typescript.yaml index 7aadefe9..33009f74 100644 --- a/.github/workflows/ci-typescript.yaml +++ b/.github/workflows/ci-typescript.yaml @@ -4,6 +4,9 @@ on: push: branches: - master + # Prereleases for the SLAM pull requests (p2-django builds against + # them); remove before merging. + - sonar3d-slam pull_request: permissions: @@ -75,12 +78,24 @@ jobs: cp README.npm.md README.md VERSION=$(pnpm pkg get version | tr -d '"') SHORT_SHA=${GITHUB_SHA::8} - pnpm version --no-git-tag-version "${VERSION}-${SHORT_SHA}" - pnpm publish --no-git-checks --access public --tag latest - echo "PROTOCOL_VERSION=${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" + # master is latest; another branch publishes a prerelease named and + # tagged after it, which leaves latest alone. + if [ "$GITHUB_REF_NAME" = master ]; then + RELEASE="${VERSION}-${SHORT_SHA}" + TAG=latest + else + TAG="${GITHUB_REF_NAME//\//-}" + # "g" before the hash: an all-digit one with a leading zero is not a + # valid SemVer prerelease identifier. + RELEASE="${VERSION}-${TAG}.g${SHORT_SHA}" + fi + pnpm version --no-git-tag-version "$RELEASE" + pnpm publish --no-git-checks --access public --tag "$TAG" + echo "PROTOCOL_VERSION=${RELEASE}" >> "$GITHUB_ENV" - name: Trigger protocol bump in blueye-ts - if: github.event_name == 'push' + # Releases only: a branch's prerelease is not for blueye-ts. + if: github.event_name == 'push' && github.ref_name == 'master' env: GH_TOKEN: ${{ secrets.BLUEYE_PROTOCOL_BUMP_PAT }} run: | diff --git a/CLAUDE.md b/CLAUDE.md index ca10cfa4..e52cdad9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,10 +72,11 @@ All proto definitions are in `protobuf_definitions/`: - **req_rep.proto** — Request/response message patterns - **mission_planning.proto** — Mission planning messages - **aquatroll.proto** — AquaTroll sensor-specific messages +- **slam.proto** — SLAM state, poses, map and sensors (its `Slam*Tel` messages are in telemetry.proto and its requests in req_rep.proto) ## CI/CD Pipeline -- **ci-build.yaml** — Protolint validation (all pushes and PRs) +- **ci-build.yaml** — Protolint validation, and `buf breaking` against master (`buf.yaml`) so a renumbered, retyped or removed field fails (all pushes and PRs) - **ci-typescript.yaml** — Generate TS, compile, smoke-test the packed tarball (PRs and master); publish to npm and trigger the blueye-ts bump (master only) - **ci-dotnet.yaml** — Build and publish NuGet package (all pushes) - **ci-python.yaml** — Triggers `blueye.protocol` repo update via repository dispatch (master, when proto files change) diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 00000000..922dc821 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,8 @@ +# buf config for the breaking-change check in CI (ci-build.yaml): a field that is +# renumbered, retyped or removed against master fails the build. Additions pass. +version: v2 +modules: + - path: protobuf_definitions +breaking: + use: + - FILE diff --git a/protobuf_definitions/message_formats.proto b/protobuf_definitions/message_formats.proto index e30431be..231c1af7 100644 --- a/protobuf_definitions/message_formats.proto +++ b/protobuf_definitions/message_formats.proto @@ -738,7 +738,7 @@ message DvlVelocity { int32 status = 2; // Vendor-specific status of the DVL. float delta_time = 3; // Time since last velocity measurement (ms). float fom = 4; // Figure of merit, a measure of the accuracy of the velocities (m/s). - Vector3 velocity = 5; // Velocity, x forward, y left, z down (m/s). + Vector3 velocity = 5; // Velocity, x forward, y right, z down (m/s). bool is_water_tracking = 6; // Water tracking status. repeated DvlTransducer transducers = 7; // List of transducers. } @@ -2083,3 +2083,27 @@ message Annotation { google.protobuf.Duration duration = 7; // Optional span; omit for a point-in-time marker. AnnotationSource source = 8; // Origin of the annotation; the app sets APP, the drone sets DRONE for its own. } + +// Unit quaternion describing a rotation (Hamilton convention: w + xi + yj + zk). +// +// Where it is a body's orientation, it rotates vectors from the body's frame to +// the frame the body is placed in. +message Quaternion { + double x = 1; // X-component of the vector part. + double y = 2; // Y-component of the vector part. + double z = 3; // Z-component of the vector part. + double w = 4; // Scalar part. +} + +// Points packed for use as typed arrays without decoding them one by one. +// +// The frame the positions are in is the one the enclosing message names. +// +// count says how many points the bytes hold: positions is exactly count * 12 bytes, +// and intensities either empty or count bytes. A cloud whose bytes disagree with +// count is malformed, and is not used. +message PointCloud { + uint32 count = 1; // Number of points. + bytes positions = 2; // count x, y, z triplets of little-endian float32 (m): 12 bytes per point. + bytes intensities = 3; // count signal strengths, one byte each (0 to 255), or empty when unknown. +} diff --git a/protobuf_definitions/req_rep.proto b/protobuf_definitions/req_rep.proto index 8e563151..69e6722a 100644 --- a/protobuf_definitions/req_rep.proto +++ b/protobuf_definitions/req_rep.proto @@ -7,6 +7,7 @@ package blueye.protocol; import "google/protobuf/any.proto"; import "message_formats.proto"; import "mission_planning.proto"; +import "slam.proto"; option csharp_namespace = "Blueye.Protocol.Protobuf"; option java_multiple_files = true; @@ -186,6 +187,26 @@ message SetLogFrequencyRep { bool success = 1; // True if message name valid and frequency successfully updated. } +// Request to reset the publishing frequency of a message to the drone's default. +message ResetPubFrequencyReq { + string message_type = 1; // Message name, f. ex. "AttitudeTel"; empty resets every message. +} + +// Response after resetting the publishing frequency. +message ResetPubFrequencyRep { + bool success = 1; // True if the message name was valid and its frequency reset. +} + +// Request to reset the logging frequency of a message to the drone's default. +message ResetLogFrequencyReq { + string message_type = 1; // Message name, f. ex. "AttitudeTel"; empty resets every message. +} + +// Response after resetting the logging frequency. +message ResetLogFrequencyRep { + bool success = 1; // True if the message name was valid and its frequency reset. +} + // Request to get latest telemetry data message GetTelemetryReq { string message_type = 1; // Message name, f. ex. "AttitudeTel". @@ -294,3 +315,107 @@ message GetMultibeamColorFilterReq { message GetMultibeamColorFilterRep { MultibeamColorFilter color_filter = 1; // The current color filter. Empty rgb means none is set (grayscale). } + +// Request to catch up on the SLAM map. +// +// Nothing comes in the reply but what is coming: the SLAM publishes it on its own +// publisher, to the clients subscribed there. The SLAM keeps its latest map +// updates: when map_id is the current map and the updates after have_sequence are +// still kept, it publishes those again (SlamMapUpdateTel, as they were, so a client +// that has them already skips them by their sequence); otherwise it publishes the +// whole map (SlamMapTel, in parts when it is large). +message GetSlamMapReq { + uint32 map_id = 1; // The map the client holds; 0 when it holds none. + uint64 have_sequence = 2; // The sequence of the last update the client applied to that map. +} + +// Response after asking to catch up on the SLAM map: what is published for it. +message GetSlamMapRep { + bool available = 1; // True if the SLAM is running and answered. + uint32 map_id = 2; // The current map (0 when the SLAM has no map). + // The client has the map at this sequence once it has what is published. The whole map may be one + // made earlier (its parts say their sequence): the updates after it are published again after it. + uint64 sequence = 3; + oneof catch_up { + // The updates from from_sequence to sequence are published again (none when from_sequence is past + // sequence: the client has the map already). + uint64 from_sequence = 4; + uint32 part_count = 5; // The whole map is published, in this many SlamMapTel parts. + } +} + +// Request to discard the SLAM map and start a new one from the current pose. +message ResetSlamReq { + // The map to discard: the SLAM resets only if it is still the current map, so that two clients + // asking at once start one new map, not two. 0 (map ids never are): only if the SLAM has no map yet. + uint32 map_id = 1; +} + +// Response after resetting the SLAM map. +message ResetSlamRep { + bool available = 1; // True if the SLAM is running and answered. + // Identifier of the map now (the new one, or the current one when the request's map_id was not + // the current map's); 0 when the SLAM has no map yet (it starts one with its next scan). + uint32 map_id = 2; + bool reset = 3; // True if the map was discarded. + SlamRequestError error = 4; // Why the map was not discarded, when it was not. +} + +// Request to start or stop the SLAM. +// +// Starting switches the sensor on (for a sonar, its acoustics) and begins a new +// map from the current pose. Stopping switches the sensor off and keeps the map, +// so it can still be viewed. Asking for the state the SLAM is in already changes +// nothing (ResetSlamReq begins a new map). The SLAM reports its state in +// SlamStatusTel. +message SetSlamActiveReq { + bool active = 1; // True to start the SLAM, false to stop it. +} + +// Response after starting or stopping the SLAM. +message SetSlamActiveRep { + bool available = 1; // True if the SLAM is running and answered. + bool active = 2; // True if the SLAM is now mapping (false if its sensor did not switch on). + SlamRequestError error = 3; // Why the SLAM is not in the state asked for, when it is not. +} + +// Request to start or stop recording the SLAM. +// +// A recording needs the SLAM to be mapping, and stopping the SLAM stops it. It is +// stored with the drone's other media (picture, video and multibeam recordings). +// Like a multibeam recording it holds the drone's DroneInfoTel, at its start and +// when it changes, which names the dive log it belongs with (log_name). +// Asking for the state the recording is in already changes nothing. +message SetSlamRecordingReq { + bool record = 1; // True to start recording, false to stop. + bool sensor_data = 2; // Also the sensor's raw data, and the dive log faster, for running the SLAM offline. + StorageLocation storage_location = 3; // Where to store the recording. +} + +// Response after starting or stopping a SLAM recording. +message SetSlamRecordingRep { + bool available = 1; // True if the SLAM is running and answered. + SlamRecordingState recording = 2; // The recording after the request. + SlamRequestError error = 3; // Why the recording is not in the state asked for, when it is not. +} + +// Request to change the settings of a SLAM sensor. +// +// The settings go to the sensor named by sensor_id. Without one, they go to the +// SLAM's sensor of their kind when it has exactly one; with several, the request +// must name one (SLAM_REQUEST_ERROR_NO_SUCH_SENSOR otherwise). Settings left +// unspecified stay as they are. The map goes on at the resolution it was made +// with; the next new map (a start, or ResetSlamReq) takes the resolution the +// sensor's range then gives. +message SetSlamSensorSettingsReq { + SlamSensorSettings settings = 1; // The settings to change. + string sensor_id = 2; // The sensor to change (SlamSensor.id); empty: the SLAM's only sensor of the settings' kind. +} + +// Response after changing the settings of a SLAM sensor. +message SetSlamSensorSettingsRep { + bool available = 1; // True if the SLAM is running and answered. + bool applied = 2; // True if the sensor took the settings. + SlamSensorSettings settings = 3; // The sensor's settings after the request. + SlamRequestError error = 4; // Why the settings were not applied, when they were not. +} diff --git a/protobuf_definitions/slam.proto b/protobuf_definitions/slam.proto new file mode 100644 index 00000000..de2670d2 --- /dev/null +++ b/protobuf_definitions/slam.proto @@ -0,0 +1,343 @@ +// SLAM +// +// The SLAM's (simultaneous localization and mapping) state, poses, map and +// sensors: what its telemetry (Slam*Tel in telemetry.proto) and its requests +// (in req_rep.proto) carry. +// +// The SLAM has its own publisher, not the drone's telemetry socket: a client +// subscribes to it on the drone at TCP port 5561 (WebSocket 9989) to get the +// Slam*Tel messages. Its requests (GetSlamMapReq, ResetSlamReq, +// SetSlamActiveReq, SetSlamSensorSettingsReq, SetSlamRecordingReq) go to the +// drone's request server as other requests do, which forwards them to the +// SLAM's (TCP 5562, WebSocket 9990). +syntax = "proto3"; + +package blueye.protocol; +import "google/protobuf/timestamp.proto"; +import "message_formats.proto"; +option csharp_namespace = "Blueye.Protocol.Protobuf"; +option java_multiple_files = true; + +// State of the SLAM (simultaneous localization and mapping). +enum SlamState { + SLAM_STATE_UNSPECIFIED = 0; // Unspecified. + SLAM_STATE_WAITING = 1; // Waiting for the first scan and orientation. + SLAM_STATE_TRACKING = 2; // The latest scan was matched against the map. + SLAM_STATE_COASTING = 3; // The latest scan could not be matched; the pose follows the motion model. + // Coasting long enough that the prediction may be far off: a scan is placed again only by a match + // that fixes the pose firmly. + SLAM_STATE_LOST = 4; +} + +// Kind of sensor a SLAM scan comes from. +enum SlamSensorType { + SLAM_SENSOR_TYPE_UNSPECIFIED = 0; // Unspecified. + SLAM_SENSOR_TYPE_SONAR_3D = 1; // A 3D imaging sonar, e.g. the Water Linked Sonar 3D-15. + SLAM_SENSOR_TYPE_STEREO_CAMERA = 2; // A stereo camera's depth. +} + +// What the SLAM's poses are the poses of: its platform. +enum SlamPlatform { + SLAM_PLATFORM_UNSPECIFIED = 0; // Not known. + // The drone, its body frame x forward, y right, z down from the middle of the hull. Each + // sensor is on it through its mount (SlamPose mount in the messages that carry one). + SLAM_PLATFORM_DRONE = 1; + // The sensor itself: the SLAM did not know how the sensor is mounted. + SLAM_PLATFORM_SENSOR = 2; +} + +// A sensor whose scans feed the SLAM, and its field of view. +message SlamSensor { + // Sensor id, unique among the SLAM's sensors. A sensor keeps the id it was given for as long as it + // is connected, whatever other sensors connect or disconnect: the model's (e.g. + // "waterlinked-sonar-3d15") when no other sensor of that model is connected, and else the model's with + // the guest port the sensor is on added (e.g. "waterlinked-sonar-3d15-gp2"). + string id = 1; + SlamSensorType sensor_type = 2; // Kind of sensor. + float fov_horizontal = 3; // Horizontal field of view, about the sensor's z axis (°). + float fov_vertical = 4; // Vertical field of view, about the sensor's y axis (°). + float max_range = 5; // Range the sensor is set to (m). + // The sensor as a guest port device (e.g. GUEST_PORT_DEVICE_ID_WATERLINKED_SONAR_3D15), for a client + // to show the device itself; GUEST_PORT_DEVICE_ID_UNSPECIFIED when it is not one (a simulated sensor). + GuestPortDeviceID device_id = 6; + bool simulated = 7; // True if the sensor is simulated (its scans are made up, for testing). +} + +// A recording of the SLAM, as the SLAM reports it. +// +// A recording is a .sbez file (a binlog) with the SLAM's output: the map at its +// start, then the poses, the map updates and the status. It may hold several maps +// (a reset, or a new mode, starts one), and each map it holds ends with the whole +// map (SlamMapTel, in parts when it is large). With sensor data it also has the +// sensor's raw data beside it (for a 3D sonar a .sonar file), and the drone's dive +// log logs its telemetry at higher rates while it runs, so the SLAM can be run +// again on both without the drone. +message SlamRecordingState { + bool is_recording = 1; // True while recording. + string name = 2; // File name of the .sbez, without its folder. + int32 seconds = 3; // Time recorded (s). + uint64 size = 4; // Size of the recording's files so far (bytes). + bool sensor_data = 5; // True if the sensor's raw data is recorded too. + StorageLocation storage_location = 6; // Where the recording is stored. +} + +// SONAR_3D reads better than protolint's SONAR3D for Sonar3d. +// protolint:disable ENUM_FIELD_NAMES_PREFIX + +// Frequency mode of a 3D imaging sonar. +enum Sonar3dFrequencyMode { + SONAR_3D_FREQUENCY_MODE_UNSPECIFIED = 0; // Unspecified: left as it is. + SONAR_3D_FREQUENCY_MODE_LOW = 1; // Wide view, long range (3D-15: 90° x 40°, 15 m, 5 scans/s). + SONAR_3D_FREQUENCY_MODE_HIGH = 2; // Narrow view, short range, fine detail (3D-15: 40° x 40°, 4 m, 20 scans/s). +} + +// protolint:enable ENUM_FIELD_NAMES_PREFIX + +// Settings of a 3D imaging sonar that feeds the SLAM. +// +// In a request, unspecified or 0 leaves a setting as it is; in a report, it +// means the sonar does not say (e.g. a firmware without frequency modes). +message SlamSonar3dSettings { + Sonar3dFrequencyMode frequency_mode = 1; // Frequency mode. + float max_range = 2; // Maximum range (m). +} + +// Settings of a sensor that feeds the SLAM. +// +// One message for each kind of sensor, so a client shows the settings of the +// sensors the SLAM has, and a sensor takes only its own. +message SlamSensorSettings { + oneof settings { + SlamSonar3dSettings sonar_3d = 1; // A 3D imaging sonar's settings. + } +} + +// A sensor that feeds the SLAM, as the SLAM reports it. +message SlamSensorStatus { + // The sensor, as its latest scan described it (before its first scan, its id and type only). + SlamSensor sensor = 1; + SlamSensorSettings settings = 2; // The sensor's settings, unset when it has none or they cannot be read. + // The sensor's pose on the platform now: its mount, at the mount's tilt now. Unset when the + // platform is the sensor. + SlamPose mount = 3; + optional float mount_tilt = 4; // Tilt of the sensor's mount (°, positive up), for a mount that tilts. +} + +// A pose: of a body in the SLAM map frame, or of a sensor on the platform. +// +// The map frame is fixed where the map was started (see SlamMapFrame): z down, x +// along the heading the map started with (north when a compass heading was used), +// y to the right of it, in metres. The body is the one the field names (the +// platform, or a sensor), its frame x forward, y right, z down. A mount is a +// sensor's pose in the platform's frame instead: the sensor is in the map at the +// platform's pose times its mount. +message SlamPose { + Vector3 position = 1; // Position of the body (m). + Quaternion orientation = 2; // Rotation from the body's frame to the frame it is placed in. +} + +// The SLAM estimate of its platform's pose, published for every scan. +message SlamPoseEstimate { + uint32 map_id = 1; // Identifier of the map; a new one is chosen whenever the map restarts. + SlamState state = 2; // State of the SLAM. + SlamPose pose = 3; // Pose of the platform (SlamMapFrame.platform) at the scan. + google.protobuf.Timestamp time = 4; // Host time the scan was received. + uint32 scan_points = 5; // Points (or cells) of the scan used for matching. + float match_inlier_ratio = 6; // Share of those matched to the map (0..1). + // Root mean square residual of the matched points against the map (m); for a 3D sonar, their distance to the + // map's planes. + float match_rms = 7; + uint32 keyframe_count = 8; // Keyframes in the map. + uint32 map_point_count = 9; // Points (or cells) in the map. + // Depth used for the scan (m), when there was one: the sensor's, from the drone's depth sensor + // through the mount (without a mount, the depth sensor's own). + optional float depth = 10; + string sensor_id = 11; // Sensor whose scan was placed (SlamSensor.id). + // The sensor's pose on the platform at the scan: the sensor was at pose times mount. + // Unset when the platform is the sensor. + SlamPose mount = 12; + // Covariance of pose as the scan's match placed it against the map, given the + // prediction: 6 x 6, row by row, of a rotation about the map's x, y and z axes + // (rad) and then a position along them (m). It says how well this scan fixed the + // pose, not how far the map has drifted. Empty when the match gave none: the scan was not matched, + // the map's first scan (nothing to match it against), or a fit whose covariance was not finite. + repeated float covariance_elements = 13; + // The newest keyframe when the scan was placed (SlamKeyframe.id), 0 before the map's first. A client + // keeping poses can move one with that keyframe when a loop closure moves it: the pose becomes the + // keyframe's new pose times its pose then, inverted, times the pose. + uint32 keyframe_id = 14; +} + +// A keyframe: a scan kept in the map, the nodes of the SLAM trajectory. +message SlamKeyframe { + uint32 id = 1; // Keyframe id, increasing along the trajectory. + uint32 submap_id = 2; // Submap whose points the keyframe added to. + // Pose of the platform at the keyframe. It moves when the trajectory is corrected. + SlamPose pose = 3; + google.protobuf.Timestamp time = 4; // Host time the keyframe's scan was received. + string sensor_id = 5; // Sensor whose scan the keyframe is (SlamSensor.id). + // The sensor's pose on the platform at the keyframe. Unset when the platform is the sensor. + SlamPose mount = 6; +} + +// A submap: part of the map from one sensor, in the platform's frame at its anchor +// keyframe. +// +// A submap moves rigidly with its anchor keyframe, so a correction of the +// trajectory moves its content without sending it again. A submap holds one kind +// of content: points today; other kinds (e.g. a 2D sonar's mosaic) will be added +// to the oneof, each saying how a later one replaces what the submap holds. +message SlamSubmap { + uint32 submap_id = 1; // Submap id. + uint32 anchor_keyframe_id = 2; // Keyframe whose platform frame the content is in. + string sensor_id = 3; // Sensor the content comes from (SlamSensor.id). + oneof content { + // Points in the platform's frame at the anchor keyframe. In a map update they are appended to the + // submap's points, unless replace is set. + PointCloud points = 4; + } + bool replace = 5; // Points: true if they replace the submap's points so far, rather than adding to them. +} + +// Poses of consecutive keyframes, packed: what a correction of the trajectory +// moved, sent without the rest of each keyframe. +message SlamKeyframePoses { + uint32 first_keyframe_id = 1; // Keyframe id of the first pose; the others follow it in id order. + // Platform poses as in SlamKeyframe.pose, 7 little-endian float32 each: x, y, z (m), then the + // orientation's x, y, z, w. 28 bytes per keyframe. + bytes poses = 2; +} + +// A loop closure: a keyframe's scan matched to the map where the trajectory had +// been before, which corrected the trajectory. The keyframes the correction moved +// come in the same update, so a client that held the map before has the +// trajectory as it was. +message SlamLoopClosure { + uint32 keyframe_id = 1; // Keyframe whose scan closed the loop. + uint32 matched_keyframe_id = 2; // Earlier keyframe it was matched near. + // How far the correction moved the newest keyframe it reached (m): the loop's keyframe, or one + // added while the loop was being closed. + float position_correction = 3; + // How far it turned that keyframe about the map's z axis (°, positive clockwise seen from + // above in a gravity-aligned map). + float heading_correction = 4; +} + +// A change to the SLAM map. +// +// Updates are numbered: a client that has the map at sequence N applies the +// update with sequence N + 1 next. When it misses one, or the map id changes, it +// asks to catch up (GetSlamMapReq): it gets the updates it missed again, or the +// whole map. +message SlamMapUpdate { + uint32 map_id = 1; // Identifier of the map. + uint64 sequence = 2; // Sequence number of this update, from 1. + repeated SlamKeyframe keyframes = 3; // Keyframes added. + repeated SlamSubmap submaps = 4; // Content added to submaps, or replacing a submap's (SlamSubmap.replace). + // The map's frame, on the update that adds the first keyframe and on any that + // learns more of it: a recording cut short has it without its last SlamMapTel. + SlamMapFrame frame = 5; + repeated SlamLoopClosure loop_closures = 6; // Loop closures whose correction moved the keyframes. + repeated SlamKeyframePoses moved_poses = 7; // New poses of the keyframes a correction moved, in runs. + repeated uint32 removed_submap_ids = 8; // Submaps taken out of the map, with whatever they hold. +} + +// Why the SLAM did not do what a request asked (the reply says what it did do). +enum SlamRequestError { + SLAM_REQUEST_ERROR_UNSPECIFIED = 0; // None: the request was done, or asked for what already was. + SLAM_REQUEST_ERROR_NOT_READABLE = 1; // The request could not be read. + SLAM_REQUEST_ERROR_OTHER_MAP = 2; // The request named a map that is not the current one. + // The request named a sensor the SLAM does not have, or named none where it has several that fit. + SLAM_REQUEST_ERROR_NO_SUCH_SENSOR = 3; + SLAM_REQUEST_ERROR_SENSOR = 4; // The sensor did not take it (it did not answer, or refused). + SLAM_REQUEST_ERROR_BUSY = 5; // Another change to the sensor is under way; ask again after it. + SLAM_REQUEST_ERROR_NOT_MAPPING = 6; // It needs the SLAM to be mapping (a recording). + SLAM_REQUEST_ERROR_STORAGE = 7; // No storage to record to, or it is full. + SLAM_REQUEST_ERROR_NOT_SUPPORTED = 8; // The SLAM or its sensor cannot do it. +} + +// Where the SLAM's orientation comes from. +enum SlamOrientationSource { + SLAM_ORIENTATION_SOURCE_UNSPECIFIED = 0; // None: the scans are matched without an orientation. + SLAM_ORIENTATION_SOURCE_SENSOR_IMU = 1; // The sensor's own IMU. + SLAM_ORIENTATION_SOURCE_DRONE_ATTITUDE = 2; // The drone's attitude, its heading from the compass. + // The drone's attitude for roll and pitch, its turn from the drone's calibrated gyro (not the compass). + SLAM_ORIENTATION_SOURCE_DRONE_GYRO = 3; +} + +// What other than its sensors the SLAM's estimate rests on now. +message SlamAiding { + SlamOrientationSource orientation = 1; // Where the orientation comes from. + bool depth = 2; // True if a depth is used (from the drone's depth sensor). + bool velocity = 3; // True if a velocity over the ground is used (a DVL with bottom lock). +} + +// What the x axis of a SLAM map's frame points along. +enum SlamHeadingReference { + SLAM_HEADING_REFERENCE_UNSPECIFIED = 0; // Not known. + // The platform's heading at the map's first scan; without gravity alignment, the + // platform's frame at the first scan as it is. + SLAM_HEADING_REFERENCE_FIRST_SCAN = 1; + // North, as the drone's compass heading (AttitudeTel yaw) gave it at the map's first scan. + SLAM_HEADING_REFERENCE_DRONE_COMPASS = 2; +} + +// How a SLAM map's frame lies in the world, for placing the map on a chart. +// +// The frame is right-handed: x along the heading reference, y to its right, z +// down (north-east-down with SLAM_HEADING_REFERENCE_DRONE_COMPASS). The origin is +// the platform's position at the map's first scan. The map's heading may drift +// from its reference over a dive: placing a map on a chart fits it to the drone's +// positions (PositionEstimateTel in the dive log). +message SlamMapFrame { + bool gravity_aligned = 1; // True if z is down and x and y level; false: the first scan's platform frame. + SlamHeadingReference heading_reference = 2; // What x points along. + // The magnetic declination the drone applied to its compass heading at the + // map's first scan (°, as MagneticDeclination.declination; 0 when it applied + // none), with SLAM_HEADING_REFERENCE_DRONE_COMPASS: x then points the place's + // declination less this east of true north (0 when the drone applied the + // place's). Unset when not known. + optional float magnetic_declination = 3; + // Depth of the origin below the surface (m), for a gravity-aligned map: a point + // at z in the map is at depth origin_depth + z. Unset without a depth. + optional float origin_depth = 4; + google.protobuf.Timestamp start_time = 5; // Host time of the map's first scan (its first keyframe's). + SlamPlatform platform = 6; // What the map's poses are the poses of. +} + +// The whole SLAM map, or a part of it. +// +// A large map comes in parts (part_count of them, in order), each a message of +// its own. Every part has the map's map_id, sequence and part_count, and its own +// part: a client groups parts by them, and drops a set whose parts disagree (one +// of another map or sequence, or missing). Every other field that is not repeated +// is set in the first part (part 0). A client has the map once it has every part, +// and puts them together field by field: every repeated field is the parts' +// concatenated in part order, and every other field is the first part's, whatever +// a later part has (proto3 does not tell a field set to its default from an unset +// one, so a client could not tell which part sets it). +// Which part carries which repeated values is up to the SLAM, but a submap is +// never split: each comes whole, in one part. +message SlamMap { + // Identifier of the map (0 when the SLAM has no map); a new one each time the map starts, + // unique while the SLAM runs. + uint32 map_id = 1; + uint64 sequence = 2; // Sequence number of the last update the map includes (0 for none). + repeated SlamKeyframe keyframes = 3; // Every keyframe, in id order. + repeated SlamSubmap submaps = 4; // Submaps, with all they hold. + SlamMapFrame frame = 5; // How the map's frame lies in the world (unset before the first scan). + repeated SlamLoopClosure loop_closures = 6; // Every loop closure, in the order they were made. + uint32 part = 7; // Index of this part, from 0. + uint32 part_count = 8; // Parts the map comes in (0 or 1: this message is the whole map). +} + +// The latest scan of a sensor, as the SLAM placed it. +message SlamScan { + uint32 map_id = 1; // Identifier of the map. + SlamPose pose = 2; // Pose of the platform at the scan. + PointCloud points = 3; // Points of the scan in the sensor frame: in the map at pose times mount. + SlamSensor sensor = 4; // Sensor the scan comes from. + // The sensor's pose on the platform at the scan. Unset when the platform is the sensor. + SlamPose mount = 5; + google.protobuf.Timestamp time = 6; // Host time the scan was received (as SlamPoseEstimate.time). +} diff --git a/protobuf_definitions/telemetry.proto b/protobuf_definitions/telemetry.proto index 73fee511..9f2cd6f6 100644 --- a/protobuf_definitions/telemetry.proto +++ b/protobuf_definitions/telemetry.proto @@ -7,6 +7,7 @@ package blueye.protocol; import "aquatroll.proto"; import "message_formats.proto"; import "mission_planning.proto"; +import "slam.proto"; option csharp_namespace = "Blueye.Protocol.Protobuf"; option java_multiple_files = true; @@ -416,3 +417,58 @@ message SotStateTel { message AnnotationTel { Annotation annotation = 1; // The annotation that was added. } + +// SLAM estimate of its platform's pose (SlamMapFrame.platform: the drone's, each +// sensor on it at its mount, which the estimate carries; or, without a known +// mount, the sensor's own). +// +// Published for every scan on the SLAM publisher, not the main telemetry socket. +message SlamPoseTel { + SlamPoseEstimate estimate = 1; // The pose estimate. +} + +// A change to the SLAM map: new keyframes and points, or a corrected trajectory. +// +// Published on the SLAM publisher. A client applies the updates that follow the +// map it holds (SlamMapTel); one it missed it asks for again (GetSlamMapReq). +message SlamMapUpdateTel { + SlamMapUpdate update = 1; // The change. +} + +// The latest scan placed in the SLAM map. +// +// Published on the SLAM publisher for live display; not part of the map. +message SlamScanTel { + SlamScan scan = 1; // The scan. +} + +// The whole SLAM map, or a part of it (SlamMap.part). +// +// Published on the SLAM publisher when a client asks for it (GetSlamMapReq). A +// client subscribes, asks, keeps the updates that arrive meanwhile, and applies +// those that follow the map's sequence once it has all of its parts (put together +// as SlamMap says: repeated fields concatenated, every other field the first +// part's). A SLAM recording has the map at its start, and ends each map it holds +// with it. +message SlamMapTel { + SlamMap map = 1; // The map, or a part of it. +} + +// The SLAM's state. +// +// Published on the SLAM publisher once a second, and when it changes. +message SlamStatusTel { + bool active = 1; // True while the SLAM is mapping: its sensors are on and scans are placed. + uint32 map_id = 2; // Identifier of the current map, 0 when the SLAM has no map. + SlamRecordingState recording = 3; // The recording, when one runs or ran. + repeated SlamSensorStatus sensors = 4; // The sensors that feed the SLAM. + bool map_full = 5; // True if the map holds as many points as it may: it is placed in, but grows no more. + bool loop_closure = 6; // True if the SLAM corrects its trajectory with loop closures (it has a back end). + SlamMapFrame frame = 7; // How the current map's frame lies in the world (unset before its first scan). + // Sequence of the newest update of the current map (SlamMapUpdate.sequence), 0 for none: a client + // that has applied less of the current map has missed one, and catches up (GetSlamMapReq). + uint64 sequence = 8; + SlamAiding aiding = 9; // What other than its sensors the estimate rests on now. + // True if the SLAM's back end failed and was dropped: the current map has no more loop closures. + bool backend_failed = 10; +}