use std::fmt;
use std::net::Ipv4Addr;
use crate::event::Event;
use crate::state::{Bay, Device, State};
use crate::types::{
AmpZoneSettings, HiddenStatus, MultiviewerStatus, PowerStatus, V2ipAudioFormat, V2ipRoute,
V2ipRouteTarget, V2ipStreamSources, VideoWallOp, VideoWallWindow, VolumeMuteStatus,
MULTIVIEWER_INPUTS, VIDEO_WALL_CLEARED,
};
use crate::wire::{
audio_cmd_header, audio_param, audio_sub, build_amp_zone_settings, build_audio_select_input,
build_bay_hide, build_edid_profile, build_edid_request, build_rc_action, build_rc_key,
build_set_bay_name, build_set_volume, build_stats_request, build_target_only,
build_v2ip_manual_source_switch, build_v2ip_source_switch, build_video_wall, mv_cmd_payload,
mv_sub, op, Addressee, BayUid, DeviceUid, EdidProfile, MultiviewerAspectRatio,
MultiviewerEdidTemplate, MultiviewerHdcpMode, MultiviewerItcMode, MultiviewerOutputMode,
MultiviewerPipPosition, MultiviewerPipSize, MultiviewerSource, MultiviewerViewMode, Opcode,
RcAction, RcKey, SendError, StreamAddr, V2ipStreams, DEVICE_NAME_LEN, V2IP_PORT_ANC,
V2IP_PORT_AUDIO, V2IP_PORT_VIDEO,
};
use super::{Remote, Shared};
#[derive(Debug)]
#[non_exhaustive]
pub enum ControlError {
UnknownDevice(DeviceUid),
UnknownBay(BayUid),
UnknownSource(String),
Unsupported(&'static str),
InvalidRequest(&'static str),
NotReported(&'static str),
Send(SendError),
}
impl fmt::Display for ControlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownDevice(uid) => write!(f, "no device {uid}"),
Self::UnknownBay(uid) => write!(f, "no bay {uid}"),
Self::UnknownSource(name) => write!(f, "no source named {name:?}"),
Self::Unsupported(what) => f.write_str(what),
Self::InvalidRequest(what) => f.write_str(what),
Self::NotReported(what) => write!(f, "{what} has not been reported"),
Self::Send(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for ControlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Send(e) => Some(e),
_ => None,
}
}
}
impl From<SendError> for ControlError {
fn from(e: SendError) -> Self {
Self::Send(e)
}
}
type WriteBack = Box<dyn FnOnce(&mut State, &mut Vec<Event>) + Send>;
struct Command {
to: Addressee,
opcode: Opcode,
payload: Vec<u8>,
write_back: Option<WriteBack>,
}
impl Command {
fn new(to: Addressee, opcode: Opcode, payload: Vec<u8>) -> Self {
Self {
to,
opcode,
payload,
write_back: None,
}
}
fn then(mut self, f: impl FnOnce(&mut State, &mut Vec<Event>) + Send + 'static) -> Self {
self.write_back = Some(Box::new(f));
self
}
}
impl Shared {
fn command(
&self,
prepare: impl FnOnce(&State) -> Result<Command, ControlError>,
) -> Result<(), ControlError> {
let command = self.read(prepare)?;
self.send(&command.to, command.opcode, &command.payload)?;
if let Some(write_back) = command.write_back {
self.mutate(|state, ev| write_back(state, ev));
}
Ok(())
}
}
fn device_of(state: &State, uid: DeviceUid) -> Result<&Device, ControlError> {
state.device(uid).ok_or(ControlError::UnknownDevice(uid))
}
fn multiviewer_of(state: &State, uid: DeviceUid) -> Result<&Device, ControlError> {
let device = device_of(state, uid)?;
if !device.is_multiviewer() {
return Err(ControlError::Unsupported("the device is not a multiviewer"));
}
Ok(device)
}
fn mv_command(device: &Device, sub: u8, args: &[u8]) -> Command {
Command::new(
Addressee::device(device),
op::V2IP_MULTIVIEWER,
mv_cmd_payload(device.uid, sub, args),
)
}
fn source_index(source: MultiviewerSource, what: &'static str) -> Result<u8, ControlError> {
source
.to_zero_based()
.ok_or(ControlError::InvalidRequest(what))
}
fn mv_setting(value: u8, highest: u8, what: &'static str) -> Result<u8, ControlError> {
if (1..=highest).contains(&value) {
Ok(value)
} else {
Err(ControlError::InvalidRequest(what))
}
}
fn bay_of(state: &State, uid: BayUid) -> Result<(&Device, &Bay), ControlError> {
let device = device_of(state, uid.device)?;
let bay = device.bay(uid.port).ok_or(ControlError::UnknownBay(uid))?;
Ok((device, bay))
}
fn source_streams(device: &Device, port: u16) -> Result<&V2ipStreamSources, ControlError> {
let source = device
.bay(port)
.ok_or(ControlError::UnknownBay(BayUid::new(device.uid, port)))?;
device
.v2ip_source_for(source)
.ok_or(ControlError::NotReported("the source's stream addresses"))
}
fn v2ip_sink(state: &State, uid: BayUid) -> Result<(&Device, &Bay), ControlError> {
let (device, bay) = bay_of(state, uid)?;
if !bay.is_v2ip_sink() {
return Err(ControlError::Unsupported("routing needs a V2IP sink"));
}
Ok((device, bay))
}
fn stream_addr(target: V2ipRouteTarget, standard_port: u16) -> StreamAddr {
if target.ip.is_unspecified() {
return StreamAddr::default();
}
StreamAddr {
ip: target.ip,
port: target.port_or(standard_port),
}
}
fn stored_name(name: &str) -> String {
let bytes = name.as_bytes();
String::from_utf8_lossy(bytes.get(..DEVICE_NAME_LEN).unwrap_or(bytes)).into_owned()
}
impl Remote {
pub fn select_video_source(&self, sink: BayUid, source_port: u16) -> Result<(), ControlError> {
self.shared.command(|state| {
let (device, bay) = v2ip_sink(state, sink)?;
if !bay.is_output() {
return Err(ControlError::Unsupported("not an output bay"));
}
let streams = source_streams(device, source_port)?;
Ok(Command::new(
Addressee::device(device),
op::V2IP_SOURCE_SWITCH,
build_v2ip_source_switch(device.uid, streams.video.ip, Ipv4Addr::UNSPECIFIED),
))
})
}
pub fn select_audio_source(&self, sink: BayUid, source_port: u16) -> Result<(), ControlError> {
self.shared.command(|state| {
let (device, _) = v2ip_sink(state, sink)?;
let streams = source_streams(device, source_port)?;
Ok(Command::new(
Addressee::device(device),
op::V2IP_SOURCE_SWITCH,
build_v2ip_source_switch(device.uid, Ipv4Addr::UNSPECIFIED, streams.audio.ip),
))
})
}
pub fn select_video_source_by_name(
&self,
sink: BayUid,
name: &str,
) -> Result<(), ControlError> {
self.select_video_source(sink, self.source_port(sink, name)?)
}
pub fn select_audio_source_addr(
&self,
sink: BayUid,
audio_ip: Ipv4Addr,
audio_port: Option<u16>,
format: Option<V2ipAudioFormat>,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = v2ip_sink(state, sink)?;
let streams = V2ipStreams {
audio: StreamAddr {
ip: audio_ip,
port: audio_port.unwrap_or(V2IP_PORT_AUDIO),
},
..V2ipStreams::default()
};
Ok(Command::new(
Addressee::device(device),
op::V2IP_MANUAL_SRC_SWITCH,
build_v2ip_manual_source_switch(device.uid, streams, format),
))
})
}
pub fn select_source_addr(
&self,
sink: BayUid,
route: V2ipRoute,
format: Option<V2ipAudioFormat>,
) -> Result<(), ControlError> {
let streams = V2ipStreams {
video: stream_addr(route.video, V2IP_PORT_VIDEO),
audio: stream_addr(route.audio, V2IP_PORT_AUDIO),
anc: stream_addr(route.anc, V2IP_PORT_ANC),
};
let format = format.unwrap_or(V2ipAudioFormat::STANDARD);
self.shared.command(move |state| {
let (device, _) = v2ip_sink(state, sink)?;
Ok(Command::new(
Addressee::device(device),
op::V2IP_MANUAL_SRC_SWITCH,
build_v2ip_manual_source_switch(device.uid, streams, Some(format)),
))
})
}
pub fn select_audio_source_by_name(
&self,
sink: BayUid,
name: &str,
format: Option<V2ipAudioFormat>,
) -> Result<(), ControlError> {
let port = self.source_port(sink, name)?;
let Some(format) = format else {
return self.select_audio_source(sink, port);
};
self.shared.command(move |state| {
let (device, _) = v2ip_sink(state, sink)?;
let audio = source_streams(device, port)?.audio;
let streams = V2ipStreams {
audio: StreamAddr {
ip: audio.ip,
port: audio.port,
},
..V2ipStreams::default()
};
Ok(Command::new(
Addressee::device(device),
op::V2IP_MANUAL_SRC_SWITCH,
build_v2ip_manual_source_switch(device.uid, streams, Some(format)),
))
})
}
fn source_port(&self, sink: BayUid, name: &str) -> Result<u16, ControlError> {
self.shared.read(|state| {
let (device, _) = bay_of(state, sink)?;
device
.bay_by_user_name(name)
.map(|b| b.port)
.ok_or_else(|| ControlError::UnknownSource(name.to_owned()))
})
}
pub fn set_bay_name(&self, bay: BayUid, name: &str) -> Result<(), ControlError> {
let name = stored_name(name);
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
let payload = build_set_bay_name(device.uid, bay.port, &name);
Ok(
Command::new(Addressee::device(device), op::CHANGE_BAY_NAME, payload).then(
move |state, ev| {
if let Some(b) = state.bay_mut(bay) {
b.set_user_name(name, ev);
}
},
),
)
})
}
pub fn set_bay_hidden(&self, bay: BayUid, hidden: bool) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
Ok(Command::new(
Addressee::device(device),
op::BAY_HIDE,
build_bay_hide(device.uid, bay.port, hidden),
)
.then(move |state, ev| {
if let Some(b) = state.bay_mut(bay) {
let status = if hidden {
HiddenStatus::Hidden
} else {
HiddenStatus::Visible
};
b.apply_hidden(status, ev);
}
}))
})
}
pub fn select_edid_profile(
&self,
bay: BayUid,
profile: EdidProfile,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
Ok(Command::new(
Addressee::device(device),
op::BAY_EDID_PROFILE,
build_edid_profile(device.uid, profile),
)
.then(move |state, ev| {
if let Some(b) = state.bay_mut(bay) {
b.set_edid_profile(profile, ev);
}
}))
})
}
pub fn send_action(&self, bay: BayUid, action: RcAction) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
Ok(Command::new(
Addressee::device(device),
op::RC_TX_ACTION,
build_rc_action(device.uid, bay.port, action),
))
})
}
pub fn send_key(&self, bay: BayUid, key: RcKey) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
Ok(Command::new(
Addressee::device(device),
op::RC_TX_KEY,
build_rc_key(device.uid, bay.port, key),
))
})
}
pub fn power_on(&self, bay: BayUid) -> Result<(), ControlError> {
self.set_power(bay, RcAction::POWER_ON, PowerStatus::On)
}
pub fn power_off(&self, bay: BayUid) -> Result<(), ControlError> {
self.set_power(bay, RcAction::POWER_OFF, PowerStatus::Off)
}
fn set_power(
&self,
bay: BayUid,
action: RcAction,
power: PowerStatus,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
Ok(Command::new(
Addressee::device(device),
op::RC_TX_ACTION,
build_rc_action(device.uid, bay.port, action),
)
.then(move |state, ev| {
if let Some(b) = state.bay_mut(bay) {
b.set_power_status(power, ev);
}
}))
})
}
pub fn set_volume(
&self,
bay: BayUid,
volume: u8,
muted: Option<bool>,
) -> Result<(), ControlError> {
let volume = volume.min(100);
let wanted = VolumeMuteStatus {
volume_left: Some(volume),
volume_right: Some(volume),
muted_left: muted,
muted_right: muted,
};
self.shared.command(move |state| {
let target = state.volume_bay(bay);
let (device, b) = bay_of(state, target)?;
if !b.has_volume_control() {
return Err(ControlError::Unsupported("the bay has no volume control"));
}
Ok(Command::new(
Addressee::device(device),
op::AUDIO_SET_VOLUME,
build_set_volume(device.uid, target.port, wanted),
)
.then(move |state, ev| {
if let Some(device) = state.device_mut(target.device) {
device.apply_bay_volume(target.port, wanted, ev);
}
}))
})
}
pub fn volume_up(&self, bay: BayUid) -> Result<(), ControlError> {
self.set_volume(bay, self.current_volume(bay)?.saturating_add(1), None)
}
pub fn volume_down(&self, bay: BayUid) -> Result<(), ControlError> {
self.set_volume(bay, self.current_volume(bay)?.saturating_sub(1), None)
}
pub fn set_muted(&self, bay: BayUid, muted: bool) -> Result<(), ControlError> {
self.set_volume(bay, self.current_volume(bay)?, Some(muted))
}
fn current_volume(&self, bay: BayUid) -> Result<u8, ControlError> {
self.shared.read(|state| {
let (_, b) = bay_of(state, state.volume_bay(bay))?;
b.audio_volume
.map(|v| v.volume())
.ok_or(ControlError::NotReported("the bay's volume"))
})
}
pub fn set_amp_zone_settings(
&self,
bay: BayUid,
settings: AmpZoneSettings,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let (device, _) = bay_of(state, bay)?;
Ok(Command::new(
Addressee::device(device),
op::AMP_ZONE_SETTINGS,
build_amp_zone_settings(device.uid, bay.port, &settings),
)
.then(move |state, ev| {
if let Some(b) = state.bay_mut(bay) {
b.set_amp_settings(settings, ev);
}
}))
})
}
pub fn set_audio_endpoint_muted(
&self,
device: DeviceUid,
endpoint: u16,
muted: bool,
) -> Result<(), ControlError> {
self.audio_endpoint(device, audio_sub::MUTE, endpoint, u32::from(muted))
}
pub fn set_audio_endpoint_trigger(
&self,
device: DeviceUid,
endpoint: u16,
active: bool,
) -> Result<(), ControlError> {
self.audio_endpoint(device, audio_sub::TRIGGER, endpoint, u32::from(active))
}
pub fn set_audio_endpoint_volume(
&self,
device: DeviceUid,
endpoint: u16,
volume: u32,
) -> Result<(), ControlError> {
self.audio_endpoint(device, audio_sub::VOLUME, endpoint, volume)
}
fn audio_endpoint(
&self,
device: DeviceUid,
sub: u16,
endpoint: u16,
value: u32,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let device = device_of(state, device)?;
let mut payload = audio_cmd_header(sub, device.uid);
payload.extend_from_slice(&audio_param(endpoint, value));
Ok(Command::new(
Addressee::device(device),
op::V2IP_AUDIO,
payload,
))
})
}
pub fn select_audio_endpoint_input(
&self,
sink: DeviceUid,
sink_endpoint: u16,
source: DeviceUid,
source_endpoint: u16,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let device = device_of(state, sink)?;
Ok(Command::new(
Addressee::device(device),
op::V2IP_AUDIO,
build_audio_select_input(sink, sink_endpoint, source, source_endpoint),
))
})
}
pub fn subscribe_v2ip_stats(
&self,
device: DeviceUid,
subscribe: bool,
) -> Result<(), ControlError> {
self.shared.command(move |state| {
let device = device_of(state, device)?;
Ok(Command::new(
Addressee::device(device),
op::V2IP_STATS,
build_stats_request(device.uid, subscribe),
))
})
}
pub fn request_edid(&self, device: DeviceUid, output: bool) -> Result<(), ControlError> {
self.shared.command(move |state| {
let device = device_of(state, device)?;
Ok(Command::new(
Addressee::device(device),
op::DEV_EDID,
build_edid_request(device.uid, output),
))
})
}
pub fn request_signal_status(&self, device: Option<DeviceUid>) -> Result<(), ControlError> {
let Some(device) = device else {
self.shared
.send(&Addressee::Broadcast, op::BAY_SIGNAL_STATUS, &[])?;
return Ok(());
};
self.shared.command(move |state| {
let device = device_of(state, device)?;
Ok(Command::new(
Addressee::device(device),
op::BAY_SIGNAL_STATUS,
build_target_only(device.uid),
))
})
}
pub fn reboot(&self, device: DeviceUid) -> Result<(), ControlError> {
self.shared.command(move |state| {
let d = device_of(state, device)?;
Ok(Command::new(
Addressee::device(d),
op::SYS_REBOOT,
build_target_only(d.uid),
)
.then(move |state, _| {
if let Some(d) = state.device_mut(device) {
d.rebooting = true;
}
}))
})
}
pub fn send_monitoring_pulse(&self) -> Result<(), ControlError> {
self.shared
.send(&Addressee::Broadcast, op::SYS_MONITORING_PULSE, &[])?;
Ok(())
}
pub fn preview_video_wall(
&self,
sink: DeviceUid,
window: VideoWallWindow,
) -> Result<(), ControlError> {
self.set_video_wall(sink, window, VideoWallOp::PREVIEW)
}
pub fn store_video_wall(
&self,
sink: DeviceUid,
window: VideoWallWindow,
) -> Result<(), ControlError> {
self.set_video_wall(sink, window, VideoWallOp::STORE)
}
pub fn revert_video_wall(&self, sink: DeviceUid) -> Result<(), ControlError> {
self.set_video_wall(sink, VIDEO_WALL_CLEARED, VideoWallOp::REVERT)
}
fn set_video_wall(
&self,
sink: DeviceUid,
window: VideoWallWindow,
op: VideoWallOp,
) -> Result<(), ControlError> {
if op != VideoWallOp::REVERT {
window.validate().map_err(ControlError::InvalidRequest)?;
}
self.shared.command(move |state| {
let device = device_of(state, sink)?;
Ok(Command::new(
Addressee::device(device),
op::V2IP_VIDEO_WALL,
build_video_wall(device.uid, window, op),
))
})
}
pub fn set_multiviewer_view_mode(
&self,
device: DeviceUid,
mode: MultiviewerViewMode,
) -> Result<(), ControlError> {
let mode = mv_setting(mode.to_wire(), 8, "the multiviewer has no such view mode")?;
self.multiviewer(device, mv_sub::VIEW_MODE, &[mode])
}
pub fn set_multiviewer_video_source(
&self,
device: DeviceUid,
screen: u8,
source: MultiviewerSource,
) -> Result<(), ControlError> {
let source = source_index(source, "the source names no multiviewer input")?;
self.shared.command(|state| {
let target = multiviewer_of(state, device)?;
let windows = target
.multiviewer
.as_ref()
.and_then(MultiviewerStatus::window_count)
.unwrap_or(1);
if screen >= windows {
return Err(ControlError::InvalidRequest(
"the window is not one the multiviewer is showing",
));
}
Ok(mv_command(target, mv_sub::VIDEO_SOURCE, &[screen, source]))
})
}
pub fn set_multiviewer_audio_source(
&self,
device: DeviceUid,
source: MultiviewerSource,
) -> Result<(), ControlError> {
let source = source_index(source, "the audio source names no multiviewer input")?;
self.multiviewer(device, mv_sub::AUDIO_SOURCE, &[source])
}
pub fn set_multiviewer_audio_volume(
&self,
device: DeviceUid,
volume: u8,
muted: bool,
) -> Result<(), ControlError> {
if volume > 100 {
return Err(ControlError::InvalidRequest(
"a multiviewer volume is a percentage",
));
}
self.multiviewer(device, mv_sub::AUDIO_VOLUME, &[volume, u8::from(muted)])
}
pub fn set_multiviewer_edid_template(
&self,
device: DeviceUid,
template: MultiviewerEdidTemplate,
) -> Result<(), ControlError> {
let template = mv_setting(
template.to_wire(),
19,
"the multiviewer has no such EDID template",
)?;
self.multiviewer(device, mv_sub::EDID_TEMPLATE, &[template])
}
pub fn set_multiviewer_remote_control(
&self,
device: DeviceUid,
source: MultiviewerSource,
) -> Result<(), ControlError> {
let source = source_index(
source,
"the remote-control source names no multiviewer input",
)?;
self.multiviewer(device, mv_sub::ROUTE_RC, &[source])
}
pub fn set_multiviewer_pip_size(
&self,
device: DeviceUid,
size: MultiviewerPipSize,
) -> Result<(), ControlError> {
let size = mv_setting(
size.to_wire(),
3,
"the multiviewer has no such picture-in-picture size",
)?;
self.multiviewer(device, mv_sub::PIP_SIZE, &[size])
}
pub fn set_multiviewer_pip_position(
&self,
device: DeviceUid,
position: MultiviewerPipPosition,
) -> Result<(), ControlError> {
let position = mv_setting(
position.to_wire(),
4,
"the multiviewer has no such picture-in-picture position",
)?;
self.multiviewer(device, mv_sub::PIP_POSITION, &[position])
}
pub fn set_multiviewer_aspect_ratio(
&self,
device: DeviceUid,
aspect: MultiviewerAspectRatio,
) -> Result<(), ControlError> {
let aspect = mv_setting(
aspect.to_wire(),
2,
"the multiviewer has no such aspect ratio",
)?;
self.multiviewer(device, mv_sub::ASPECT, &[aspect])
}
pub fn set_multiviewer_auto_switch(
&self,
device: DeviceUid,
enable: bool,
) -> Result<(), ControlError> {
self.multiviewer(device, mv_sub::AUTO_SWITCH, &[u8::from(enable)])
}
pub fn set_multiviewer_output_mode(
&self,
device: DeviceUid,
mode: MultiviewerOutputMode,
) -> Result<(), ControlError> {
let mode = mv_setting(
mode.to_wire(),
14,
"the multiviewer has no such output mode",
)?;
self.multiviewer(device, mv_sub::OUTPUT_MODE, &[mode])
}
pub fn set_multiviewer_output_itc(
&self,
device: DeviceUid,
mode: MultiviewerItcMode,
) -> Result<(), ControlError> {
let mode = mv_setting(
mode.to_wire(),
2,
"the multiviewer has no such IT-content mode",
)?;
self.multiviewer(device, mv_sub::OUTPUT_ITC_MODE, &[mode])
}
pub fn set_multiviewer_hdcp_mode(
&self,
device: DeviceUid,
mode: MultiviewerHdcpMode,
) -> Result<(), ControlError> {
let mode = mv_setting(mode.to_wire(), 3, "the multiviewer has no such HDCP mode")?;
self.multiviewer(device, mv_sub::HDCP_MODE, &[mode])
}
pub fn set_multiviewer_input_source(
&self,
device: DeviceUid,
input: u8,
source: DeviceUid,
) -> Result<(), ControlError> {
if usize::from(input) >= MULTIVIEWER_INPUTS {
return Err(ControlError::InvalidRequest(
"the multiviewer has no such input",
));
}
let mut args = Vec::with_capacity(24);
args.extend_from_slice(source.as_bytes());
args.push(input);
args.extend_from_slice(&[0; 7]);
self.multiviewer(device, mv_sub::CONFIG_SOURCE, &args)
}
pub fn multiviewer_auto_route(&self, device: DeviceUid) -> Result<(), ControlError> {
self.multiviewer(device, mv_sub::AUTO_ROUTE, &[])
}
fn multiviewer(&self, device: DeviceUid, sub: u8, args: &[u8]) -> Result<(), ControlError> {
self.shared
.command(|state| Ok(mv_command(multiviewer_of(state, device)?, sub, args)))
}
}