1pub mod parser;
2
3use crate::parser::{generate_default_elevation_calibration, RefTime};
4use chrono::Utc;
5use cu29::prelude::*;
6use cu_sensor_payloads::{PointCloud, PointCloudSoa};
7use socket2::{Domain, Protocol, SockAddr, Socket, Type};
8use std::io::ErrorKind;
9use std::io::Read;
10use std::net::SocketAddr;
11use uom::si::f32::{Angle, Length};
12
13const DEFAULT_ADDR: &str = "0.0.0.0:2368";
15
16fn spherical_to_cartesian(
19 azimuth: Angle,
20 elevation: Angle,
21 distance: Length,
22) -> (Length, Length, Length) {
23 let x = distance * elevation.cos() * azimuth.cos();
24 let y = distance * elevation.cos() * azimuth.sin();
25 let z = distance * elevation.sin();
26 (x, y, z)
27}
28
29pub struct Xt32 {
30 socket: Socket,
31 reftime: RefTime,
32 channel_elevations: [Angle; 32],
33}
34
35impl Xt32 {
36 fn sync(&mut self, robot_clock: &RobotClock) {
38 self.reftime = (Utc::now(), robot_clock.now());
39 }
40}
41
42impl Freezable for Xt32 {}
43
44const MAX_POINTS: usize = 32 * 10;
45
46pub type LidarCuMsgPayload = PointCloudSoa<MAX_POINTS>;
47
48fn channel_time(t6: CuTime, i: u64) -> CuTime {
54 if i == 0 {
55 CuDuration(t6.0 - 1512 + 280) } else {
57 CuDuration(t6.0 + 1512 * (i - 1) + 280)
58 }
59}
60
61impl CuSrcTask for Xt32 {
62 type Output<'m> = output_msg!(LidarCuMsgPayload);
63
64 fn new(config: Option<&ComponentConfig>) -> CuResult<Self>
65 where
66 Self: Sized,
67 {
68 let addr: SocketAddr = if let Some(cfg) = config {
69 let addr_str = cfg.get("socket_addr").unwrap_or(DEFAULT_ADDR.to_string());
70 addr_str.as_str().parse().unwrap()
71 } else {
72 DEFAULT_ADDR.parse().unwrap()
73 };
74
75 let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap();
76 socket.bind(&SockAddr::from(addr)).unwrap();
77 socket.set_nonblocking(true).unwrap();
78
79 let rt: RefTime = (Utc::now(), RobotClock::new().now());
81 Ok(Xt32 {
82 socket,
83 reftime: rt,
84 channel_elevations: generate_default_elevation_calibration(), })
86 }
87 fn start(&mut self, robot_clock: &RobotClock) -> CuResult<()> {
88 self.sync(robot_clock);
89 Ok(())
90 }
91 fn process(&mut self, _clock: &RobotClock, new_msg: &mut Self::Output<'_>) -> CuResult<()> {
92 let payload = new_msg.payload_mut().insert(LidarCuMsgPayload::default());
93 let mut buf = [0u8; 1500];
94 match self.socket.read(&mut buf) {
95 Ok(size) => {
96 let lidar_packet = parser::parse_packet(&buf[..size])
97 .map_err(|e| CuError::new_with_cause("Failed to parse Hesai UDP packet", e))?;
98 let t6 = lidar_packet
100 .block_ts(&self.reftime)
101 .map_err(|e| CuError::new_with_cause("Failed to get block timings", e))?[5]; let mut min_tov = CuTime::MAX;
104 let mut max_tov = CuTime::MIN;
105
106 for block in lidar_packet.blocks.iter() {
108 let azimuth = block.azimuth();
109 block.channels.iter().enumerate().for_each(|(i, c)| {
110 let elevation = self.channel_elevations[i];
111 let d = c.distance();
112 let r = c.reflectivity();
113
114 let (x, y, z) = spherical_to_cartesian(azimuth, elevation, d);
115 let t = channel_time(t6, i as u64);
116 if t < min_tov {
118 min_tov = t;
119 } else if t > max_tov {
120 max_tov = t;
121 }
122 payload.push(PointCloud::new_uom(t, x, y, z, r, None));
123 });
124 }
125
126 let tov_range = CuTimeRange {
127 start: min_tov,
128 end: max_tov,
129 };
130 new_msg.tov = Tov::Range(tov_range); }
132 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
133 new_msg.clear_payload();
135 return Ok(());
136 }
137 Err(e) => return Err(CuError::new_with_cause("IO Error on UDP socket", e)), }
139 Ok(())
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::parser::Packet;
147 use chrono::DateTime;
148 use cu29::cutask::CuMsg;
149 use cu_udp_inject::PcapStreamer;
150
151 #[test]
152 fn test_xt32() {
153 let clock = RobotClock::new();
154 let mut streamer = PcapStreamer::new("tests/hesai-xt32-small.pcap", "127.0.0.1:2368");
155 let config = ComponentConfig::new();
156
157 let mut xt32 = Xt32::new(Some(&config)).unwrap();
158
159 let new_payload = LidarCuMsgPayload::default();
160 let mut new_msg = CuMsg::<LidarCuMsgPayload>::new(Some(new_payload));
161
162 let datetime = DateTime::parse_from_rfc3339("2024-09-17T15:47:11.684855Z")
164 .unwrap()
165 .with_timezone(&Utc);
166
167 xt32.reftime = (datetime, clock.now());
168
169 const PACKET_SIZE: usize = size_of::<Packet>();
171 while streamer
172 .send_next::<PACKET_SIZE>()
173 .expect("Failed to send next packet")
174 {
175 let err = xt32.process(&clock, &mut new_msg);
176 if let Err(e) = err {
177 println!("Error: {e:?}");
178 continue;
179 }
180 if let Some(payload) = new_msg.payload() {
181 println!("Lidar Payload: {payload:?}");
182 }
183 break;
184 }
185 }
186}