use std::{
collections::HashSet,
ffi::{OsStr, OsString},
fs,
io::{Error, ErrorKind, Result},
path::{Path, PathBuf},
};
use super::{
util::{FunctionDir, Status},
Function, Handle,
};
pub(crate) fn driver() -> &'static OsStr {
OsStr::new("uvc")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Format {
Yuyv,
Mjpeg,
}
impl Format {
fn all() -> &'static [Format] {
&[Format::Yuyv, Format::Mjpeg]
}
fn dir_name(&self) -> &'static OsStr {
match self {
Format::Yuyv => OsStr::new("yuyv"),
Format::Mjpeg => OsStr::new("mjpeg"),
}
}
fn group_dir_name(&self) -> &'static OsStr {
match self {
Format::Yuyv => OsStr::new("uncompressed"),
_ => self.dir_name(),
}
}
fn group_path(&self) -> PathBuf {
format!("streaming/{}/{}", self.group_dir_name().to_string_lossy(), self.dir_name().to_string_lossy())
.into()
}
fn header_link_path(&self) -> PathBuf {
format!("streaming/header/h/{}", self.dir_name().to_string_lossy()).into()
}
fn color_matching_path(&self) -> PathBuf {
format!("streaming/color_matching/{}", self.dir_name().to_string_lossy()).into()
}
fn color_matching_link_path(&self) -> PathBuf {
self.group_path().join("color_matching")
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ColorMatching {
pub color_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
}
impl ColorMatching {
pub fn new(color_primaries: u8, transfer_characteristics: u8, matrix_coefficients: u8) -> Self {
Self { color_primaries, transfer_characteristics, matrix_coefficients }
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub width: u32,
pub height: u32,
pub format: Format,
pub fps: Vec<u16>,
}
impl Frame {
pub fn new(width: u32, height: u32, fps: Vec<u16>, format: Format) -> Self {
Self { width, height, format, fps }
}
}
impl From<Frame> for UvcFrame {
fn from(frame: Frame) -> Self {
UvcFrame {
width: frame.width,
height: frame.height,
intervals: frame.fps.iter().filter(|i| **i != 0).map(|i| 1_000_000_000 / *i as u32).collect(),
color_matching: None,
format: frame.format,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UvcFrame {
pub width: u32,
pub height: u32,
pub intervals: Vec<u32>,
pub color_matching: Option<ColorMatching>,
pub format: Format,
}
impl UvcFrame {
fn dir_name(&self) -> String {
format!("{}p", self.height)
}
fn path(&self) -> PathBuf {
self.format.group_path().join(self.dir_name())
}
pub fn new(width: u32, height: u32, format: Format, intervals: impl IntoIterator<Item = u32>) -> Self {
Self { width, height, intervals: intervals.into_iter().collect(), color_matching: None, format }
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct UvcBuilder {
pub streaming_interval: Option<u8>,
pub streaming_max_burst: Option<u8>,
pub streaming_max_packet: Option<u32>,
pub function_name: Option<String>,
pub frames: Vec<UvcFrame>,
pub processing_controls: Option<u8>,
pub camera_controls: Option<u8>,
}
impl UvcBuilder {
pub fn build(self) -> (Uvc, Handle) {
let dir = FunctionDir::new();
(Uvc { dir: dir.clone() }, Handle::new(UvcFunction { builder: self, dir }))
}
pub fn add_frame<F>(&mut self, frame: F)
where
UvcFrame: From<F>,
{
self.frames.push(frame.into());
}
#[must_use]
pub fn with_frames<F>(mut self, frames: impl IntoIterator<Item = F>) -> Self
where
UvcFrame: From<F>,
{
self.frames = frames.into_iter().map(UvcFrame::from).collect();
self
}
}
#[derive(Debug)]
struct UvcFunction {
builder: UvcBuilder,
dir: FunctionDir,
}
impl Function for UvcFunction {
fn driver(&self) -> OsString {
driver().into()
}
fn dir(&self) -> FunctionDir {
self.dir.clone()
}
fn register(&self) -> Result<()> {
if self.builder.frames.is_empty() {
return Err(Error::new(ErrorKind::InvalidInput, "at least one frame must exist"));
}
if self.builder.frames.iter().any(|f| f.intervals.is_empty()) {
return Err(Error::new(ErrorKind::InvalidInput, "at least one interval must exist for every frame"));
}
let mut formats_to_link: HashSet<Format> = HashSet::new();
for frame in &self.builder.frames {
self.dir.create_dir_all(frame.path())?;
self.dir.write(frame.path().join("wWidth"), frame.width.to_string())?;
self.dir.write(frame.path().join("wHeight"), frame.height.to_string())?;
self.dir.write(
frame.path().join("dwMaxVideoFrameBufferSize"),
(frame.width * frame.height * 2).to_string(),
)?;
self.dir.write(
frame.path().join("dwFrameInterval"),
frame.intervals.iter().map(|i| i.to_string()).collect::<Vec<String>>().join("\n"),
)?;
formats_to_link.insert(frame.format);
if let Some(color_matching) = frame.color_matching.as_ref() {
let color_matching_path = frame.format.color_matching_path();
if !color_matching_path.is_dir() {
self.dir.create_dir_all(&color_matching_path)?;
self.dir.write(
frame.format.color_matching_path().join("bColorPrimaries"),
color_matching.color_primaries.to_string(),
)?;
self.dir.write(
frame.format.color_matching_path().join("bTransferCharacteristics"),
color_matching.transfer_characteristics.to_string(),
)?;
self.dir.write(
frame.format.color_matching_path().join("bMatrixCoefficients"),
color_matching.matrix_coefficients.to_string(),
)?;
self.dir.symlink(&color_matching_path, frame.format.color_matching_link_path())?;
} else {
log::warn!("Color matching information already exists for format {:?}", frame.format);
}
}
}
self.dir.create_dir_all("streaming/header/h")?;
self.dir.create_dir_all("control/header/h")?;
for format in formats_to_link {
self.dir.symlink(format.group_path(), format.header_link_path())?;
}
self.dir.symlink("streaming/header/h", "streaming/class/fs/h")?;
self.dir.symlink("streaming/header/h", "streaming/class/hs/h")?;
self.dir.symlink("streaming/header/h", "streaming/class/ss/h")?;
self.dir.symlink("control/header/h", "control/class/fs/h")?;
self.dir.symlink("control/header/h", "control/class/ss/h")?;
if let Some(processing_controls) = self.builder.processing_controls {
self.dir.write("control/processing/default/bmControls", processing_controls.to_string())?;
}
if let Some(camera_controls) = self.builder.camera_controls {
self.dir.write("control/terminal/camera/default/bmControls", camera_controls.to_string())?;
}
if let Some(interval) = self.builder.streaming_interval {
self.dir.write("streaming_interval", interval.to_string())?;
}
if let Some(max_burst) = self.builder.streaming_max_burst {
self.dir.write("streaming_maxburst", max_burst.to_string())?;
}
if let Some(max_packet) = self.builder.streaming_max_packet {
self.dir.write("streaming_maxpacket", max_packet.to_string())?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct Uvc {
dir: FunctionDir,
}
impl Uvc {
pub fn builder() -> UvcBuilder {
UvcBuilder::default()
}
pub fn new<F>(frames: impl IntoIterator<Item = F>) -> (Uvc, Handle)
where
UvcFrame: From<F>,
{
let frames = frames.into_iter().map(UvcFrame::from).collect();
let builder = UvcBuilder { frames, ..Default::default() };
builder.build()
}
pub fn status(&self) -> Status {
self.dir.status()
}
}
fn remove_class_headers<P: AsRef<Path>>(path: P) -> Result<()> {
for entry in fs::read_dir(path)? {
let Ok(entry) = entry else { continue };
let path = entry.path();
let header_path = path.join("h");
if header_path.is_symlink() {
log::trace!("removing UVC header {:?}", path);
fs::remove_file(header_path)?;
}
}
Ok(())
}
pub(crate) fn remove_handler(dir: PathBuf) -> Result<()> {
let ctrl_class = dir.join("control/class");
if ctrl_class.is_dir() {
remove_class_headers(ctrl_class)?;
}
let stream_class = dir.join("streaming/class");
if stream_class.is_dir() {
remove_class_headers(stream_class)?;
}
if dir.join("streaming").is_dir() {
for format in Format::all() {
let header_link_path = dir.join(format.header_link_path());
if header_link_path.is_symlink() {
log::trace!("removing UVC header link {:?}", header_link_path);
fs::remove_file(header_link_path)?;
}
let color_matching_dir = dir.join(format.color_matching_path());
if color_matching_dir.is_dir() {
log::trace!("removing UVC color matching information {:?}", color_matching_dir);
fs::remove_file(dir.join(format.color_matching_link_path()))?;
fs::remove_dir(color_matching_dir)?;
}
let group_dir = dir.join(format.group_path());
if group_dir.is_dir() {
for entry in fs::read_dir(&group_dir)? {
let Ok(entry) = entry else { continue };
let path = entry.path();
if path.is_dir() && !path.is_symlink() {
log::trace!("removing UVC frame {:?}", path);
fs::remove_dir(path)?;
}
}
log::trace!("removing UVC group {:?}", group_dir);
fs::remove_dir(group_dir)?;
}
}
}
let stream_header = dir.join("streaming/header/h");
if stream_header.is_dir() {
fs::remove_dir(stream_header)?;
}
let control_header = dir.join("control/header/h");
if control_header.is_dir() {
fs::remove_dir(control_header)?;
}
Ok(())
}