diff --git a/README.md b/README.md index fe98a3e..9e7b3aa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Tiny4Linux: A GUI-Controller for the OBSBOT Tiny2 With the Tiny series, OBSBOT offers an affordable PTZ camera option for everyone. -The OBSBOT Center provides a way to fully control these devices: https://www.obsbot.com/de/download/obsbot-tiny-2 \ +The OBSBOT Center provides a way to fully control these devices: https://www.obsbot.com/de/download/obsbot-tiny-2 + As of [September 2025](https://web.archive.org/web/20251008100439/https://www.obsbot.com/download/obsbot-tiny-2), however, this software is only available for users on Windows and macOS. According to [the latest Steam Hardware Survey](https://web.archive.org/web/20251008101418/https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam), Linux users account for 2.68%, which is actually more than macOS users. Especially within the tech community, many users (rightly so :P) rely on Linux as their operating system. @@ -22,13 +23,15 @@ which itself is substantially based on [samliddicott's meet4k package](https://g ## New features and improvements -- Toggle for Sleep/Wake +- Toggle for **Sleep/Wake** +- Controls for the **tracking speed** - Updated Dependencies ### Shortly planned features - Apply position-presets -- UI-Upgrade +- UI-Upgrade (WIP) +- Tests & Codecov check ## Run the GUI diff --git a/src/gui/main.rs b/src/gui/main.rs index 1daeb34..64d0bc0 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -7,12 +7,13 @@ use iced::widget::{button, column, image, row, text, text_input, toggler}; use iced::{Alignment, Length, Size, Subscription, executor, time, window}; use iced::{Application, Command, Element, Settings, Theme}; use std::time::Duration; -use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam, SleepMode}; +use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam, SleepMode, TrackingSpeed}; #[derive(Debug, Clone, PartialEq)] enum Message { ChangeSleeping(bool), ChangeTracking(AIMode), + ChangeTrackingSpeed(TrackingSpeed), ChangeHDR(bool), ChangeExposure(ExposureMode), ChangeDebugging(bool), @@ -29,6 +30,7 @@ struct MainPanel { camera: Option, awake: SleepMode, tracking: AIMode, + tracking_speed: TrackingSpeed, hdr_on: bool, debugging_on: bool, text_input: String, @@ -57,6 +59,7 @@ impl Application for MainPanel { camera, awake: status.awake, tracking: status.ai_mode, + tracking_speed: status.speed, hdr_on: status.hdr_on, debugging_on: false, text_input: String::new(), @@ -101,6 +104,11 @@ impl Application for MainPanel { camera.set_ai_mode(tracking_type).unwrap(); Command::none() } + Message::ChangeTrackingSpeed(new_speed) => { + self.tracking_speed = new_speed; + camera.set_tracking_speed(new_speed).unwrap(); + Command::none() + } Message::ChangeHDR(new_mode) => { self.hdr_on = new_mode; camera.set_hdr_mode(new_mode).unwrap(); @@ -254,21 +262,45 @@ impl Application for MainPanel { }), ] .spacing(10), + column![ + text("Exposure: "), + row![ + button("Manual") + .on_press(Message::ChangeExposure(ExposureMode::Manual)) + .width(Length::Fill) + .style(Button::Secondary), + button("Face") + .on_press(Message::ChangeExposure(ExposureMode::Face)) + .width(Length::Fill) + .style(Button::Secondary), + button("Global") + .on_press(Message::ChangeExposure(ExposureMode::Global)) + .width(Length::Fill) + .style(Button::Secondary), + ] + .spacing(10) + ] + .spacing(10) + .padding([10, 0, 10, 0]), row![ - button("Manual") - .on_press(Message::ChangeExposure(ExposureMode::Manual)) - .width(Length::Fill) - .style(Button::Secondary), - button("Face") - .on_press(Message::ChangeExposure(ExposureMode::Face)) - .width(Length::Fill) - .style(Button::Secondary), - button("Global") - .on_press(Message::ChangeExposure(ExposureMode::Global)) - .width(Length::Fill) - .style(Button::Secondary), + text("Tracking Speed: "), + button("Standard") + .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Standard)) + .style(if self.tracking_speed == TrackingSpeed::Standard { + Button::Primary + } else { + Button::Secondary + }), + button("Sport") + .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Sport)) + .style(if self.tracking_speed == TrackingSpeed::Sport { + Button::Primary + } else { + Button::Secondary + }), ] - .spacing(10), + .spacing(10) + .padding([10, 0, 10, 0]), toggler(Some("HDR".to_string()), self.hdr_on, Message::ChangeHDR), toggler( Some("Debugging".to_string()), @@ -276,7 +308,11 @@ impl Application for MainPanel { Message::ChangeDebugging ), text(if self.awake == SleepMode::Awake { - self.tracking.to_string() + format!( + "{tracking_mode} ({tracking_speed})", + tracking_mode = self.tracking.to_string(), + tracking_speed = self.tracking_speed.to_string() + ) } else { self.awake.to_string() }) @@ -334,7 +370,7 @@ impl Application for MainPanel { fn main() -> iced::Result { MainPanel::run(Settings { window: window::Settings { - size: Size::from([300, 640]), + size: Size::from([300, 720]), resizable: false, decorations: true, ..Default::default() diff --git a/src/lib.rs b/src/lib.rs index 0e594c4..887e73e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ pub struct Camera { pub struct CameraStatus { pub awake: SleepMode, pub ai_mode: AIMode, + pub speed: TrackingSpeed, pub hdr_on: bool, } @@ -48,6 +49,7 @@ impl CameraStatus { CameraStatus { awake: Self::decode_sleep_mode(bytes), ai_mode: Self::decode_ai_mode(bytes), + speed: Self::decode_tracking_speed(bytes), hdr_on: Self::decode_hdr_on(bytes), } } @@ -79,6 +81,14 @@ impl CameraStatus { } } + fn decode_tracking_speed(bytes: &[u8]) -> TrackingSpeed { + match bytes[0x21] { + 0 => TrackingSpeed::Standard, + 2 => TrackingSpeed::Sport, + _ => TrackingSpeed::Standard, + } + } + fn decode_hdr_on(bytes: &[u8]) -> bool { bytes[0x6] != 0 } @@ -87,6 +97,7 @@ impl CameraStatus { CameraStatus { awake: SleepMode::Unknown, ai_mode: AIMode::Unknown, + speed: TrackingSpeed::Standard, hdr_on: false, } } @@ -162,6 +173,21 @@ impl TryFrom for AIMode { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TrackingSpeed { + Standard, + Sport, +} + +impl Display for TrackingSpeed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TrackingSpeed::Standard => write!(f, "Standard"), + TrackingSpeed::Sport => write!(f, "Sport"), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum ExposureMode { Manual, @@ -196,6 +222,8 @@ pub trait OBSBotWebCam { fn get_sleep_mode(&self) -> Result; fn set_ai_mode(&self, mode: AIMode) -> Result<(), Error>; fn get_ai_mode(&self) -> Result; + fn get_tracking_speed(&self) -> Result; + fn set_tracking_speed(&self, speed: TrackingSpeed) -> Result<(), Error>; fn set_hdr_mode(&self, mode: bool) -> Result<(), Error>; fn set_exposure_mode(&self, mode: ExposureMode) -> Result<(), Error>; } @@ -258,6 +286,39 @@ impl OBSBotWebCam for Camera { Ok(self.get_status()?.ai_mode) } + fn get_tracking_speed(&self) -> Result { + Ok(self.get_status()?.speed) + } + + fn set_tracking_speed(&self, speed: TrackingSpeed) -> Result<(), Error> { + const FRAME_ID: [u8; 2] = [0xaa, 0x25]; + const SEGMENT_SIZE: [u8; 2] = [0x0c, 0x00]; + const FUNCTION_GROUP: [u8; 6] = [0x0a, 0x04, 0xc4, 0x0c, 0x01, 0x00]; + const APPENDIX: [u8; 7] = [0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]; + + let (sequence_nr, checksum, command) = match speed { + TrackingSpeed::Standard => ([0x20, 0x00], [0xab, 0xcb], [0xe6, 0x3f, 0x00]), + TrackingSpeed::Sport => ([0x21, 0x00], [0xfa, 0x0e], [0x67, 0xfe, 0x02]), + }; + + let cmd: [u8; 24] = [ + FRAME_ID.as_slice(), + sequence_nr.as_slice(), + SEGMENT_SIZE.as_slice(), + checksum.as_slice(), + FUNCTION_GROUP.as_slice(), + command.as_slice(), + APPENDIX.as_slice(), + ] + .concat() + .try_into() + .unwrap(); + + self.get_status()?.speed = speed; + + self.send_cmd(0x2, 0x2, &cmd) + } + fn set_hdr_mode(&self, mode: bool) -> Result<(), Error> { let cmd = if mode { [0x01, 0x01, 0x01]