// Copyright 2019-2021 Robotec.ai. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using UnityEngine; using UnityEngine.Profiling; using System; namespace ROS2 { /// /// An abstract base class for ROS2-enabled sensor. /// public abstract class ISensor : MonoBehaviour { /// /// The desired update frequency for the sensor. The maximum can be the rate with which FixedUpdate is called, /// which depends on the physics step (usually 50 or 100 times per second). /// public double desiredUpdateFreq = 25.0; /// /// The frameID corresponds to the ROS frame_id element of the header and is important /// for transformations /// public string frameID = "sensor"; /// /// A topic to which the sensor publishes. Only one per sensor. Don't add the namespace of /// the agent name, it is handled externally (i.e. sensor does not know to what object it belongs). /// public string topicName = ""; /// /// Controls whether sensor is publishing messages /// public bool publishing = false; /// /// Creates sensor publishers and registers it in the executor so that it publishes when new data is available /// /// Central ros2 monobehavior for Unity /// ros2 node that will publish sensor data /// name of the agent (vehicle) to be added to the sensor publish namespace public abstract void CreateROSParticipants(ROS2UnityComponent ros2Unity, ROS2Node node, string agentName); /// /// Returns the constructed frame name, taking in account the agent name(space) /// public abstract string frameName(); } /// /// A base template class for the sensor. The type is the message type of sensor data. /// public abstract class Sensor : ISensor where T : MessageWithHeader, new() { /// /// Acquires the value by performing sensor type characteristic computations (e.g. raycasts). /// Implemented in subclasses. /// /// The message which contains the sensor data. /// Mind that the header for message is handled in a generic way by this class. protected abstract T AcquireValue(); /// /// Returns true when there is a new data available from sensor. /// protected abstract bool HasNewData(); protected double desiredFrameTime = 0.0; private const double minimumFrequency = 0.001; private Publisher publisher; private Subscription clockSubscriber; private ROS2UnityComponent ros2UnityComponent; private ROS2Node ros2Node; private string ownerAgentName; private double lastTimestamp; private double timeSinceLastFixedUpdate; private T readings; private bool newReadings; public override string frameName() { return ownerAgentName + "/" + frameID; } /// /// Visualises the effects of the sensor. It doesn't make sense for some sensor and the /// default implementation is empty. /// protected virtual void VisualiseEffects() { } /// /// When parameters in editor change (i.e. frequency), /// this function is called to calculate new frame time. /// protected virtual void OnValidate() { CalculateFrameTime(); } /// /// An entry point for the per-frame processing done in subclass /// protected virtual void OnUpdate() {} /// /// See superclass definition /// public override void CreateROSParticipants(ROS2UnityComponent ros2Unity, ROS2Node node, string agentName) { if (!ros2Unity.Ok()) { throw new System.InvalidOperationException("Publisher for sensor can't be created when node is not OK"); } if (String.IsNullOrEmpty(topicName)) { throw new System.InvalidOperationException("Topic name not set for the sensor " + this); } ownerAgentName = agentName; ros2UnityComponent = ros2Unity; ros2Node = node; string nsName = agentName.Replace(" ", "_"); publisher = node.CreateSensorPublisher(nsName + "/" + topicName); ros2UnityComponent.RegisterExecutable(ExecutorThreadSensorPublishAction); publishing = true; } /// /// This is executed in an executor thread (through RegisterExecutable) /// Sensor fequency is indirectly handed through newReadings, which are acquired at a requested /// frequency if possible (e. g. due to simulation resource constraints) /// internal void ExecutorThreadSensorPublishAction() { if (!HasNewData()) return; if (publisher != null & publishing) { if (ros2UnityComponent.Ok()) { readings = AcquireValue(); readings.SetHeaderFrame(frameName()); if (readings != null) { MessageWithHeader readingsHeader = readings as MessageWithHeader; ros2Node.clock.UpdateROSTimestamp(ref readingsHeader); publisher.Publish(readings); } } } } /// /// Once each frame, visualise effects of the sensor (if any). Visualisation /// rate is independent of publishing/acquisition rate, which happen at the sensor /// frequency instead of the app frame rate. /// void Update() { VisualiseEffects(); OnUpdate(); } /// /// Initialize header and calculate frame time /// void Awake() { // turn on publishing on start publishing = true; CalculateFrameTime(); lastTimestamp = DateTime.UtcNow.Ticks / 1E7; } /// /// Sensor frequency is used to calculate frame time, based on desired frequency and the bounds. /// void CalculateFrameTime() { double maxFrameFreq = 1.0 / Time.fixedDeltaTime; if (desiredUpdateFreq > maxFrameFreq) { Debug.LogWarning("Desired frame rate of " + desiredUpdateFreq + " can't be met, " + "physics frequency is " + maxFrameFreq); desiredUpdateFreq = maxFrameFreq; //Can't go faster than physics } if (desiredUpdateFreq < minimumFrequency) { Debug.LogWarning("Minimum frequency of " + minimumFrequency + " applied instead of " + desiredUpdateFreq); desiredUpdateFreq = minimumFrequency; } desiredFrameTime = 1.0 / desiredUpdateFreq; } } } // namespace ROS2