pub mod parser;
use crate::parser::{RefTime, generate_default_elevation_calibration};
use bincode::de::Decoder;
use bincode::enc::Encoder;
use bincode::error::{DecodeError, EncodeError};
use bincode::{Decode, Encode};
use chrono::Utc;
use cu_sensor_payloads::{PointCloud, PointCloudSoa};
use cu29::prelude::*;
use cu29::units::si::angle::radian;
use cu29::units::si::f32::{Angle, Length};
use cu29::units::si::length::meter;
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
use std::io::ErrorKind;
use std::io::Read;
use std::net::SocketAddr;
use std::ops::{Deref, DerefMut};
const DEFAULT_ADDR: &str = "0.0.0.0:2368";
fn spherical_to_cartesian(
azimuth: Angle,
elevation: Angle,
distance: Length,
) -> (Length, Length, Length) {
let distance_m = distance.get::<meter>();
let azimuth_rad = azimuth.get::<radian>();
let elevation_rad = elevation.get::<radian>();
let x = Length::new::<meter>(distance_m * elevation_rad.cos() * azimuth_rad.cos());
let y = Length::new::<meter>(distance_m * elevation_rad.cos() * azimuth_rad.sin());
let z = Length::new::<meter>(distance_m * elevation_rad.sin());
(x, y, z)
}
#[derive(Reflect)]
#[reflect(from_reflect = false)]
pub struct Xt32 {
#[reflect(ignore)]
socket: Socket,
#[reflect(ignore)]
reftime: RefTime,
#[reflect(ignore)]
channel_elevations: [Angle; 32],
}
impl Xt32 {
fn sync(&mut self, robot_clock: &RobotClock) {
self.reftime = (Utc::now(), robot_clock.now());
}
}
impl Freezable for Xt32 {}
const MAX_POINTS: usize = 32 * 10;
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize, Reflect)]
#[reflect(opaque, from_reflect = false)]
pub struct LidarCuMsgPayload(pub PointCloudSoa<MAX_POINTS>);
impl Deref for LidarCuMsgPayload {
type Target = PointCloudSoa<MAX_POINTS>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for LidarCuMsgPayload {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Encode for LidarCuMsgPayload {
fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
self.0.encode(encoder)
}
}
impl Decode<()> for LidarCuMsgPayload {
fn decode<D: Decoder<Context = ()>>(decoder: &mut D) -> Result<Self, DecodeError> {
Ok(Self(PointCloudSoa::<MAX_POINTS>::decode(decoder)?))
}
}
fn channel_time(t6: CuTime, i: u64) -> CuTime {
if i == 0 {
CuDuration(t6.0 - 1512 + 280) } else {
CuDuration(t6.0 + 1512 * (i - 1) + 280)
}
}
impl CuSrcTask for Xt32 {
type Resources<'r> = ();
type Output<'m> = output_msg!(LidarCuMsgPayload);
fn new(config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
where
Self: Sized,
{
let addr: SocketAddr = if let Some(cfg) = config {
let addr_str = cfg
.get::<String>("socket_addr")?
.unwrap_or(DEFAULT_ADDR.to_string());
addr_str.as_str().parse().unwrap()
} else {
DEFAULT_ADDR.parse().unwrap()
};
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap();
socket.bind(&SockAddr::from(addr)).unwrap();
socket.set_nonblocking(true).unwrap();
let rt: RefTime = (Utc::now(), RobotClock::new().now());
Ok(Xt32 {
socket,
reftime: rt,
channel_elevations: generate_default_elevation_calibration(), })
}
fn start(&mut self, ctx: &CuContext) -> CuResult<()> {
self.sync(ctx);
Ok(())
}
fn process(&mut self, _ctx: &CuContext, new_msg: &mut Self::Output<'_>) -> CuResult<()> {
let payload = new_msg.payload_mut().insert(LidarCuMsgPayload::default());
let mut buf = [0u8; 1500];
let size = match self.socket.read(&mut buf) {
Ok(size) => size,
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
new_msg.clear_payload();
return Ok(());
}
Err(e) => return Err(CuError::new_with_cause("IO Error on UDP socket", e)), };
let lidar_packet = parser::parse_packet(&buf[..size])
.map_err(|e| CuError::new_with_cause("Failed to parse Hesai UDP packet", e))?;
let t6 = lidar_packet
.block_ts(&self.reftime)
.map_err(|e| CuError::new_with_cause("Failed to get block timings", e))?[5];
let mut min_tov = CuTime::MAX;
let mut max_tov = CuTime::MIN;
for block in lidar_packet.blocks.iter() {
let azimuth = block.azimuth();
block.channels.iter().enumerate().for_each(|(i, c)| {
let elevation = self.channel_elevations[i];
let d = c.distance();
let r = c.reflectivity();
let (x, y, z) = spherical_to_cartesian(azimuth, elevation, d);
let t = channel_time(t6, i as u64);
if t < min_tov {
min_tov = t;
} else if t > max_tov {
max_tov = t;
}
payload.push(PointCloud::new_units(t, x, y, z, r, None));
});
}
let tov_range = CuTimeRange {
start: min_tov,
end: max_tov,
};
new_msg.tov = Tov::Range(tov_range); Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::Packet;
use chrono::DateTime;
use cu_udp_inject::PcapStreamer;
use cu29::cutask::CuMsg;
#[test]
fn test_xt32() {
let ctx = CuContext::new_with_clock();
let mut streamer = PcapStreamer::new("tests/hesai-xt32-small.pcap", "127.0.0.1:2368");
let config = ComponentConfig::new();
let mut xt32 = Xt32::new(Some(&config), ()).unwrap();
let new_payload = LidarCuMsgPayload::default();
let mut new_msg = CuMsg::<LidarCuMsgPayload>::new(Some(new_payload));
let datetime = DateTime::parse_from_rfc3339("2024-09-17T15:47:11.684855Z")
.unwrap()
.with_timezone(&Utc);
xt32.reftime = (datetime, ctx.now());
const PACKET_SIZE: usize = size_of::<Packet>();
while streamer
.send_next::<PACKET_SIZE>()
.expect("Failed to send next packet")
{
let err = xt32.process(&ctx, &mut new_msg);
if let Err(e) = err {
println!("Error: {e:?}");
continue;
}
if let Some(payload) = new_msg.payload() {
println!("Lidar Payload: {payload:?}");
}
break;
}
}
}