1
0
forked from zbw/yiliao2026
Files
yiliao2026/scripts/base_feedback_monitor.py
2000-01-01 08:38:51 +08:00

299 lines
12 KiB
Python

#!/usr/bin/env python3
import csv
import math
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from nav_msgs.msg import Odometry
from origincar_msg.msg import Data
from sensor_msgs.msg import Imu
from std_msgs.msg import Float32
def quaternion_to_yaw(x: float, y: float, z: float, w: float) -> float:
siny_cosp = 2.0 * (w * z + x * y)
cosy_cosp = 1.0 - 2.0 * (y * y + z * z)
return math.atan2(siny_cosp, cosy_cosp)
class BaseFeedbackMonitor(Node):
def __init__(self) -> None:
super().__init__("base_feedback_monitor")
qos = QoSProfile(depth=10)
self.workspace_root = Path.cwd()
self.log_dir = self.workspace_root / "feedback_logs" / datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_dir.mkdir(parents=True, exist_ok=True)
self.csv_path = self.log_dir / "feedback.csv"
self.csv_file = self.csv_path.open("w", newline="", encoding="utf-8")
self.csv_writer = csv.writer(self.csv_file)
self.csv_writer.writerow([
"wall_time",
"elapsed_s",
"imu_ready",
"odom_ready",
"voltage_ready",
"robotpose_ready",
"robotvel_ready",
"gyrodebug_ready",
"voltage_v",
"imu_orientation_x",
"imu_orientation_y",
"imu_orientation_z",
"imu_orientation_w",
"imu_yaw_rad",
"imu_angular_velocity_x",
"imu_angular_velocity_y",
"imu_angular_velocity_z",
"imu_linear_acceleration_x",
"imu_linear_acceleration_y",
"imu_linear_acceleration_z",
"odom_pose_x",
"odom_pose_y",
"odom_yaw_rad",
"odom_twist_vx",
"odom_twist_vy",
"odom_twist_wz",
"robotpose_x",
"robotpose_y",
"robotpose_z",
"robotvel_x",
"robotvel_y",
"robotvel_z",
"gyro_z_filtered_pre_bias",
"gyro_z_bias_model",
"gyro_z_final_for_yaw",
])
self.start_time = self.get_clock().now()
self.imu_msg: Optional[Imu] = None
self.odom_msg: Optional[Odometry] = None
self.voltage_msg: Optional[Float32] = None
self.robotpose_msg: Optional[Data] = None
self.robotvel_msg: Optional[Data] = None
self.gyrodebug_msg: Optional[Data] = None
self.create_subscription(Imu, "imu/data_raw", self.imu_callback, qos)
self.create_subscription(Odometry, "odom", self.odom_callback, qos)
self.create_subscription(Float32, "PowerVoltage", self.voltage_callback, qos)
self.create_subscription(Data, "robotpose", self.robotpose_callback, qos)
self.create_subscription(Data, "robotvel", self.robotvel_callback, qos)
self.create_subscription(Data, "gyro_debug", self.gyrodebug_callback, qos)
self.create_timer(0.2, self.render)
self.create_timer(0.2, self.log_snapshot)
self.get_logger().info(f"Logging feedback to {self.csv_path}")
def imu_callback(self, msg: Imu) -> None:
self.imu_msg = msg
def odom_callback(self, msg: Odometry) -> None:
self.odom_msg = msg
def voltage_callback(self, msg: Float32) -> None:
self.voltage_msg = msg
def robotpose_callback(self, msg: Data) -> None:
self.robotpose_msg = msg
def robotvel_callback(self, msg: Data) -> None:
self.robotvel_msg = msg
def gyrodebug_callback(self, msg: Data) -> None:
self.gyrodebug_msg = msg
def _fmt_float(self, value: Optional[float], digits: int = 4) -> str:
if value is None:
return "N/A"
return f"{value:.{digits}f}"
def _topic_state(self, msg: object) -> str:
return "OK" if msg is not None else "WAIT"
def _get_snapshot(self) -> dict:
imu_yaw = None
odom_yaw = None
if self.imu_msg is not None:
imu_yaw = quaternion_to_yaw(
self.imu_msg.orientation.x,
self.imu_msg.orientation.y,
self.imu_msg.orientation.z,
self.imu_msg.orientation.w,
)
if self.odom_msg is not None:
odom_yaw = quaternion_to_yaw(
self.odom_msg.pose.pose.orientation.x,
self.odom_msg.pose.pose.orientation.y,
self.odom_msg.pose.pose.orientation.z,
self.odom_msg.pose.pose.orientation.w,
)
return {
"wall_time": datetime.now().isoformat(timespec="milliseconds"),
"elapsed_s": (self.get_clock().now() - self.start_time).nanoseconds / 1e9,
"imu_ready": int(self.imu_msg is not None),
"odom_ready": int(self.odom_msg is not None),
"voltage_ready": int(self.voltage_msg is not None),
"robotpose_ready": int(self.robotpose_msg is not None),
"robotvel_ready": int(self.robotvel_msg is not None),
"gyrodebug_ready": int(self.gyrodebug_msg is not None),
"voltage_v": self.voltage_msg.data if self.voltage_msg is not None else None,
"imu_orientation_x": self.imu_msg.orientation.x if self.imu_msg is not None else None,
"imu_orientation_y": self.imu_msg.orientation.y if self.imu_msg is not None else None,
"imu_orientation_z": self.imu_msg.orientation.z if self.imu_msg is not None else None,
"imu_orientation_w": self.imu_msg.orientation.w if self.imu_msg is not None else None,
"imu_yaw_rad": imu_yaw,
"imu_angular_velocity_x": self.imu_msg.angular_velocity.x if self.imu_msg is not None else None,
"imu_angular_velocity_y": self.imu_msg.angular_velocity.y if self.imu_msg is not None else None,
"imu_angular_velocity_z": self.imu_msg.angular_velocity.z if self.imu_msg is not None else None,
"imu_linear_acceleration_x": self.imu_msg.linear_acceleration.x if self.imu_msg is not None else None,
"imu_linear_acceleration_y": self.imu_msg.linear_acceleration.y if self.imu_msg is not None else None,
"imu_linear_acceleration_z": self.imu_msg.linear_acceleration.z if self.imu_msg is not None else None,
"odom_pose_x": self.odom_msg.pose.pose.position.x if self.odom_msg is not None else None,
"odom_pose_y": self.odom_msg.pose.pose.position.y if self.odom_msg is not None else None,
"odom_yaw_rad": odom_yaw,
"odom_twist_vx": self.odom_msg.twist.twist.linear.x if self.odom_msg is not None else None,
"odom_twist_vy": self.odom_msg.twist.twist.linear.y if self.odom_msg is not None else None,
"odom_twist_wz": self.odom_msg.twist.twist.angular.z if self.odom_msg is not None else None,
"robotpose_x": self.robotpose_msg.x if self.robotpose_msg is not None else None,
"robotpose_y": self.robotpose_msg.y if self.robotpose_msg is not None else None,
"robotpose_z": self.robotpose_msg.z if self.robotpose_msg is not None else None,
"robotvel_x": self.robotvel_msg.x if self.robotvel_msg is not None else None,
"robotvel_y": self.robotvel_msg.y if self.robotvel_msg is not None else None,
"robotvel_z": self.robotvel_msg.z if self.robotvel_msg is not None else None,
"gyro_z_filtered_pre_bias": self.gyrodebug_msg.x if self.gyrodebug_msg is not None else None,
"gyro_z_bias_model": self.gyrodebug_msg.y if self.gyrodebug_msg is not None else None,
"gyro_z_final_for_yaw": self.gyrodebug_msg.z if self.gyrodebug_msg is not None else None,
}
def log_snapshot(self) -> None:
snapshot = self._get_snapshot()
self.csv_writer.writerow(snapshot.values())
self.csv_file.flush()
def render(self) -> None:
snapshot = self._get_snapshot()
sys.stdout.write("\033[2J\033[H")
sys.stdout.write("origincar base feedback monitor\n")
sys.stdout.write("=" * 40 + "\n")
sys.stdout.write(
f"imu/data_raw: {self._topic_state(self.imu_msg)} | "
f"odom: {self._topic_state(self.odom_msg)} | "
f"PowerVoltage: {self._topic_state(self.voltage_msg)} | "
f"robotpose: {self._topic_state(self.robotpose_msg)} | "
f"robotvel: {self._topic_state(self.robotvel_msg)} | "
f"gyro_debug: {self._topic_state(self.gyrodebug_msg)}\n\n"
)
sys.stdout.write(f"log file: {self.csv_path}\n\n")
if self.voltage_msg is not None:
sys.stdout.write(f"voltage: {self._fmt_float(self.voltage_msg.data, 3)} V\n")
else:
sys.stdout.write("voltage: N/A\n")
if self.imu_msg is not None:
imu = self.imu_msg
sys.stdout.write(
"imu orientation: "
f"x={self._fmt_float(imu.orientation.x)} "
f"y={self._fmt_float(imu.orientation.y)} "
f"z={self._fmt_float(imu.orientation.z)} "
f"w={self._fmt_float(imu.orientation.w)} "
f"yaw={self._fmt_float(snapshot['imu_yaw_rad'])} rad\n"
)
sys.stdout.write(
"imu angular vel: "
f"x={self._fmt_float(imu.angular_velocity.x)} "
f"y={self._fmt_float(imu.angular_velocity.y)} "
f"z={self._fmt_float(imu.angular_velocity.z)} rad/s\n"
)
sys.stdout.write(
"imu linear acc: "
f"x={self._fmt_float(imu.linear_acceleration.x)} "
f"y={self._fmt_float(imu.linear_acceleration.y)} "
f"z={self._fmt_float(imu.linear_acceleration.z)} m/s^2\n"
)
else:
sys.stdout.write("imu orientation: N/A\n")
sys.stdout.write("imu angular vel: N/A\n")
sys.stdout.write("imu linear acc: N/A\n")
if self.odom_msg is not None:
odom = self.odom_msg
sys.stdout.write(
"odom pose: "
f"x={self._fmt_float(odom.pose.pose.position.x)} "
f"y={self._fmt_float(odom.pose.pose.position.y)} "
f"yaw={self._fmt_float(snapshot['odom_yaw_rad'])}\n"
)
sys.stdout.write(
"odom twist: "
f"vx={self._fmt_float(odom.twist.twist.linear.x)} "
f"vy={self._fmt_float(odom.twist.twist.linear.y)} "
f"wz={self._fmt_float(odom.twist.twist.angular.z)}\n"
)
else:
sys.stdout.write("odom pose: N/A\n")
sys.stdout.write("odom twist: N/A\n")
if self.robotpose_msg is not None:
pose = self.robotpose_msg
sys.stdout.write(
"robotpose: "
f"x={self._fmt_float(pose.x)} "
f"y={self._fmt_float(pose.y)} "
f"z={self._fmt_float(pose.z)}\n"
)
else:
sys.stdout.write("robotpose: N/A\n")
if self.robotvel_msg is not None:
vel = self.robotvel_msg
sys.stdout.write(
"robotvel: "
f"x={self._fmt_float(vel.x)} "
f"y={self._fmt_float(vel.y)} "
f"z={self._fmt_float(vel.z)}\n"
)
else:
sys.stdout.write("robotvel: N/A\n")
if self.gyrodebug_msg is not None:
gyro = self.gyrodebug_msg
sys.stdout.write(
"gyro debug: "
f"pre_bias={self._fmt_float(gyro.x, 6)} "
f"bias_z={self._fmt_float(gyro.y, 6)} "
f"final_z={self._fmt_float(gyro.z, 6)} rad/s\n"
)
else:
sys.stdout.write("gyro debug: N/A\n")
sys.stdout.write("\nCtrl+C to exit\n")
sys.stdout.flush()
def main(args=None) -> None:
rclpy.init(args=args)
node = BaseFeedbackMonitor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.csv_file.close()
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()