chassis_base.cpp
Go to the documentation of this file.
1 /*******************************************************************************
2  * BSD 3-Clause License
3  *
4  * Copyright (c) 2021, Qiayuan Liao
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are met:
9  *
10  * * Redistributions of source code must retain the above copyright notice, this
11  * list of conditions and the following disclaimer.
12  *
13  * * Redistributions in binary form must reproduce the above copyright notice,
14  * this list of conditions and the following disclaimer in the documentation
15  * and/or other materials provided with the distribution.
16  *
17  * * Neither the name of the copyright holder nor the names of its
18  * contributors may be used to endorse or promote products derived from
19  * this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24  * ARE
25  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32  *******************************************************************************/
33 
34 //
35 // Created by huakang on 2021/3/21.
36 //
40 #include <rm_common/ori_tool.h>
42 #include <angles/angles.h>
44 
45 namespace rm_chassis_controllers
46 {
47 template class ChassisBase<rm_control::RobotStateInterface, hardware_interface::EffortJointInterface>;
50 
51 template <typename... T>
53  ros::NodeHandle& controller_nh)
54 {
55  if (!controller_nh.getParam("publish_rate", publish_rate_) || !controller_nh.getParam("timeout", timeout_) ||
56  !controller_nh.getParam("power/vel_coeff", velocity_coeff_) ||
57  !controller_nh.getParam("power/effort_coeff", effort_coeff_) ||
58  !controller_nh.getParam("power/power_offset", power_offset_))
59  {
60  ROS_ERROR("Some chassis params doesn't given (namespace: %s)", controller_nh.getNamespace().c_str());
61  return false;
62  }
63  wheel_radius_ = getParam(controller_nh, "wheel_radius", 0.02);
64  twist_angular_ = getParam(controller_nh, "twist_angular", M_PI / 6);
65  max_odom_vel_ = getParam(controller_nh, "max_odom_vel", 0);
66  enable_odom_tf_ = getParam(controller_nh, "enable_odom_tf", true);
67  publish_odom_tf_ = getParam(controller_nh, "publish_odom_tf", false);
68 
69  // Get and check params for covariances
70  XmlRpc::XmlRpcValue twist_cov_list;
71  controller_nh.getParam("twist_covariance_diagonal", twist_cov_list);
73  ROS_ASSERT(twist_cov_list.size() == 6);
74  for (int i = 0; i < twist_cov_list.size(); ++i)
75  ROS_ASSERT(twist_cov_list[i].getType() == XmlRpc::XmlRpcValue::TypeDouble);
76 
77  robot_state_handle_ = robot_hw->get<rm_control::RobotStateInterface>()->getHandle("robot_state");
78  effort_joint_interface_ = robot_hw->get<hardware_interface::EffortJointInterface>();
79 
80  // Setup odometry realtime publisher + odom message constant fields
81  odom_pub_.reset(new realtime_tools::RealtimePublisher<nav_msgs::Odometry>(root_nh, "odom", 100));
82  odom_pub_->msg_.header.frame_id = "odom";
83  odom_pub_->msg_.child_frame_id = "base_link";
84  odom_pub_->msg_.twist.covariance = { static_cast<double>(twist_cov_list[0]), 0., 0., 0., 0., 0., 0.,
85  static_cast<double>(twist_cov_list[1]), 0., 0., 0., 0., 0., 0.,
86  static_cast<double>(twist_cov_list[2]), 0., 0., 0., 0., 0., 0.,
87  static_cast<double>(twist_cov_list[3]), 0., 0., 0., 0., 0., 0.,
88  static_cast<double>(twist_cov_list[4]), 0., 0., 0., 0., 0., 0.,
89  static_cast<double>(twist_cov_list[5]) };
90 
91  ramp_x_ = new RampFilter<double>(0, 0.001);
92  ramp_y_ = new RampFilter<double>(0, 0.001);
93  ramp_w_ = new RampFilter<double>(0, 0.001);
94 
95  // init odom tf
96  if (enable_odom_tf_)
97  {
98  odom2base_.header.frame_id = "odom";
99  odom2base_.header.stamp = ros::Time::now();
100  odom2base_.child_frame_id = "base_link";
101  odom2base_.transform.rotation.w = 1;
102  tf_broadcaster_.init(root_nh);
103  tf_broadcaster_.sendTransform(odom2base_);
104  }
105  world2odom_.setRotation(tf2::Quaternion::getIdentity());
106 
107  outside_odom_sub_ =
108  controller_nh.subscribe<nav_msgs::Odometry>("/odometry", 10, &ChassisBase::outsideOdomCallback, this);
109  cmd_chassis_sub_ =
110  controller_nh.subscribe<rm_msgs::ChassisCmd>("/cmd_chassis", 1, &ChassisBase::cmdChassisCallback, this);
111  cmd_vel_sub_ = root_nh.subscribe<geometry_msgs::Twist>("cmd_vel", 1, &ChassisBase::cmdVelCallback, this);
112 
113  if (controller_nh.hasParam("pid_follow"))
114  if (!pid_follow_.init(ros::NodeHandle(controller_nh, "pid_follow")))
115  return false;
116 
117  return true;
118 }
119 
120 template <typename... T>
121 void ChassisBase<T...>::update(const ros::Time& time, const ros::Duration& period)
122 {
123  rm_msgs::ChassisCmd cmd_chassis = cmd_rt_buffer_.readFromRT()->cmd_chassis_;
124  geometry_msgs::Twist cmd_vel = cmd_rt_buffer_.readFromRT()->cmd_vel_;
125 
126  if ((time - cmd_rt_buffer_.readFromRT()->stamp_).toSec() > timeout_)
127  {
128  vel_cmd_.x = 0.;
129  vel_cmd_.y = 0.;
130  vel_cmd_.z = 0.;
131  }
132  else
133  {
134  ramp_x_->setAcc(cmd_chassis.accel.linear.x);
135  ramp_y_->setAcc(cmd_chassis.accel.linear.y);
136  ramp_x_->input(cmd_vel.linear.x);
137  ramp_y_->input(cmd_vel.linear.y);
138  vel_cmd_.x = ramp_x_->output();
139  vel_cmd_.y = ramp_y_->output();
140  vel_cmd_.z = cmd_vel.angular.z;
141  }
142 
143  if (cmd_rt_buffer_.readFromRT()->cmd_chassis_.follow_source_frame.empty())
144  follow_source_frame_ = "yaw";
145  else
146  follow_source_frame_ = cmd_rt_buffer_.readFromRT()->cmd_chassis_.follow_source_frame;
147  if (cmd_rt_buffer_.readFromRT()->cmd_chassis_.command_source_frame.empty())
148  command_source_frame_ = "yaw";
149  else
150  command_source_frame_ = cmd_rt_buffer_.readFromRT()->cmd_chassis_.command_source_frame;
151 
152  if (state_ != cmd_chassis.mode)
153  {
154  state_ = cmd_chassis.mode;
155  state_changed_ = true;
156  }
157 
158  updateOdom(time, period);
159 
160  switch (state_)
161  {
162  case RAW:
163  raw();
164  break;
165  case FOLLOW:
166  follow(time, period);
167  break;
168  case TWIST:
169  twist(time, period);
170  break;
171  }
172 
173  ramp_w_->setAcc(cmd_chassis.accel.angular.z);
174  ramp_w_->input(vel_cmd_.z);
175  vel_cmd_.z = ramp_w_->output();
176 
177  moveJoint(time, period);
178  powerLimit();
179 }
180 
181 template <typename... T>
182 void ChassisBase<T...>::follow(const ros::Time& time, const ros::Duration& period)
183 {
184  if (state_changed_)
185  {
186  state_changed_ = false;
187  ROS_INFO("[Chassis] Enter FOLLOW");
188 
189  recovery();
190  pid_follow_.reset();
191  }
192 
193  tfVelToBase(command_source_frame_);
194  try
195  {
196  double roll{}, pitch{}, yaw{};
197  quatToRPY(robot_state_handle_.lookupTransform("base_link", follow_source_frame_, ros::Time(0)).transform.rotation,
198  roll, pitch, yaw);
199  double follow_error = angles::shortest_angular_distance(yaw, 0);
200  pid_follow_.computeCommand(-follow_error, period);
201  vel_cmd_.z = pid_follow_.getCurrentCmd() + cmd_rt_buffer_.readFromRT()->cmd_chassis_.follow_vel_des;
202  }
203  catch (tf2::TransformException& ex)
204  {
205  ROS_WARN("%s", ex.what());
206  }
207 }
208 
209 template <typename... T>
210 void ChassisBase<T...>::twist(const ros::Time& time, const ros::Duration& period)
211 {
212  if (state_changed_)
213  {
214  state_changed_ = false;
215  ROS_INFO("[Chassis] Enter TWIST");
216 
217  recovery();
218  pid_follow_.reset();
219  }
220  tfVelToBase(command_source_frame_);
221  try
222  {
223  double roll{}, pitch{}, yaw{};
224  quatToRPY(robot_state_handle_.lookupTransform("base_link", command_source_frame_, ros::Time(0)).transform.rotation,
225  roll, pitch, yaw);
226 
227  double angle[4] = { -0.785, 0.785, 2.355, -2.355 };
228  double off_set = 0.0;
229  for (double i : angle)
230  {
231  if (std::abs(angles::shortest_angular_distance(yaw, i)) < 0.79)
232  {
233  off_set = i;
234  break;
235  }
236  }
237  double follow_error =
238  angles::shortest_angular_distance(yaw, twist_angular_ * sin(2 * M_PI * time.toSec()) + off_set);
239 
240  pid_follow_.computeCommand(-follow_error, period); // The actual output is opposite to the calculated value
241  vel_cmd_.z = pid_follow_.getCurrentCmd();
242  }
243  catch (tf2::TransformException& ex)
244  {
245  ROS_WARN("%s", ex.what());
246  }
247 }
248 
249 template <typename... T>
251 {
252  if (state_changed_)
253  {
254  state_changed_ = false;
255  ROS_INFO("[Chassis] Enter RAW");
256 
257  recovery();
258  }
259  tfVelToBase(command_source_frame_);
260 }
261 
262 template <typename... T>
263 void ChassisBase<T...>::updateOdom(const ros::Time& time, const ros::Duration& period)
264 {
265  geometry_msgs::Twist vel_base = odometry(); // on base_link frame
266  if (enable_odom_tf_)
267  {
268  geometry_msgs::Vector3 linear_vel_odom, angular_vel_odom;
269  try
270  {
271  odom2base_ = robot_state_handle_.lookupTransform("odom", "base_link", ros::Time(0));
272  }
273  catch (tf2::TransformException& ex)
274  {
275  tf_broadcaster_.sendTransform(odom2base_); // TODO: For some reason, the sendTransform in init sometime not work?
276  ROS_WARN("%s", ex.what());
277  return;
278  }
279  odom2base_.header.stamp = time;
280  // integral vel to pos and angle
281  tf2::doTransform(vel_base.linear, linear_vel_odom, odom2base_);
282  tf2::doTransform(vel_base.angular, angular_vel_odom, odom2base_);
283  double length =
284  std::sqrt(std::pow(linear_vel_odom.x, 2) + std::pow(linear_vel_odom.y, 2) + std::pow(linear_vel_odom.z, 2));
285  if (length < max_odom_vel_)
286  {
287  // avoid nan vel
288  odom2base_.transform.translation.x += linear_vel_odom.x * period.toSec();
289  odom2base_.transform.translation.y += linear_vel_odom.y * period.toSec();
290  odom2base_.transform.translation.z += linear_vel_odom.z * period.toSec();
291  }
292  length =
293  std::sqrt(std::pow(angular_vel_odom.x, 2) + std::pow(angular_vel_odom.y, 2) + std::pow(angular_vel_odom.z, 2));
294  if (length > 0.001)
295  { // avoid nan quat
296  tf2::Quaternion odom2base_quat, trans_quat;
297  tf2::fromMsg(odom2base_.transform.rotation, odom2base_quat);
298  trans_quat.setRotation(tf2::Vector3(angular_vel_odom.x / length, angular_vel_odom.y / length,
299  angular_vel_odom.z / length),
300  length * period.toSec());
301  odom2base_quat = trans_quat * odom2base_quat;
302  odom2base_quat.normalize();
303  odom2base_.transform.rotation = tf2::toMsg(odom2base_quat);
304  }
305  }
306 
307  if (topic_update_)
308  {
309  auto* odom_msg = odom_buffer_.readFromRT();
310 
311  tf2::Transform world2sensor;
312  world2sensor.setOrigin(
313  tf2::Vector3(odom_msg->pose.pose.position.x, odom_msg->pose.pose.position.y, odom_msg->pose.pose.position.z));
314  world2sensor.setRotation(tf2::Quaternion(odom_msg->pose.pose.orientation.x, odom_msg->pose.pose.orientation.y,
315  odom_msg->pose.pose.orientation.z, odom_msg->pose.pose.orientation.w));
316 
317  if (world2odom_.getRotation() == tf2::Quaternion::getIdentity()) // First received
318  {
319  tf2::Transform odom2sensor;
320  try
321  {
322  geometry_msgs::TransformStamped tf_msg =
323  robot_state_handle_.lookupTransform("odom", "livox_frame", odom_msg->header.stamp);
324  tf2::fromMsg(tf_msg.transform, odom2sensor);
325  }
326  catch (tf2::TransformException& ex)
327  {
328  ROS_WARN("%s", ex.what());
329  return;
330  }
331  world2odom_ = world2sensor * odom2sensor.inverse();
332  }
333  tf2::Transform base2sensor;
334  try
335  {
336  geometry_msgs::TransformStamped tf_msg =
337  robot_state_handle_.lookupTransform("base_link", "livox_frame", odom_msg->header.stamp);
338  tf2::fromMsg(tf_msg.transform, base2sensor);
339  }
340  catch (tf2::TransformException& ex)
341  {
342  ROS_WARN("%s", ex.what());
343  return;
344  }
345  tf2::Transform odom2base = world2odom_.inverse() * world2sensor * base2sensor.inverse();
346  odom2base_.transform.translation.x = odom2base.getOrigin().x();
347  odom2base_.transform.translation.y = odom2base.getOrigin().y();
348  odom2base_.transform.translation.z = odom2base.getOrigin().z();
349  topic_update_ = false;
350  }
351 
352  robot_state_handle_.setTransform(odom2base_, "rm_chassis_controllers");
353 
354  if (publish_rate_ > 0.0 && last_publish_time_ + ros::Duration(1.0 / publish_rate_) < time)
355  {
356  if (odom_pub_->trylock())
357  {
358  odom_pub_->msg_.header.stamp = time;
359  odom_pub_->msg_.twist.twist.linear.x = vel_base.linear.x;
360  odom_pub_->msg_.twist.twist.linear.y = vel_base.linear.y;
361  odom_pub_->msg_.twist.twist.angular.z = vel_base.angular.z;
362  odom_pub_->unlockAndPublish();
363  }
364  if (enable_odom_tf_ && publish_odom_tf_)
365  tf_broadcaster_.sendTransform(odom2base_);
366  last_publish_time_ = time;
367  }
368 }
369 
370 template <typename... T>
372 {
373  ramp_x_->clear(vel_cmd_.x);
374  ramp_y_->clear(vel_cmd_.y);
375  ramp_w_->clear(vel_cmd_.z);
376 }
377 
378 template <typename... T>
380 {
381  double power_limit = cmd_rt_buffer_.readFromRT()->cmd_chassis_.power_limit;
382  // Three coefficients of a quadratic equation in one variable
383  double a = 0., b = 0., c = 0.;
384  for (const auto& joint : joint_handles_)
385  {
386  double cmd_effort = joint.getCommand();
387  double real_vel = joint.getVelocity();
388  if (joint.getName().find("wheel") != std::string::npos) // The pivot joint of swerve drive doesn't need power limit
389  {
390  a += square(cmd_effort);
391  b += std::abs(cmd_effort * real_vel);
392  c += square(real_vel);
393  }
394  }
395  a *= effort_coeff_;
396  c = c * velocity_coeff_ - power_offset_ - power_limit;
397  // Root formula for quadratic equation in one variable
398  double zoom_coeff = (square(b) - 4 * a * c) > 0 ? ((-b + sqrt(square(b) - 4 * a * c)) / (2 * a)) : 0.;
399  for (auto joint : joint_handles_)
400  if (joint.getName().find("wheel") != std::string::npos)
401  {
402  joint.setCommand(zoom_coeff > 1 ? joint.getCommand() : joint.getCommand() * zoom_coeff);
403  }
404 }
405 
406 template <typename... T>
407 void ChassisBase<T...>::tfVelToBase(const std::string& from)
408 {
409  try
410  {
411  tf2::doTransform(vel_cmd_, vel_cmd_, robot_state_handle_.lookupTransform("base_link", from, ros::Time(0)));
412  }
413  catch (tf2::TransformException& ex)
414  {
415  ROS_WARN("%s", ex.what());
416  }
417 }
418 
419 template <typename... T>
420 void ChassisBase<T...>::cmdChassisCallback(const rm_msgs::ChassisCmdConstPtr& msg)
421 {
422  cmd_struct_.cmd_chassis_ = *msg;
423  cmd_rt_buffer_.writeFromNonRT(cmd_struct_);
424 }
425 
426 template <typename... T>
427 void ChassisBase<T...>::cmdVelCallback(const geometry_msgs::Twist::ConstPtr& msg)
428 {
429  cmd_struct_.cmd_vel_ = *msg;
430  cmd_struct_.stamp_ = ros::Time::now();
431  cmd_rt_buffer_.writeFromNonRT(cmd_struct_);
432 }
433 
434 template <typename... T>
435 void ChassisBase<T...>::outsideOdomCallback(const nav_msgs::Odometry::ConstPtr& msg)
436 {
437  odom_buffer_.writeFromNonRT(*msg);
438  topic_update_ = true;
439 }
440 
441 } // namespace rm_chassis_controllers
XmlRpc::XmlRpcValue::size
int size() const
rm_chassis_controllers::ChassisBase::twist
void twist(const ros::Time &time, const ros::Duration &period)
The mode TWIST: Just moving chassis.
Definition: chassis_base.cpp:241
angles::shortest_angular_distance
static double shortest_angular_distance(double from, double to)
RampFilter< double >
tf2::Quaternion::getIdentity
static const Quaternion & getIdentity()
msg
msg
chassis_base.h
angle
TFSIMD_FORCE_INLINE tfScalar angle(const Quaternion &q1, const Quaternion &q2)
tf2::Transform::inverse
Transform inverse() const
rm_chassis_controllers::ChassisBase::update
void update(const ros::Time &time, const ros::Duration &period) override
Receive real_time command from manual. Execute different action according to current mode....
Definition: chassis_base.cpp:152
tf2::fromMsg
void fromMsg(const A &, B &b)
hardware_interface::InterfaceManager::get
T * get()
ros::NodeHandle::getParam
bool getParam(const std::string &key, bool &b) const
rm_chassis_controllers::ChassisBase::recovery
void recovery()
Set chassis velocity to zero.
Definition: chassis_base.cpp:402
rm_chassis_controllers::ChassisBase::follow
void follow(const ros::Time &time, const ros::Duration &period)
The mode FOLLOW: chassis will follow gimbal.
Definition: chassis_base.cpp:213
tf2::Transform::setRotation
TF2SIMD_FORCE_INLINE void setRotation(const Quaternion &q)
rm_control::RobotStateInterface
TimeBase< Time, Duration >::toSec
double toSec() const
tf2::Quaternion::normalize
Quaternion & normalize()
tf2::Transform::getOrigin
TF2SIMD_FORCE_INLINE Vector3 & getOrigin()
getHandle
ROSCONSOLE_CONSOLE_IMPL_DECL void * getHandle(const std::string &name)
imu_sensor_interface.h
realtime_tools::RealtimePublisher
rm_chassis_controllers::ChassisBase::updateOdom
void updateOdom(const ros::Time &time, const ros::Duration &period)
Init frame on base_link. Integral vel to pos and angle.
Definition: chassis_base.cpp:294
XmlRpc::XmlRpcValue::TypeDouble
TypeDouble
hardware_interface::RobotHW
tf2::Transform::setOrigin
TF2SIMD_FORCE_INLINE void setOrigin(const Vector3 &origin)
rm_chassis_controllers::ChassisBase::raw
void raw()
Definition: chassis_base.cpp:281
tf2::Transform
hardware_interface::EffortJointInterface
ros::NodeHandle::hasParam
bool hasParam(const std::string &key) const
ROS_WARN
#define ROS_WARN(...)
hardware_interface::ImuSensorInterface
ros::NodeHandle::subscribe
Subscriber subscribe(const std::string &topic, uint32_t queue_size, const boost::function< void(C)> &callback, const VoidConstPtr &tracked_object=VoidConstPtr(), const TransportHints &transport_hints=TransportHints())
math_utilities.h
XmlRpc::XmlRpcValue::getType
const Type & getType() const
tf2::Quaternion::setRotation
void setRotation(const Vector3 &axis, const tf2Scalar &angle)
XmlRpc::XmlRpcValue::TypeArray
TypeArray
rm_chassis_controllers::ChassisBase::init
bool init(hardware_interface::RobotHW *robot_hw, ros::NodeHandle &root_nh, ros::NodeHandle &controller_nh) override
Get and check params for covariances. Setup odometry realtime publisher + odom message constant field...
Definition: chassis_base.cpp:83
rm_chassis_controllers::ChassisBase::outsideOdomCallback
void outsideOdomCallback(const nav_msgs::Odometry::ConstPtr &msg)
Definition: chassis_base.cpp:466
ros::Time
tf2::doTransform
void doTransform(const T &data_in, T &data_out, const geometry_msgs::TransformStamped &transform)
getParam
T getParam(ros::NodeHandle &pnh, const std::string &param_name, const T &default_val)
rm_chassis_controllers::ChassisBase::cmdChassisCallback
void cmdChassisCallback(const rm_msgs::ChassisCmdConstPtr &msg)
Write current command from rm_msgs::ChassisCmd.
Definition: chassis_base.cpp:451
length
TFSIMD_FORCE_INLINE tfScalar length(const Quaternion &q)
rm_chassis_controllers::ChassisBase::tfVelToBase
void tfVelToBase(const std::string &from)
Transform tf velocity to base link frame.
Definition: chassis_base.cpp:438
ROS_ERROR
#define ROS_ERROR(...)
ros_utilities.h
tf2::Quaternion
tf2_geometry_msgs.h
tf2::toMsg
B toMsg(const A &a)
quatToRPY
void quatToRPY(const geometry_msgs::Quaternion &q, double &roll, double &pitch, double &yaw)
rm_chassis_controllers::ChassisBase::cmdVelCallback
void cmdVelCallback(const geometry_msgs::Twist::ConstPtr &msg)
Write current command from geometry_msgs::Twist.
Definition: chassis_base.cpp:458
ros::NodeHandle::getNamespace
const std::string & getNamespace() const
DurationBase< Duration >::toSec
double toSec() const
tf2::TransformException
ROS_INFO
#define ROS_INFO(...)
rm_chassis_controllers::ChassisBase::powerLimit
void powerLimit()
To limit the chassis power according to current power limit.
Definition: chassis_base.cpp:410
ROS_ASSERT
#define ROS_ASSERT(cond)
ros::Duration
square
T square(T val)
XmlRpc::XmlRpcValue
ros::NodeHandle
angles.h
rm_chassis_controllers
Definition: balance.h:15
ori_tool.h
ros::Time::now
static Time now()


rm_chassis_controllers
Author(s): Qiayuan Liao
autogenerated on Sun May 4 2025 02:57:17