phoxal 0.66.0

Phoxal - production-oriented autonomous robot framework: the one framework library, holding the runtime engine, the api contract tree, the typed bus, the canonical model, and the bundle.
Documentation
crate::endpoints! {
    frame: Sample<Frame>;
}

#[derive(
    phoxal_macros::DescribeWire,
    Copy,
    Eq,
    Clone,
    Debug,
    PartialEq,
    serde::Serialize,
    serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
pub enum Encoding {
    Jpeg,
    Png,
    L8,
    Rgb8,
    Rgba8,
}

#[derive(
    phoxal_macros::DescribeWire, Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
pub struct Intrinsics {
    pub fx: f32,
    pub fy: f32,
    pub cx: f32,
    pub cy: f32,
}

#[derive(
    phoxal_macros::DescribeWire, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
pub struct Distortion {
    pub model: String,
    pub coefficients: Vec<f32>,
}

#[derive(
    phoxal_macros::DescribeWire, Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
pub struct ExposureTiming {
    pub exposure_start_ns: Option<u64>,
    pub exposure_duration_ns: Option<u64>,
}

#[derive(
    phoxal_macros::DescribeWire, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
pub struct CalibrationIdentity {
    pub id: String,
    pub version: String,
}

#[derive(
    phoxal_macros::DescribeWire, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize,
)]
#[serde(try_from = "FrameWire")]
pub struct Frame {
    pub width: u32,
    pub height: u32,
    pub encoding: Encoding,
    pub intrinsics: Option<Intrinsics>,
    pub distortion: Option<Distortion>,
    pub exposure: Option<ExposureTiming>,
    pub calibration: Option<CalibrationIdentity>,
    #[serde(with = "serde_bytes")]
    pub data: Vec<u8>,
}

#[derive(serde::Deserialize)]
struct FrameWire {
    width: u32,
    height: u32,
    encoding: Encoding,
    intrinsics: Option<Intrinsics>,
    distortion: Option<Distortion>,
    exposure: Option<ExposureTiming>,
    calibration: Option<CalibrationIdentity>,
    #[serde(with = "serde_bytes")]
    data: Vec<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidFrame(&'static str);
impl std::fmt::Display for InvalidFrame {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.0)
    }
}
impl std::error::Error for InvalidFrame {}

impl Frame {
    #[expect(
        clippy::too_many_arguments,
        reason = "the validating constructor receives the complete wire frame atomically"
    )]
    pub fn try_new(
        width: u32,
        height: u32,
        encoding: Encoding,
        intrinsics: Option<Intrinsics>,
        distortion: Option<Distortion>,
        exposure: Option<ExposureTiming>,
        calibration: Option<CalibrationIdentity>,
        data: Vec<u8>,
    ) -> Result<Self, InvalidFrame> {
        if width == 0 || height == 0 {
            return Err(InvalidFrame("camera dimensions must be nonzero"));
        }
        let pixels = usize::try_from(width)
            .ok()
            .and_then(|w| usize::try_from(height).ok().and_then(|h| w.checked_mul(h)))
            .ok_or(InvalidFrame("camera dimensions overflow"))?;
        match encoding {
            Encoding::L8 | Encoding::Rgb8 | Encoding::Rgba8 => {
                let channels = match encoding {
                    Encoding::L8 => 1,
                    Encoding::Rgb8 => 3,
                    Encoding::Rgba8 => 4,
                    _ => unreachable!(),
                };
                if data.len()
                    != pixels
                        .checked_mul(channels)
                        .ok_or(InvalidFrame("camera image shape overflows"))?
                {
                    return Err(InvalidFrame(
                        "camera raw image length does not match dimensions",
                    ));
                }
            }
            Encoding::Jpeg | Encoding::Png if data.is_empty() => {
                return Err(InvalidFrame("camera encoded image must not be empty"));
            }
            Encoding::Jpeg | Encoding::Png => {}
        }
        if let Some(value) = intrinsics
            && !(value.fx.is_finite()
                && value.fy.is_finite()
                && value.fx > 0.0
                && value.fy > 0.0
                && value.cx.is_finite()
                && value.cy.is_finite())
        {
            return Err(InvalidFrame(
                "camera intrinsics must be finite with positive focal lengths",
            ));
        }
        if let Some(value) = &distortion
            && (value.model.trim().is_empty() || !value.coefficients.iter().all(|v| v.is_finite()))
        {
            return Err(InvalidFrame("camera distortion must be named and finite"));
        }
        if let Some(value) = &calibration
            && (value.id.trim().is_empty() || value.version.trim().is_empty())
        {
            return Err(InvalidFrame("camera calibration identity must be nonempty"));
        }
        Ok(Self {
            width,
            height,
            encoding,
            intrinsics,
            distortion,
            exposure,
            calibration,
            data,
        })
    }
}

impl TryFrom<FrameWire> for Frame {
    type Error = InvalidFrame;
    fn try_from(value: FrameWire) -> Result<Self, Self::Error> {
        Self::try_new(
            value.width,
            value.height,
            value.encoding,
            value.intrinsics,
            value.distortion,
            value.exposure,
            value.calibration,
            value.data,
        )
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn constructor_checks_raw_shape() {
        assert!(Frame::try_new(2, 2, Encoding::Rgb8, None, None, None, None, vec![0; 3]).is_err());
    }
}