framesmith-cli 0.1.0

CLI tool for controlling Samsung Frame TVs over the local network
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

/// Protocol version derived from the crate version at compile time.
/// The server and client must agree on this; a mismatch triggers a
/// server restart so the new binary takes over.
pub const PROTOCOL_VERSION: &str = env!("CARGO_PKG_VERSION");

// ---------------------------------------------------------------------------
// Client → Server
// ---------------------------------------------------------------------------

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VersionedRequest {
    pub version: String,
    pub request: Request,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Request {
    // Art image management
    ArtList,
    ArtShowInfo {
        content_id: Option<String>,
    },
    ArtSelect {
        content_id: String,
    },
    ArtUpload {
        file_path: PathBuf,
        select: bool,
        replace_selected: bool,
        canvas_color: Option<String>,
    },
    ArtDelete {
        content_ids: Vec<String>,
    },
    ArtThumbnail {
        content_id: String,
        output_path: PathBuf,
    },
    ArtFavorite {
        content_id: String,
        on: bool,
    },
    // Matte/filter
    MatteList,
    MatteSet {
        content_id: String,
        matte_id: String,
        portrait: Option<String>,
    },
    FilterList,
    FilterSet {
        content_id: String,
        filter_id: String,
    },
    // Art mode
    ModeOn,
    ModeOff,
    ModeStatus,
    // Slideshow
    SlideshowOn {
        duration_secs: Option<u64>,
        shuffle: bool,
        category: Option<String>,
    },
    SlideshowOff,
    SlideshowStatus,
    SlideshowConfigure {
        duration_secs: Option<u64>,
        shuffle: Option<bool>,
        category: Option<String>,
    },
    // Display
    BrightnessGet,
    BrightnessSet {
        value: u8,
    },
    ColorTempGet,
    ColorTempSet {
        value: i8,
    },
    RotationGet,
    AutoBrightnessOn,
    AutoBrightnessOff,
    // Motion
    MotionTimer {
        minutes: String,
    },
    MotionSensitivity {
        level: String,
    },
    // Remote
    RemoteButton {
        button: String,
    },
    // Auth
    AuthPair,
    AuthVerify,
    // Server management
    Shutdown,
    Status,
}

// ---------------------------------------------------------------------------
// Server → Client
// ---------------------------------------------------------------------------

#[derive(Debug, Serialize, Deserialize)]
pub enum ServerMessage {
    Heartbeat,
    Response(Response),
    ShuttingDown,
    VersionMismatch {
        server_version: String,
        client_version: String,
    },
}

#[derive(Debug, Serialize, Deserialize)]
pub enum Response {
    Ok { data: serde_json::Value },
    Error { message: String },
    TvDisconnected { message: String },
}

// ---------------------------------------------------------------------------
// Wire format helpers: [4-byte big-endian length][JSON payload]
// ---------------------------------------------------------------------------

pub async fn write_message<W: AsyncWriteExt + Unpin, T: Serialize>(
    writer: &mut W,
    msg: &T,
) -> std::io::Result<()> {
    let json = serde_json::to_vec(msg)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
    let len = json.len() as u32;
    writer.write_all(&len.to_be_bytes()).await?;
    writer.write_all(&json).await?;
    writer.flush().await?;
    Ok(())
}

pub async fn read_message<R: AsyncReadExt + Unpin, T: for<'de> Deserialize<'de>>(
    reader: &mut R,
) -> std::io::Result<T> {
    let mut len_buf = [0u8; 4];
    reader.read_exact(&mut len_buf).await?;
    let len = u32::from_be_bytes(len_buf) as usize;

    if len > 64 * 1024 * 1024 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("message too large: {len} bytes"),
        ));
    }

    let mut buf = vec![0u8; len];
    reader.read_exact(&mut buf).await?;
    serde_json::from_slice(&buf)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}