diff --git a/protobuf_definitions/control.proto b/protobuf_definitions/control.proto index 065ff262..0a4a4866 100644 --- a/protobuf_definitions/control.proto +++ b/protobuf_definitions/control.proto @@ -6,6 +6,7 @@ syntax = "proto3"; package blueye.protocol; import "aquatroll.proto"; import "message_formats.proto"; +import "mission_planning.proto"; option csharp_namespace = "Blueye.Protocol.Protobuf"; option java_multiple_files = true; @@ -129,6 +130,28 @@ message PauseMissionCtrl { message ClearMissionCtrl { } +// Clears the mission rules. +message ClearMissionRulesCtrl { +} + +// Run a single instruction right away, outside any mission. +// +// Instructions that do not move the drone (camera, tilt, lights, laser, multibeam, tilt +// stabilization, CV model, annotation, set speed) run in any mission state, alongside a running +// mission. Instructions that move the drone (waypoint, go to home, transect, survey, depth set +// point, relative depth, go to surface or seabed, control mode, set heading) run only while no +// mission is running, one at a time, and are rejected otherwise. A wait is rejected. The outcome +// is reported in InstructionResultTel with the same request_id. +message RunInstructionCtrl { + Instruction instruction = 1; // Instruction to execute. + uint32 request_id = 2; // Client-chosen id, echoed in InstructionResultTel. +} + +// Cancel an instruction started with RunInstructionCtrl. +message CancelInstructionCtrl { + uint32 request_id = 1; // The request_id given to RunInstructionCtrl. +} + // Issue a command to reset the position estimate. message ResetPositionCtrl { ResetPositionSettings settings = 1; // Reset settings. diff --git a/protobuf_definitions/message_formats.proto b/protobuf_definitions/message_formats.proto index e30431be..5a10be32 100644 --- a/protobuf_definitions/message_formats.proto +++ b/protobuf_definitions/message_formats.proto @@ -806,6 +806,14 @@ enum NotificationType { NOTIFICATION_TYPE_DVL_THERMAL_PROTECTION_MODE_DETECTED = 32; // DVL thermal protection mode detected. NOTIFICATION_TYPE_VIDEO_PARTITION_NOT_MOUNTED = 33; // Video partition not mounted. NOTIFICATION_TYPE_DVL_PROTOCOL_UNSUPPORTED = 34; // DVL firmware speaks a protocol version Blunux does not support. + NOTIFICATION_TYPE_INSTRUCTION_STARTED = 35; // An instruction started; the value names it or is its target. + NOTIFICATION_TYPE_MISSION_PAUSED_BY_INSTRUCTION = 36; // Paused after the instruction whose id is the value. + NOTIFICATION_TYPE_MISSION_UPDATED = 37; // The loaded mission was changed; the value says what changed. + NOTIFICATION_TYPE_MISSION_RULE_FIRED = 38; // A mission rule fired; the value is its name. + NOTIFICATION_TYPE_MISSION_RULE_RELEASED = 39; // A mission rule released; the value is its name. + // A multibeam instruction was made for a different sonar than the one the multibeam driver + // reports, so it was skipped; the value is the id of the skipped instruction. + NOTIFICATION_TYPE_MULTIBEAM_SONAR_MISMATCH = 40; } // List of available notification levels. diff --git a/protobuf_definitions/mission_planning.proto b/protobuf_definitions/mission_planning.proto index 2a6dc8a1..7c554262 100644 --- a/protobuf_definitions/mission_planning.proto +++ b/protobuf_definitions/mission_planning.proto @@ -4,6 +4,7 @@ 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; @@ -19,13 +20,14 @@ message Mission { float default_surge_speed = 7; // Default cruise speed of the mission (m/s) (optional). float default_heave_speed = 8; // Default heave speed of the mission (m/s) (optional). float default_circle_of_acceptance = 9; // Default circle of acceptance for waypoints (m) (optional). + uint32 revision = 10; // Set by the drone, increases with every SetInstructionUpdateReq. } // A mission consists of one or multiple instructions. One instruction can be of different types. message Instruction { uint32 id = 1; uint32 group_id = 2; // Group id used for polygons. - bool auto_continue = 3; // False will pause the mission after this instruction. + bool pause_on_completion = 3; // Pause the mission after this instruction. RunMissionCtrl continues it. oneof command { WaypointCommand waypoint_command = 4; // Go to waypoint. DepthSetPointCommand depth_set_point_command = 5; // Go to depth. @@ -38,6 +40,18 @@ message Instruction { GoToSeabedCommand go_to_seabed_command = 12; // Go to seabed. GoToHomeCommand go_to_home_command = 13; // Go to home position. SurveyCommand survey_command = 14; // Execute a survey pattern over a polygon area. + MultibeamRecordCommand multibeam_record_command = 15; // Start or stop multibeam recording. + MultibeamDriverCommand multibeam_driver_command = 16; // Start or stop the multibeam driver. + SetMultibeamConfigCommand set_multibeam_config_command = 17; // Change the multibeam configuration. + SetLightsCommand set_lights_command = 18; // Set the main or guest port lights. + SetLaserCommand set_laser_command = 19; // Set the laser. + SetTiltStabilizationCommand set_tilt_stabilization_command = 20; // Enable or disable tilt stabilization. + SetCvModelCommand set_cv_model_command = 21; // Start or stop a computer vision model. + AnnotationCommand annotation_command = 22; // Record an annotation in the dive log. + SetHeadingCommand set_heading_command = 23; // Turn to a heading and hold it. + TransectCommand transect_command = 24; // Drive a straight line on a course for a distance. + RelativeDepthCommand relative_depth_command = 25; // Change the depth by an amount. + SetSpeedCommand set_speed_command = 26; // Change the cruise speed for the rest of the mission. } } @@ -79,6 +93,7 @@ enum ControlModeHorizontal { CONTROL_MODE_HORIZONTAL_MANUAL = 1; // Manual control mode. CONTROL_MODE_HORIZONTAL_AUTO_HEADING = 2; // Auto heading control mode. CONTROL_MODE_HORIZONTAL_STATION_KEEPING = 3; // Station keeping control mode. + CONTROL_MODE_HORIZONTAL_WEATHER_VANING = 4; // Weather vaning control mode (station keeping into the current). } // A ControlModeCommand is used to set vertical and horizontal control mode during a mission. @@ -87,6 +102,125 @@ message ControlModeCommand { ControlModeHorizontal control_mode_horizontal = 6; // Desired control mode in surge and yaw. } +// DetectionTrigger is satisfied by what the computer vision models see. +// +// The trigger is satisfied while at least one detection of the running models matches all of the +// filters below. The models have to be running; a trigger for a model that is not running is not +// satisfied and TriggerStatus reports sensor_unavailable. +message DetectionTrigger { + reserved 6, 7, 8; // hold_ms, release_ms and cooldown_ms, which moved to MissionRule. + reserved "hold_ms", "release_ms", "cooldown_ms"; + repeated string class_names = 1; // Any of these class names; empty matches every class. + string model_name = 2; // Only detections from this model; empty matches every model. + Camera camera = 3; // Only detections from this camera; unspecified matches every camera. + float min_confidence = 4; // Detections below this confidence are ignored (0..1). + float min_area_fraction = 5; // Least bounding box area over image area a detection must have (0..1). +} + +// DepthTrigger is satisfied while the drone is between two depths below the surface. +// +// A depth with a tolerance is the two bounds around it: 9 and 11 for ten metres give or take a +// metre. Leaving max_depth at zero gives no upper bound, so a min_depth of 20 on its own is +// satisfied deeper than 20 m. Neither bound may be negative, and a max_depth that is set has to be +// above min_depth: the two are the edges of a band, not a single depth, and a rule whose band has +// no width is refused rather than left never firing. The drone measures its depth from the moment +// it is powered on, so this trigger reports sensor_unavailable only while no depth has reached the +// mission at all. +message DepthTrigger { + float min_depth = 1; // Shallowest depth that satisfies the trigger (m below the surface). + float max_depth = 2; // Deepest depth that satisfies the trigger (m). Zero means no upper bound. +} + +// AltitudeTrigger is satisfied while the drone is between two heights above the seabed. +// +// A height with a tolerance is the two bounds around it: 2.5 and 3.5 for three metres off the +// bottom give or take half a metre. Leaving max_altitude at zero gives no upper bound. Neither +// bound may be negative, and a max_altitude that is set has to be above min_altitude, as for +// DepthTrigger. The height comes from a DVL or an altimeter; with neither mounted, out of its +// range, or with a reading the drone does not trust, the trigger is simply not satisfied and +// TriggerStatus reports sensor_unavailable. +message AltitudeTrigger { + float min_altitude = 1; // Lowest height above the seabed that satisfies the trigger (m). + float max_altitude = 2; // Highest height that satisfies the trigger (m). Zero means no upper bound. +} + +// LocationTrigger is satisfied while the drone is inside a circle around a position. +// +// Entering the circle makes the trigger satisfied and leaving it makes it unsatisfied, so a rule +// with one location trigger can start a recording in its enter instructions and stop it in its exit +// instructions. Any valid position estimate will do, whichever sensors it is built from; while the +// drone has none the trigger is simply not satisfied and TriggerStatus reports sensor_unavailable. +message LocationTrigger { + LatLongPosition position = 1; // Centre of the circle (decimal degrees). + float circle_of_acceptance = 2; // Radius of the circle around the position (m). Must be above zero. +} + +// Trigger is one condition of a mission rule. +message Trigger { + oneof condition { + DetectionTrigger detection_trigger = 1; // Satisfied by a detection from a computer vision model. + DepthTrigger depth_trigger = 2; // Satisfied at a depth below the surface. + AltitudeTrigger altitude_trigger = 3; // Satisfied at a height above the seabed. + LocationTrigger location_trigger = 4; // Satisfied inside a circle around a position. + } +} + +// Which condition a Trigger carries. The values are the field numbers of the Trigger oneof. +enum TriggerKind { + TRIGGER_KIND_UNSPECIFIED = 0; // No condition is set. + TRIGGER_KIND_DETECTION = 1; // A DetectionTrigger. + TRIGGER_KIND_DEPTH = 2; // A DepthTrigger. + TRIGGER_KIND_ALTITUDE = 3; // An AltitudeTrigger. + TRIGGER_KIND_LOCATION = 4; // A LocationTrigger. +} + +// MissionRule runs instructions when all of its triggers are satisfied at once and when they stop +// being so. A drone runs at most 16 rules with at most 8 triggers and 8 instructions per list, and +// a rule has to carry at least one instruction in one of its lists. A rule with no triggers never +// fires. Instructions that move the drone (waypoint, go to home, +// transect, survey, depth set point, relative depth, go to surface or seabed, control mode, set +// heading) and waits are refused in rules; camera, tilt, lights, laser, multibeam, tilt +// stabilization, CV model, annotation and set speed are allowed. +message MissionRule { + reserved 4; // The single detection_trigger, replaced by the triggers list. + reserved "detection_trigger"; + uint32 id = 1; // Rule id, defined by the client. Must be greater than zero and unique. + string name = 2; // Rule name, for the log and the app. + bool disabled = 3; // A disabled rule never fires. Rules run unless this is set. + repeated Instruction enter_instructions = 5; // Run when the rule fires. + repeated Instruction exit_instructions = 6; // Run when the rule releases. + repeated Trigger triggers = 7; // Conditions that must all be satisfied at once for the rule to fire. + uint32 hold_ms = 8; // How long all the triggers must stay satisfied before the rule fires. At most an hour. + uint32 release_ms = 9; // How long a trigger must stay unsatisfied before the rule releases. At most an hour. + uint32 cooldown_ms = 10; // Time after a release before the rule can fire again. At most an hour. +} + +// MissionRuleStatus is the state of one rule. +message MissionRuleStatus { + uint32 id = 1; // Rule id. + MissionRuleState state = 2; // What the rule is doing. + uint32 episodes = 3; // Times the rule has fired since it was set. + google.protobuf.Timestamp last_fired = 4; // When the rule last fired; unset before the first time. + InstructionResultState last_result = 5; // Outcome of the last instruction the rule ran. + string last_reason = 6; // Reason of the last result, when there is one. + repeated TriggerStatus triggers = 7; // One entry per trigger, in the order the rule lists them. +} + +// TriggerStatus is the state of one trigger of a rule, so a client can show which condition is +// holding the rule back. +message TriggerStatus { + TriggerKind kind = 1; // Which condition the trigger carries. + bool satisfied = 2; // True while the condition holds. + bool sensor_unavailable = 3; // True while the drone has no reading to judge the condition by. +} + +// InsertInstruction adds an instruction to the loaded mission. +message InsertInstruction { + Instruction instruction = 1; // The instruction to add. Its id must be new to the mission. + uint32 before_id = 2; // Id of the instruction to insert in front of, unless append is set. + bool append = 3; // Add the instruction at the end of the mission. +} + // A WaypointCommand will request the drone to drive to a point automatically. message WaypointCommand { Waypoint waypoint = 1; // Waypoint to go to. @@ -116,6 +250,8 @@ message WaitForCommand { message CameraCommand { CameraAction camera_action = 1; // Camera command. float action_param = 2; // Used for taking photos based on a time or distance interval. + Camera camera = 3; // Which camera the action applies to. The main camera when unspecified. + StorageLocation storage_location = 4; // Where to store the photos or the recording. Internal when unspecified. } // GoToSurfaceCommand is used to go to the surface. @@ -133,6 +269,109 @@ message GoToHomeCommand { float desired_speed = 1; // Desired speed to home (m/s). } +// MultibeamRecordCommand starts or stops recording of the multibeam sonar. +message MultibeamRecordCommand { + bool enable = 1; // True to start recording, false to stop. + StorageLocation storage_location = 2; // Where to store the recording. Internal when unspecified. +} + +// MultibeamDriverCommand starts or stops the multibeam sonar driver. +// +// The instruction completes when the driver reports the requested state, so a following +// MultibeamRecordCommand or TiltMultibeamCommand finds the sonar ready. +message MultibeamDriverCommand { + bool activate = 1; // True to start the driver, false to stop it. + MultibeamConfig config = 2; // Configuration to start with. When left out, the last known configuration is used. + // The sonar `config` was made for, as the multibeam driver reports it. The driver still starts or + // stops when a different sonar is connected, but `config` is skipped in favour of the last + // configuration the driver reported and the drone sends a + // NOTIFICATION_TYPE_MULTIBEAM_SONAR_MISMATCH notification. Unspecified applies `config` to + // whichever sonar is connected. A `config` that the sonar cannot run, measured the same way as + // for SetMultibeamConfigCommand, skips the whole instruction instead, so the driver is never + // started with a configuration nobody asked for. + GuestPortDeviceID expected_device_id = 3; +} + +// SetMultibeamConfigCommand changes the configuration of a running multibeam sonar. +// +// A configuration is made for one sonar, because ranges, frequencies and beam counts differ between +// them. The drone compares expected_device_id with the sonar the multibeam driver reports, not with +// what the guest port discovery found, and when they differ it skips the whole instruction and +// sends a NOTIFICATION_TYPE_MULTIBEAM_SONAR_MISMATCH notification; the mission carries on with the +// next instruction. Unspecified applies the configuration to whichever sonar is connected. +// +// The configuration is also measured against what that sonar can do: the range for the frequency +// mode asked for, a high frequency mode on a sonar that has only one, and the number of beams. A +// configuration outside them skips the instruction with a reason naming the sonar. Nothing is +// quietly clamped, so a sonar never ends up running a configuration nobody asked for. +message SetMultibeamConfigCommand { + MultibeamConfig config = 1; // The configuration to apply. + GuestPortDeviceID expected_device_id = 2; // The sonar this configuration was made for. +} + +// Which lights a SetLightsCommand applies to. +enum LightTarget { + LIGHT_TARGET_UNSPECIFIED = 0; // Unspecified, treated as the main lights. + LIGHT_TARGET_MAIN = 1; // The drone's own lights. + LIGHT_TARGET_GUESTPORT = 2; // Lights connected to a guest port. +} + +// SetLightsCommand sets the intensity of the lights. +message SetLightsCommand { + float intensity = 1; // Light intensity (0..1). + LightTarget target = 2; // Which lights to set. +} + +// SetLaserCommand sets the laser connected to a guest port. +message SetLaserCommand { + Laser laser = 1; // Laser intensity and modulation. +} + +// SetTiltStabilizationCommand enables or disables camera tilt stabilization. +message SetTiltStabilizationCommand { + bool enabled = 1; // True to stabilize the camera tilt. +} + +// SetCvModelCommand starts or stops a computer vision model on the drone. +// +// The instruction completes when the model reports the requested state. +message SetCvModelCommand { + string package_id = 1; // The model package to start or stop, as listed in CvModelInfo. + bool running = 2; // True to start the model, false to stop it. +} + +// AnnotationCommand records an annotation in the dive log, as AnnotationCtrl does. +message AnnotationCommand { + Annotation annotation = 1; // The annotation to record. The drone sets the timestamp and the source. +} + +// SetHeadingCommand turns the drone to a heading and holds it with auto heading. +message SetHeadingCommand { + float heading = 1; // Target heading relative to north (deg, 0..360), or the turn when relative is set. + bool relative = 2; // True to turn by `heading` degrees from the current heading; positive is clockwise. +} + +// TransectCommand drives a straight line from the current position, like a waypoint the drone +// computes itself from a course and a distance. +message TransectCommand { + float course = 1; // Course relative to north (deg, 0..360). + float distance = 2; // Length of the line (m). + float speed = 3; // Desired speed over ground (m/s). The mission default when 0. + DepthSetPoint depth_set_point = 4; // Optional depth or altitude to hold along the line. + float circle_of_acceptance = 5; // Radius of acceptance at the end of the line (m). The mission default when 0. +} + +// RelativeDepthCommand changes the depth by an amount from where the drone is when it starts. +message RelativeDepthCommand { + float depth_change = 1; // Change in depth (m); positive is deeper, negative shallower. + float speed = 2; // Desired speed to the new depth (m/s). The mission default when 0. +} + +// SetSpeedCommand changes the cruise speed used by the following instructions of the mission. +message SetSpeedCommand { + float cruise_speed = 1; // Desired surge speed (m/s). Must be greater than zero. +} + // A SurveyCommand defines a survey (lawn-mower) pattern over a polygon area. // The planner generates waypoints from the polygon + parameters and stores them // in the waypoints field. The drone executes these pre-computed waypoints directly @@ -207,6 +446,18 @@ enum InstructionType { INSTRUCTION_TYPE_GO_TO_SEABED = 11; // Go to the seabed. INSTRUCTION_TYPE_GO_TO_HOME = 12; // Returning to home. INSTRUCTION_TYPE_SURVEY = 13; // Execute a survey pattern over a polygon area. + INSTRUCTION_TYPE_MULTIBEAM_RECORD = 14; // Start or stop multibeam recording. + INSTRUCTION_TYPE_MULTIBEAM_DRIVER = 15; // Start or stop the multibeam driver. + INSTRUCTION_TYPE_SET_MULTIBEAM_CONFIG = 16; // Change the multibeam configuration. + INSTRUCTION_TYPE_SET_LIGHTS = 17; // Set the lights. + INSTRUCTION_TYPE_SET_LASER = 18; // Set the laser. + INSTRUCTION_TYPE_SET_TILT_STABILIZATION = 19; // Enable or disable tilt stabilization. + INSTRUCTION_TYPE_SET_CV_MODEL = 20; // Start or stop a computer vision model. + INSTRUCTION_TYPE_ANNOTATION = 21; // Record an annotation. + INSTRUCTION_TYPE_SET_HEADING = 22; // Turn to a heading. + INSTRUCTION_TYPE_TRANSECT = 23; // Drive a straight line. + INSTRUCTION_TYPE_RELATIVE_DEPTH = 24; // Change the depth by an amount. + INSTRUCTION_TYPE_SET_SPEED = 25; // Change the cruise speed. } // Reference for the auto pilot when a mission is active. @@ -249,4 +500,34 @@ message MissionStatus { repeated uint32 completed_path_segment_ids = 7; // Ids of the completed path segments. uint32 total_number_of_path_segments = 8; // Total number of path segments in the mission. uint32 id = 9; // Mission id of the active mission. + int32 active_instruction_id = 10; // Id of the instruction that is running, -1 when none. + uint32 revision = 11; // Revision of the loaded mission, see Mission.revision. + string failure_reason = 12; // Why the mission failed to load or start or was aborted by the drone. +} + +// Outcome of an instruction started with RunInstructionCtrl. +enum InstructionResultState { + INSTRUCTION_RESULT_STATE_UNSPECIFIED = 0; // Unspecified. + INSTRUCTION_RESULT_STATE_ACCEPTED = 1; // The instruction was accepted and has started. + INSTRUCTION_RESULT_STATE_REJECTED = 2; // The instruction was not started, see reason. + INSTRUCTION_RESULT_STATE_COMPLETED = 3; // The instruction completed. + INSTRUCTION_RESULT_STATE_FAILED = 4; // The instruction stopped before completing, see reason. + INSTRUCTION_RESULT_STATE_CANCELLED = 5; // The instruction was cancelled by a client or by the drone. +} + +// MissionRuleState is where a rule is in its episode. +enum MissionRuleState { + MISSION_RULE_STATE_UNSPECIFIED = 0; // Unspecified. + MISSION_RULE_STATE_IDLE = 1; // Waiting for the trigger. + MISSION_RULE_STATE_ACTIVE = 2; // The rule has fired and has not released yet. + MISSION_RULE_STATE_COOLDOWN = 3; // Released; waiting for the cooldown before it can fire again. + MISSION_RULE_STATE_DISABLED = 4; // The rule is disabled. +} + +// Result of an instruction started with RunInstructionCtrl. +message InstructionResult { + uint32 request_id = 1; // The request_id given to RunInstructionCtrl. + uint32 instruction_id = 2; // Id of the instruction, as given by the client. + InstructionResultState state = 3; // Outcome so far. + string reason = 4; // Why the instruction was rejected, failed or was cancelled (optional). } diff --git a/protobuf_definitions/req_rep.proto b/protobuf_definitions/req_rep.proto index 8e563151..26ea807a 100644 --- a/protobuf_definitions/req_rep.proto +++ b/protobuf_definitions/req_rep.proto @@ -135,6 +135,10 @@ message SetMissionReq { // Response after setting a new mission. message SetMissionRep { + // True when the mission was loaded. Drones without this field answer an empty message, so not + // accepted with no reason means the mission was loaded the old way. + bool accepted = 1; + string reason = 2; // Why the mission was refused. } // Service request to the reference_generator to get the active mission. @@ -146,13 +150,43 @@ message GetMissionRep { Mission mission = 1; // Active mission with waypoints. } -// Updates an instruction in current mission with a new instruction payload. +// Changes one instruction of the loaded mission. The mission is validated and +// planned again as a whole and Mission.revision increases. An instruction that +// has completed cannot be changed. Replacing or removing the instruction that +// is running stops it; a replacement starts from where the drone is. message SetInstructionUpdateReq { - Instruction instruction = 1; // instruction that will replace the desired instruction. + oneof operation { + Instruction replace = 1; // Replace the instruction that has the same id. + InsertInstruction insert = 2; // Add an instruction. + uint32 remove_id = 3; // Remove the instruction with this id. + } } // Response after updating an instruction in the current mission. message SetInstructionUpdateRep { + bool accepted = 1; // True when the mission was changed. + string reason = 2; // Why the change was refused. + uint32 revision = 3; // Revision of the mission after the change. +} + +// Replaces the mission rules. Rules run independently of missions. +message SetMissionRulesReq { + repeated MissionRule rules = 1; // The rules to run; an empty list clears them. +} + +// Response after setting the mission rules. +message SetMissionRulesRep { + bool accepted = 1; // True when the rules were set. + string reason = 2; // Why the rules were refused. +} + +// Get the mission rules. +message GetMissionRulesReq { +} + +// The mission rules. +message GetMissionRulesRep { + repeated MissionRule rules = 1; // The rules currently set. } // Set the heading mode used in dead reckoning. diff --git a/protobuf_definitions/telemetry.proto b/protobuf_definitions/telemetry.proto index 73fee511..21aeb036 100644 --- a/protobuf_definitions/telemetry.proto +++ b/protobuf_definitions/telemetry.proto @@ -65,6 +65,19 @@ message NotificationTel { Notification notification = 1; // Notification from the control system. } +// Result of an instruction started with RunInstructionCtrl. +// +// Published when the instruction is accepted or rejected, and again when it completes, fails or +// is cancelled. +message InstructionResultTel { + InstructionResult result = 1; // Result of the instruction. +} + +// State of the mission rules. +message MissionRulesTel { + repeated MissionRuleStatus rules = 1; // One entry per rule. +} + // Control force in all directions. message ControlForceTel { ControlForce control_force = 1; // Control force in all directions.