use crate::config;
use crate::io::subdev;
use crate::solver::{get_app_term, get_atom, get_u32};
use crate::util::mbus_list::MBUS_LIST;
use logru_arithmetic::logru;
use logru::ast::Term;
use logru::search::query_dfs;
use logru::textual;
pub use logru::textual::TextualUniverse;
use media_subsystem::{EntityId, EntityName, InterfaceId, LinkEnabled, MediaEntF, MediaIntfT, MediaLinkType, MediaLnkFl, MediaV2Entity, MediaV2Interface, PadRole};
use super::media;
use super::media::Topology;
use super::solver;
use std::fmt;
use std::ops::BitOr;
use tracing::debug;
use v4l::{FourCC, FrameSize};
use v4l::framesize;
use v4l::framesize::FrameSizeEnum;
use v4l2_subdev::MediaBusFmt;
pub const TOPOLOGYLIB: &'static str = "
% ===== Topology traversal procedures
next_entity(Eid, EidNext) :-
pad(Pid, Eid, source, _, _),
link(_, Pid, PidSink, _),
pad(PidSink, EidNext, sink, _, _).
last_interface(Eid, Iid) :-
interfacelink(_, Iid, Eid, _).
last_interface(Eid, Iid) :-
next_entity(Eid, EidNext), last_interface(EidNext, Iid).
% Finds all confgurations along the path. Configs is a list of alternating source-sink pads to connect together. The outermost element is the sensor source pad, the innermost is the video capture device, which is not a pad.
%
% Pad format:
% padcfg(
% EntityId,
% PadIdx, % index within the entity
% format(Mbus, Width, Height) % Mbus is an atom from the mbus code list, e.g. `mbus_Y8_1X8`.
% ).
%
% Output format:
% outcfg(EntityId, PadIdx, FourCC, W, H). % FourCC is a fourCC code as an atom, like `fourcc_BA10`.
%
% Video format shape:
% videofomat(FourCC, W, H).
%
% The application should traverse the list from outside to inside and connect pads while configuring with interleaved `VIDIOC_SUBDEV_S_FMT` and `MEDIA_IOC_SETUP_LINK` calls, finishing on the video device with `VIDIOC_S_FMT`.
% In theory, it doesn't matter which direction traversal takes.
% In practice, we don't have fast integer constraints, and devices requiring them are after the sensor, so traversal must be from the sensor to the end.
% Carrying VideoFormat is required becase if it's checked after the list is built, this slows dows the code to unbearable levels.
% Once that changes, it may make more sense to go in reverse and stop carrying the VideoFormat all the way through.
% Traversal forward might still be useful even then because that's how the kernel interface expects the pipeline configuration to be discovered.
% GIven an entity ID and the desired format on the video device, return a list of configs.
config_path(SensorEntityId, VideoFormat, Configs) :-
entity(Sensor, SensorEntityId, sensor),
config_path_by_name(Sensor, VideoFormat, Configs).
% GIven an entity name and the desired format on the video device, return a list of configs.
config_path_by_name(Sensor, VideoFormat, Configs) :-
source(Sensor, PadIdx, Mbus, W, H),
entity(Sensor, EntityId, sensor),
config_follow_source_pad(EntityId, PadIdx, format(Mbus, W, H), VideoFormat, Configs).
config_intermediate(PadId, Format, VideoFormat, PadConfigs) :-
pad(PadId, EntityId, sink, PadIdx, _),
entity(Name, EntityId, _),
processing(Name, PadIdx, Format, PadIdxOut, FormatOut),
config_follow_source_pad(EntityId, PadIdxOut, FormatOut, VideoFormat, ConfigsProduced),
append(ConfigsProduced, padcfg(EntityId, PadIdx, Format), PadConfigs).
config_follow_source_pad(EntityId, PadIdxOut, FormatOut, VideoFormat, ConfigsProduced) :-
pad(PadIdOut, EntityId, source, PadIdxOut, _),
link(_, PadIdOut, PadIdSink, _),
config_next(PadIdSink, FormatOut, VideoFormat, ConfigsOutput),
append(ConfigsOutput, padcfg(EntityId, PadIdxOut, FormatOut), ConfigsProduced).
% gathers configs all the way to the output
config_next(PadId, Format, VideoFormat, PadConfigs) :-
config_intermediate(PadId, Format, VideoFormat, PadConfigs).
config_next(PadId, Format, VideoFormat, l(Config, nil)) :-
eq(Config, outcfg(_, _, VideoFormat)),
config_output(PadId, Format, Config).
config_output(PadId, Format, outcfg(EntityId, PadIdx, videoformat(FourCC, W, H))) :-
pad(PadId, EntityId, sink, PadIdx, _),
entity(Name, EntityId, io),
output(Name, Format, FourCC),
eq(Format, format(_, W, H)).
% usage: config_path_by_name(dev_s5k3l6xx_3_002d, videoformat(Fourcc, W, H), A).
% Extracts the video device config from the config list generated in config_path_by_name.
videodevice_config(ConfigList, VideoConfig) :- last(ConfigList, VideoConfig).
";
pub fn sanitize_name(name: &EntityName) -> String {
match name {
EntityName::Bytes(_) => String::from("name_not_utf8"), EntityName::Text(name) => {
name.chars()
.map(|c| if c.is_alphanumeric() || c == '_' {
c
} else {
'_'
})
.collect()
},
}
}
pub fn fourcc_to_predicate(fourcc: FourCC) -> String {
format!("fourcc_{}", fourcc.to_string())
}
pub fn fourcc_from_predicate(pred: &str) -> Option<FourCC> {
if pred.starts_with("fourcc_") {
Some(FourCC::new(&pred["fourcc_".len()..].as_bytes().try_into().ok()?))
} else {
None
}
}
pub fn fourcc_from_term(query: &textual::UniverseQuery<'_>, term: &Term)
-> Option<FourCC>
{
match get_app_term(query, term)? {
(name, &[]) => fourcc_from_predicate(name),
_ => None,
}
}
pub fn mbus_to_predicate(mbus: MediaBusFmt) -> String {
let name = MBUS_LIST.iter()
.find(|(m, _)| *m == mbus)
.map(|(_, name)| name).expect("Mbus code not in the list!");
format!("mbus_{}", name)
}
pub fn mbus_from_predicate(pred: &str) -> Option<MediaBusFmt> {
if pred.starts_with("mbus_") {
let name = &pred["mbus_".len()..];
MBUS_LIST.iter()
.find(|(_, n)| *n == name)
.map(|(mbus, _)| *mbus)
} else {
None
}
}
pub fn mbus_from_term(query: &textual::UniverseQuery<'_>, term: &Term)
-> Option<MediaBusFmt>
{
match get_app_term(query, term)? {
(name, &[]) => mbus_from_predicate(name),
_ => None,
}
}
pub fn framesizes_as_facts(device: &EntityName, reports: Vec<FrameSize>) -> impl Iterator<Item=String> + use<'_> {
reports
.into_iter()
.map(|f| framesize_as_fact(device, f))
}
pub fn framesize_as_fact(device: &EntityName, FrameSize { fourcc, size }: FrameSize) -> String {
let device = sanitize_name(device);
match size {
FrameSizeEnum::Discrete(framesize::Discrete { width, height }) => format!(
"
output(dev_{device}, format(Mbus, {width}, {height}), FourCC) :-
eq(FourCC, fourcc_{fourcc}),
mbus_mapping(dev_{device}, Mbus, FourCC).
",
device=device,
width=width,
height=height,
fourcc=fourcc.to_string(),
),
FrameSizeEnum::Stepwise(
framesize::Stepwise {min_width, max_width, step_width, max_height, min_height, step_height }
) => format!(
"
output(dev_{device}, format(Mbus, W, H), FourCC) :-
eq(FourCC, fourcc_{fourcc}),
mbus_mapping(dev_{device}, Mbus, FourCC),
isInRange(W, {w_min}, {w_max}, {w_step}),
isInRange(H, {h_min}, {h_max}, {h_step}).
",
device=device,
fourcc=fourcc.to_string(),
w_step=step_width,
w_min=min_width,
w_max=max_width,
h_step=step_height,
h_min=min_height,
h_max=max_height,
),
}
}
pub fn mbus_all_guesses(device: &EntityName) -> String {
format!(
"% This is a video-node centric device. It does not expose a mbus mapping.
mbus_mapping(dev_{device}, mbus_FIXED, FourCC).",
device=sanitize_name(device),
)
}
pub fn topology_as_facts<E>(topology: &Topology, mut add_fact: impl FnMut(&str) -> Result<(), E>) -> Result<(), E> {
let topo = &topology.0;
topo.entities.iter()
.map(|e| {
let name = sanitize_name(&e.name);
let f = match e.function {
media_subsystem::MediaEntF::CamSensor => "sensor",
media_subsystem::MediaEntF::VidIfBridge => "bridge",
media_subsystem::MediaEntF::Flash => "flash",
media_subsystem::MediaEntF::IoV4L => "io",
media_subsystem::MediaEntF::Lens => "lens",
MediaEntF::ProcVideoPixelFormatter => "video_pixel_formatter",
_ => "other",
};
format!("entity(dev_{}, {}, {}).", name, e.id.0, f)
})
.map(|s| add_fact(&s))
.collect::<Result<(), E>>()?;
topo.pads.iter()
.map(|p| {
let r = match p.role {
media_subsystem::PadRole::Sink => "sink",
media_subsystem::PadRole::Source => "source",
};
format!("pad({}, {}, {}, {}, {}).", p.id.0, p.entity_id.0, r, p.index, p.must_connect)
})
.map(|s| add_fact(&s))
.collect::<Result<(), E>>()?;
topo.links.iter()
.map(|l| {
let en = match l.flags & MediaLnkFl::ENABLED == MediaLnkFl::ENABLED {
true => "enabled",
false => "disabled",
};
match l.connection {
MediaLinkType::Data { source, sink } => {
format!("link({}, {}, {}, {}).", l.id, source.0, sink.0, en)
},
MediaLinkType::Interface { interface, entity } => {
format!("interfacelink({}, {}, {}, {}).", l.id, interface.0, entity.0, en)
},
MediaLinkType::Ancillary { source, sink } => {
format!("ancillarylink({}, {}, {}, {}).", l.id, source.0, sink.0, en)
},
MediaLinkType::Other { source, sink, .. } => {
format!("%other link({}, {}, {}, {}).", l.id, source, sink, en)
},
}
})
.map(|s| add_fact(&s))
.collect::<Result<(), E>>()?;
Ok(())
}
use std::path::PathBuf;
pub trait Database {
type Error: fmt::Debug;
fn empty() -> Self;
fn add_facts(&mut self, facts: &str) -> Result<(), Self::Error>;
}
pub struct TopologyDatabase<'a, T: Database>{
topology: &'a Topology,
database: T,
}
impl<'a, T: Database> TopologyDatabase<'a, T> {
pub fn new(topology: &'a Topology) -> Self {
let mut universe = T::empty();
universe.add_facts(solver::STDLIB).unwrap();
universe.add_facts(TOPOLOGYLIB).unwrap();
universe.add_facts("% ===== Topology").unwrap();
topology_as_facts(topology, |s| universe.add_facts(s)).unwrap();
Self {
topology,
database: universe,
}
}
fn generate_entity_fact(&self, _io: &mut subdev::Io, e: &MediaV2Entity) -> String {
let name = sanitize_name(&e.name);
match (entity_interface(&self.topology, e.id), e.function) {
(None, MediaEntF::CamSensor) => {
let pad = self.topology.0.pads.iter()
.find(|p| p.entity_id == e.id && p.role == PadRole::Source);
if let Some(pad) = pad {
format!(
"
source(
dev_{},
% format is defined by the video device (?), because there's no interface on which to set it for this entity
{}, Mbus, Width, Height
).",
name,
pad.index,
)
} else {
String::new()
}
},
(None, MediaEntF::IoV4L) => String::new(),
(None, _) => {
let inpad = self.topology.0.pads.iter()
.find(|p| p.entity_id == e.id && p.role == PadRole::Sink);
let outpad = self.topology.0.pads.iter()
.find(|p| p.entity_id == e.id && p.role == PadRole::Source);
if let (Some(inpad), Some(outpad)) = (inpad, outpad) {
format!(
"
processing(
dev_{},
{}, Format,
{}, Format
).",
name,
inpad.index,
outpad.index,
)
} else {
String::new()
}
},
(Some(_iid), MediaEntF::CamSensor) => {
String::new()
},
(Some(_iid), MediaEntF::IoV4L) => String::new(),
_ => String::new(),
}
}
pub fn generate_subdev_facts<'b>(&'a self, io: &'b mut subdev::Io) -> impl Iterator<Item=String> + use<'a, 'b, T> {
self.topology.0.entities.iter()
.map(|e| self.generate_entity_fact(io, e))
}
pub fn add_subdev_info(&mut self, io: &mut subdev::Io) {
let facts = self.generate_subdev_facts(io)
.collect::<Vec<_>>();
for fact in facts {
self.database.add_facts(&fact).expect(&format!("{}", &fact));
}
}
pub fn add_facts(&mut self, facts: &str) -> Result<(), T::Error> {
self.database.add_facts(facts)
}
pub fn into_database(self) -> T {
self.database
}
}
impl Database for TextualUniverse {
type Error = textual::ParseError;
fn empty() -> Self {
Self::new()
}
fn add_facts(&mut self, facts: &str) -> Result<(), Self::Error> {
self.load_str(facts)
}
}
pub struct SaveDumpsWrapper<T: Database> {
database: T,
facts: Vec<String>,
}
impl<T: Database> Database for SaveDumpsWrapper<T> {
type Error = T::Error;
fn empty() -> Self {
Self {
database: T::empty(),
facts: Vec::new(),
}
}
fn add_facts(&mut self, facts: &str) -> Result<(), Self::Error> {
self.facts.push(facts.into());
self.database.add_facts(facts)
}
}
impl<T: Database> SaveDumpsWrapper<T> {
pub fn facts(&self) -> &[String] {
&self.facts
}
pub fn into_database(self) -> T {
self.database
}
}
pub fn entity_interface(topology: &media::Topology, e: EntityId) -> Option<InterfaceId> {
topology.0.links.iter()
.find_map(|l| match l.connection {
MediaLinkType::Interface { interface, entity } => {
if entity == e {
Some(interface)
} else {
None
}
},
_ => None,
})
}
pub fn interface_subdevice_paths<'a>(topology: &'a media::Topology, io: &mut media::Io) -> Vec<(&'a MediaV2Interface, PathBuf)> {
topology.0.interfaces.iter()
.filter(|i| i.intf_type == MediaIntfT::V4LSubdev)
.filter_map(|i| io.interface_find_path(i).ok().map(|p| (i, p)))
.collect()
}
pub fn outputs(universe: &TopologyDatabase<TextualUniverse>, entity: EntityId)
-> Vec<InterfaceId>
{
let q = universe.database.prepare_query(&format!(
"last_interface({}, Iid).",
entity.0,
)).unwrap();
let names = q.query().scope.as_ref().unwrap();
query_dfs(universe.database.resolver().clone(), &q.query())
.filter_map(|solution|
super::solver::solution_to_hash(names, solution)
.get("Iid")
.as_ref()
.and_then(|term| term.as_ref())
.and_then(|term| get_u32(term))
)
.map(|i| InterfaceId(i))
.collect()
}
fn contains_all<T: Copy + Eq + BitOr<T, Output=T>>(flags: T, needs_set: T) -> bool {
(flags | needs_set) == flags
}
pub fn video_capture_interfaces<'a>(
io: &mut media::Io,
database: &'a TopologyDatabase<TextualUniverse>,
entity: EntityId,
) -> Vec<&'a MediaV2Interface> {
outputs(database, entity).into_iter()
.filter_map(|interface_id|
database.topology.0.interfaces.iter().find(|interface| interface.id == interface_id)
)
.filter(|i| i.intf_type == MediaIntfT::V4LVideo)
.filter(|i| io.interface_find_path(i)
.and_then(v4l::Device::with_path)
.and_then(|dev| dev.query_caps())
.map(|caps| contains_all(caps.capabilities,
v4l::capability::Flags::VIDEO_CAPTURE | v4l::capability::Flags::STREAMING
) | contains_all(caps.capabilities,
v4l::capability::Flags::VIDEO_CAPTURE_MPLANE | v4l::capability::Flags::STREAMING
))
.unwrap_or_else(|e| {
debug!("Failed to check the type of video interface at {:?}: {:?}", i.devnode, e);
false
})
)
.collect()
}
pub fn entity_for_interface<'a>(
universe: &'a TopologyDatabase<TextualUniverse>,
interface: InterfaceId,
) -> Option<&'a MediaV2Entity> {
let q = universe.database.prepare_query(&format!(
"interfacelink(_, {}, Eid, _).",
interface.0,
)).unwrap();
let names = q.query().scope.as_ref().unwrap();
query_dfs(universe.database.resolver().clone(), &q.query())
.filter_map(|solution|
super::solver::solution_to_hash(names, solution)
.get("Eid")
.as_ref()
.and_then(|term| term.as_ref())
.and_then(|term| get_u32(term))
)
.map(EntityId)
.map(|id| universe.topology.0.entities.iter().find(|e| e.id == id).unwrap())
.next()
}
pub fn link_state(
universe: &TopologyDatabase<TextualUniverse>,
source: &config::PadState,
sink: &config::PadState,
) -> Option<LinkEnabled> {
let q = universe.database.prepare_query(&format!(
"link(_, PadIdSource, PadIdSink, State),
pad(PadIdSource, {source_entity}, source, {source_pad}, _),
pad(PadIdSink, {sink_entity}, sink, {sink_pad}, _).",
source_entity=source.id.0,
source_pad=source.pad_idx,
sink_entity=sink.id.0,
sink_pad=sink.pad_idx,
)).unwrap();
let names = q.query().scope.as_ref().unwrap();
query_dfs(universe.database.resolver().clone(), &q.query())
.filter_map(|solution|
super::solver::solution_to_hash(names, solution)
.get("State")
.as_ref()
.and_then(|term| term.as_ref())
.and_then(|term| get_atom(&q, term))
)
.filter_map(|atom| match atom {
"enabled" => Some(LinkEnabled::Enabled),
"disabled" => Some(LinkEnabled::Disabled),
_ => None,
})
.next()
}