use anyhow::{Result, bail};
use framesmith::{
ArtImage, Canvas, DisplayBrightness, DisplayColorTemp, FrameTv, MotionSensitivity, MotionTimer,
RemoteControlButton, SlideshowCategory, SlideshowConfig,
};
use crate::ipc::{Request, Response};
pub async fn dispatch(request: Request, tv: &FrameTv) -> Response {
match handle(request, tv).await {
Ok(data) => Response::Ok { data },
Err(e) => {
let msg = format!("{e:#}");
if is_connection_error(&msg) {
Response::TvDisconnected { message: msg }
} else {
Response::Error { message: msg }
}
}
}
}
fn is_connection_error(msg: &str) -> bool {
let lower = msg.to_lowercase();
lower.contains("connection")
|| lower.contains("timeout")
|| lower.contains("timed out")
|| lower.contains("websocket")
|| lower.contains("broken pipe")
|| lower.contains("reset by peer")
}
async fn handle(request: Request, tv: &FrameTv) -> Result<serde_json::Value> {
match request {
Request::ArtList => {
let images = tv.get_uploaded_art_images().await?;
let items: Vec<serde_json::Value> = images
.iter()
.map(|img| {
serde_json::json!({
"content_id": img.id(),
"category": img.category(),
"width": img.width(),
"height": img.height(),
"matte_id": img.matte_id(),
"portrait_matte_id": img.portrait_matte_id(),
})
})
.collect();
Ok(serde_json::json!(items))
}
Request::ArtShowInfo { content_id } => {
let img = match content_id {
Some(id) => tv.get_art_image_info(&id).await?,
None => tv.get_selected_art_image().await?,
};
Ok(serde_json::json!({
"content_id": img.id(),
"category": img.category(),
"width": img.width(),
"height": img.height(),
"matte_id": img.matte_id(),
"portrait_matte_id": img.portrait_matte_id(),
}))
}
Request::ArtSelect { content_id } => {
tv.select_art_image_by_id(&content_id).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::ArtUpload {
file_path,
select,
replace_selected,
canvas_color,
} => {
let ext = file_path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase());
let mut builder = ArtImage::builder();
builder = match ext.as_deref() {
Some("jpg" | "jpeg") => builder.load_jpeg(&file_path)?,
Some("png") => builder.load_png(&file_path)?,
Some(other) => bail!("unsupported image format: .{other}"),
None => bail!("cannot determine image format: file has no extension"),
};
if let Some(color_str) = &canvas_color {
let canvas = parse_canvas_color(color_str)?;
let rotation = tv.get_display_rotation().await?;
let (tv_w, tv_h) = match rotation {
framesmith::DisplayRotation::Portrait => (2160, 3840),
_ => (3840, 2160),
};
builder = builder.center_image(tv_w, tv_h, canvas);
}
let image = builder.build()?;
let old_content_id = if replace_selected {
Some(tv.get_selected_art_image().await?.id().to_owned())
} else {
None
};
let uploaded = if select || replace_selected {
tv.upload_and_select_art_image(image).await?
} else {
tv.upload_art_image(image).await?
};
if let Some(old_id) = &old_content_id
&& old_id != uploaded.id()
{
tv.delete_uploaded_art_images(&[old_id.as_str()]).await?;
}
Ok(serde_json::json!({
"content_id": uploaded.id(),
"width": uploaded.width(),
"height": uploaded.height(),
"selected": select || replace_selected,
"replaced": old_content_id.is_some(),
}))
}
Request::ArtDelete { content_ids } => {
let ids: Vec<&str> = content_ids.iter().map(|s| s.as_str()).collect();
tv.delete_uploaded_art_images(&ids).await?;
Ok(serde_json::json!({"status": "ok", "deleted": content_ids}))
}
Request::ArtThumbnail {
content_id,
output_path,
} => {
let thumb = tv.get_art_image_thumbnail(&content_id).await?;
std::fs::write(&output_path, thumb.as_bytes())?;
Ok(serde_json::json!({
"status": "ok",
"path": output_path.display().to_string(),
"size": thumb.as_bytes().len(),
}))
}
Request::ArtFavorite { content_id, on } => {
tv.set_art_image_favorite(&content_id, on).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::MatteList => {
let mattes = tv.get_available_mattes().await?;
let items: Vec<serde_json::Value> = mattes
.iter()
.map(|m| {
serde_json::json!({
"matte_id": m.id(),
"matte_type": m.matte_type(),
})
})
.collect();
Ok(serde_json::json!(items))
}
Request::MatteSet {
content_id,
matte_id,
portrait,
} => {
tv.set_art_image_matte(&content_id, &matte_id, portrait.as_deref())
.await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::FilterList => {
let filters = tv.get_available_photo_filters().await?;
let items: Vec<serde_json::Value> = filters
.iter()
.map(|f| {
serde_json::json!({
"filter_id": f.id(),
"filter_name": f.name(),
})
})
.collect();
Ok(serde_json::json!(items))
}
Request::FilterSet {
content_id,
filter_id,
} => {
tv.set_art_image_photo_filter(&content_id, &filter_id)
.await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::ModeOn => {
tv.enable_artmode().await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::ModeOff => {
tv.disable_artmode().await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::ModeStatus => {
let enabled = tv.is_artmode_enabled().await?;
Ok(serde_json::json!({"art_mode": enabled}))
}
Request::SlideshowOn {
duration_secs,
shuffle,
category,
} => {
let config = if duration_secs.is_some() || shuffle || category.is_some() {
let mut cfg = SlideshowConfig::default();
if let Some(d) = duration_secs {
cfg.art_image_duration = std::time::Duration::from_secs(d);
}
if shuffle {
cfg.shuffle = true;
}
if let Some(cat) = &category {
cfg.category = parse_slideshow_category(cat);
}
Some(cfg)
} else {
None
};
tv.enable_slideshow(config).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::SlideshowOff => {
tv.disable_slideshow().await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::SlideshowStatus => {
let enabled = tv.is_slideshow_enabled().await?;
let config = tv.get_slideshow_config().await?;
Ok(serde_json::json!({
"enabled": enabled,
"duration_secs": config.art_image_duration.as_secs(),
"shuffle": config.shuffle,
"category": format!("{:?}", config.category),
}))
}
Request::SlideshowConfigure {
duration_secs,
shuffle,
category,
} => {
let mut config = tv.get_slideshow_config().await?;
if let Some(d) = duration_secs {
config.art_image_duration = std::time::Duration::from_secs(d);
}
if let Some(s) = shuffle {
config.shuffle = s;
}
if let Some(cat) = &category {
config.category = parse_slideshow_category(cat);
}
tv.set_slideshow_config(config).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::BrightnessGet => {
let b = tv.get_display_brightness().await?;
Ok(serde_json::json!({"brightness": b.as_value()}))
}
Request::BrightnessSet { value } => {
let Some(brightness) = DisplayBrightness::from_value(value) else {
bail!("invalid brightness value: {value} (must be 0-10)");
};
tv.set_display_brightness(brightness).await?;
Ok(serde_json::json!({"status": "ok", "brightness": value}))
}
Request::ColorTempGet => {
let ct = tv.get_display_color_temp().await?;
Ok(serde_json::json!({"color_temp": ct.as_value()}))
}
Request::ColorTempSet { value } => {
let ct = DisplayColorTemp::from_value(value);
let Some(ct) = ct else {
bail!("invalid color temperature: {value} (must be -5 to 5)");
};
tv.set_display_color_temp(ct).await?;
Ok(serde_json::json!({"status": "ok", "color_temp": value}))
}
Request::RotationGet => {
let rotation = tv.get_display_rotation().await?;
Ok(serde_json::json!({"rotation": format!("{rotation:?}")}))
}
Request::AutoBrightnessOn => {
tv.enable_auto_brightness().await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::AutoBrightnessOff => {
tv.disable_auto_brightness().await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::MotionTimer { minutes } => {
let timer = parse_motion_timer(&minutes)?;
tv.set_motion_timer(timer).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::MotionSensitivity { level } => {
let sensitivity = parse_motion_sensitivity(&level)?;
tv.set_motion_sensitivity(sensitivity).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::RemoteButton { button } => {
let btn = parse_remote_button(&button)?;
tv.press_remote_control_button(btn).await?;
Ok(serde_json::json!({"status": "ok"}))
}
Request::AuthPair => {
Ok(serde_json::json!({"status": "paired"}))
}
Request::AuthVerify => {
tv.get_device_info().await?;
Ok(serde_json::json!({"status": "valid"}))
}
Request::Shutdown => {
Ok(serde_json::json!({"status": "ok"}))
}
Request::Status => {
Ok(serde_json::json!({"status": "ok"}))
}
}
}
fn parse_canvas_color(s: &str) -> Result<Canvas> {
match s.to_lowercase().as_str() {
"black" => Ok(Canvas::black()),
"white" => Ok(Canvas::white()),
_ => Canvas::from_hex_color(s)
.map_err(|e| anyhow::anyhow!("invalid canvas color '{s}': {e}")),
}
}
fn parse_slideshow_category(s: &str) -> SlideshowCategory {
match s.to_lowercase().as_str() {
"favorites" => SlideshowCategory::Favorites,
_ => SlideshowCategory::MyPictures,
}
}
fn parse_motion_timer(s: &str) -> Result<MotionTimer> {
Ok(match s {
"off" => MotionTimer::Off,
"5" => MotionTimer::Min5,
"15" => MotionTimer::Min15,
"30" => MotionTimer::Min30,
"60" => MotionTimer::Min60,
"120" => MotionTimer::Min120,
"240" => MotionTimer::Min240,
_ => bail!("invalid motion timer value: {s}"),
})
}
fn parse_motion_sensitivity(s: &str) -> Result<MotionSensitivity> {
Ok(match s.to_lowercase().as_str() {
"low" => MotionSensitivity::Low,
"medium" => MotionSensitivity::Medium,
"high" => MotionSensitivity::High,
_ => bail!("invalid motion sensitivity: {s}"),
})
}
fn parse_remote_button(s: &str) -> Result<RemoteControlButton> {
use RemoteControlButton::*;
Ok(match s.to_lowercase().as_str() {
"power" => Power,
"power-off" | "poweroff" => PowerOff,
"sleep" => Sleep,
"home" => Home,
"menu" => Menu,
"source" => Source,
"enter" => Enter,
"back" => Back,
"return" => Return,
"up" => Up,
"down" => Down,
"left" => Left,
"right" => Right,
"vol-up" | "volup" => VolumeUp,
"vol-down" | "voldown" => VolumeDown,
"mute" => Mute,
"ch-up" | "chup" => ChannelUp,
"ch-down" | "chdown" => ChannelDown,
"ch-list" | "chlist" => ChannelList,
"play" => Play,
"pause" => Pause,
"stop" => Stop,
"rewind" => Rewind,
"fast-forward" | "fastforward" => FastForward,
"info" => Info,
"guide" => Guide,
"tools" => Tools,
"caption" => Caption,
"0" => Number0,
"1" => Number1,
"2" => Number2,
"3" => Number3,
"4" => Number4,
"5" => Number5,
"6" => Number6,
"7" => Number7,
"8" => Number8,
"9" => Number9,
"red" => Red,
"green" => Green,
"yellow" => Yellow,
"blue" => Blue,
"ambient" => Ambient,
"picture-size" | "picturesize" => PictureSize,
"hdmi1" => Hdmi1,
"hdmi2" => Hdmi2,
"hdmi3" => Hdmi3,
"hdmi4" => Hdmi4,
_ => bail!("unknown remote button: {s}"),
})
}