From 7c152514ac67f51e80873a2c8898e96577586685 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 17:37:23 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extracted=20decoding?= =?UTF-8?q?=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b96a9c8..1eebccb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,10 +42,17 @@ pub struct CameraStatus { impl CameraStatus { pub fn decode(bytes: &[u8]) -> Self { + CameraStatus { + ai_mode: Self::decode_ai_mode(bytes), + hdr_on: Self::decode_hdr_on(bytes), + } + } + + fn decode_ai_mode(bytes: &[u8]) -> AIMode { let m = bytes[0x18]; let n = bytes[0x1c]; - let ai_mode = match (m, n) { + match (m, n) { (0, 0) => AIMode::NoTracking, (2, 0) => AIMode::NormalTracking, (2, 1) => AIMode::UpperBody, @@ -57,11 +64,11 @@ impl CameraStatus { (6, 0) => AIMode::Hand, (1, 0) => AIMode::Group, (_, _) => panic!(), - }; - - let hdr_on = bytes[0x6] != 0; + } + } - CameraStatus { ai_mode, hdr_on } + fn decode_hdr_on(bytes: &[u8]) -> bool { + bytes[0x6] != 0 } pub fn default() -> Self { From 6b1268e01fb97e7a6981e48d55d67094a82237ea Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 17:41:06 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Moved=20enum=20for=20b?= =?UTF-8?q?etter=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bringing AIMode parts closer together --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1eebccb..952cb3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,13 +93,6 @@ pub enum AIMode { Group, } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ExposureMode { - Manual, - Global, - Face, -} - impl Display for AIMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -137,6 +130,13 @@ impl TryFrom for AIMode { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ExposureMode { + Manual, + Global, + Face, +} + pub enum TrackingMode { Headroom, Standard, From 6a30565be96704b5f98e8c805b5ed1aa50995243 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 17:49:38 +0200 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=A5=85=20Added=20unkown=20AIMode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handling for unknown AIModes --- src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 952cb3c..33517db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ impl CameraStatus { (4, 0) => AIMode::Whiteboard, (6, 0) => AIMode::Hand, (1, 0) => AIMode::Group, - (_, _) => panic!(), + (_, _) => AIMode::Unknown, } } @@ -73,7 +73,7 @@ impl CameraStatus { pub fn default() -> Self { CameraStatus { - ai_mode: AIMode::NoTracking, + ai_mode: AIMode::Unknown, hdr_on: false, } } @@ -91,6 +91,7 @@ pub enum AIMode { Whiteboard, Hand, Group, + Unknown, } impl Display for AIMode { @@ -106,6 +107,7 @@ impl Display for AIMode { AIMode::Whiteboard => write!(f, "Whiteboard"), AIMode::Hand => write!(f, "Hand"), AIMode::Group => write!(f, "Group"), + AIMode::Unknown => write!(f, "Unknown"), } } } @@ -179,6 +181,7 @@ impl OBSBotWebCam for Camera { AIMode::CloseUp => [0x16, 0x02, 0x02, 0x02], AIMode::Headless => [0x16, 0x02, 0x02, 0x03], AIMode::LowerBody => [0x16, 0x02, 0x02, 0x04], + AIMode::Unknown => [0x16, 0x02, 0x00, 0x00], }; self.send_cmd(0x2, 0x6, &cmd) } From 4b62cbe741e59b8236959e823642b75c129c7ae5 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 17:50:35 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8=20API=20for=20reading=20the=20Sle?= =?UTF-8?q?ep-Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sleep/wake-state can be determined --- src/lib.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 33517db..b10b688 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,7 @@ pub struct Camera { } pub struct CameraStatus { + pub awake: SleepMode, pub ai_mode: AIMode, pub hdr_on: bool, } @@ -43,11 +44,20 @@ pub struct CameraStatus { impl CameraStatus { pub fn decode(bytes: &[u8]) -> Self { CameraStatus { + awake: Self::decode_sleep_mode(bytes), ai_mode: Self::decode_ai_mode(bytes), hdr_on: Self::decode_hdr_on(bytes), } } + fn decode_sleep_mode(bytes: &[u8]) -> SleepMode { + match bytes[0x02] { + 0 => SleepMode::Awake, + 1 => SleepMode::Sleep, + _ => SleepMode::Unknown, + } + } + fn decode_ai_mode(bytes: &[u8]) -> AIMode { let m = bytes[0x18]; let n = bytes[0x1c]; @@ -73,12 +83,30 @@ impl CameraStatus { pub fn default() -> Self { CameraStatus { + awake: SleepMode::Unknown, ai_mode: AIMode::Unknown, hdr_on: false, } } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SleepMode { + Awake, + Sleep, + Unknown, +} + +impl Display for SleepMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SleepMode::Awake => write!(f, "Awake"), + SleepMode::Sleep => write!(f, "Sleeping"), + SleepMode::Unknown => write!(f, "Unknown"), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum AIMode { NoTracking, @@ -162,6 +190,7 @@ impl TryFrom for TrackingMode { } 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 set_hdr_mode(&self, mode: bool) -> Result<(), Error>; @@ -169,6 +198,10 @@ pub trait OBSBotWebCam { } impl OBSBotWebCam for Camera { + fn get_sleep_mode(&self) -> Result { + Ok(self.get_status()?.awake) + } + fn set_ai_mode(&self, mode: AIMode) -> Result<(), Error> { let cmd = match mode { AIMode::NoTracking => [0x16, 0x02, 0x00, 0x00], From 3f1cad194f02d667789ffaf935f680f14f19daca Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 18:08:25 +0200 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8=20API=20for=20setting=20the=20Sle?= =?UTF-8?q?ep-Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sleep/wake-state can be set - invalid "unknown"-mode gets caught --- src/lib.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b10b688..335bba5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ pub enum Error { OscError(#[from] rosc::OscError), #[error("no camera found")] NoCameraFound, + #[error("Invalid setting")] + InvalidSetting, } #[derive(Debug)] @@ -190,6 +192,7 @@ impl TryFrom for TrackingMode { } pub trait OBSBotWebCam { + fn set_sleep_mode(&self, mode: SleepMode) -> Result<(), Error>; fn get_sleep_mode(&self) -> Result; fn set_ai_mode(&self, mode: AIMode) -> Result<(), Error>; fn get_ai_mode(&self) -> Result; @@ -198,6 +201,38 @@ pub trait OBSBotWebCam { } impl OBSBotWebCam for Camera { + fn set_sleep_mode(&self, mode: SleepMode) -> Result<(), Error> { + if mode == SleepMode::Unknown { + return Err(Error::InvalidSetting); + } + + const FRAME_ID: [u8; 2] = [0xaa, 0x25]; + const SEGMENT_SIZE: [u8; 2] = [0x0c, 0x00]; + const FUNCTION_GROUP: [u8; 6] = [0x0a, 0x02, 0xc2, 0xa0, 0x04, 0x00]; + + let (sequence_nr, checksum, command) = match mode { + SleepMode::Awake => ([0xa5, 0x00], [0x5f, 0xef], [0xbe, 0x07, 0x00, 0x00, 0x00]), + SleepMode::Sleep => ([0x42, 0x00], [0xea, 0x63], [0xbf, 0xfb, 0x01, 0x00, 0x00]), + SleepMode::Unknown => panic!(), + }; + + let cmd: [u8; 19] = [ + FRAME_ID.as_slice(), + sequence_nr.as_slice(), + SEGMENT_SIZE.as_slice(), + checksum.as_slice(), + FUNCTION_GROUP.as_slice(), + command.as_slice(), + ] + .concat() + .try_into() + .unwrap(); + + self.get_status()?.awake = mode; + + self.send_cmd(0x2, 0x2, &cmd) + } + fn get_sleep_mode(&self) -> Result { Ok(self.get_status()?.awake) } From c742f7bc9a36c2cb1af78b25bb7016efc7d28a8c Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 18:18:12 +0200 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=A8=20Integrated=20Sleeping=20in=20th?= =?UTF-8?q?e=20GUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - camera can be set to sleep/wake --- src/gui/main.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index fd0291e..5ea7513 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -3,10 +3,11 @@ use iced::widget::{button, column, image, row, text, text_input, toggler}; use iced::{Alignment, Length, Subscription, executor, time, window}; use iced::{Application, Command, Element, Settings, Theme}; use std::time::Duration; -use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam}; +use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam, SleepMode}; #[derive(Debug, Clone, PartialEq)] enum Message { + ChangeSleeping(bool), ChangeTracking(AIMode), ChangeHDR(bool), ChangeExposure(ExposureMode), @@ -22,6 +23,7 @@ enum Message { struct MainPanel { camera: Option, + awake: SleepMode, tracking: AIMode, hdr_on: bool, debugging_on: bool, @@ -49,6 +51,7 @@ impl Application for MainPanel { ( MainPanel { camera, + awake: status.awake, tracking: status.ai_mode, hdr_on: status.hdr_on, debugging_on: false, @@ -78,6 +81,17 @@ impl Application for MainPanel { let camera = self.camera.as_ref().unwrap(); match message { + Message::ChangeSleeping(should_sleep) => { + if should_sleep { + self.awake = SleepMode::Sleep; + camera.set_sleep_mode(SleepMode::Sleep).unwrap(); + } else { + self.awake = SleepMode::Awake; + camera.set_sleep_mode(SleepMode::Awake).unwrap(); + } + + Command::none() + } Message::ChangeTracking(tracking_type) => { self.tracking = tracking_type; camera.set_ai_mode(tracking_type).unwrap(); @@ -136,6 +150,11 @@ impl Application for MainPanel { .vertical_alignment(Vertical::Center), image("src/assets/obsbot-tiny-2.png").height(100) ], + toggler( + Some("Sleeping".to_string()), + self.awake != SleepMode::Awake, + Message::ChangeSleeping + ), button("Static").on_press(Message::ChangeTracking(AIMode::NoTracking)), button("Normal Tracking").on_press(Message::ChangeTracking(AIMode::NormalTracking)), row![ @@ -192,7 +211,11 @@ impl Application for MainPanel { self.debugging_on, Message::ChangeDebugging ), - text(self.tracking) + text(if self.awake == SleepMode::Awake { + self.tracking.to_string() + } else { + self.awake.to_string() + }) ]; if self.debugging_on { From 6c28799c1e588ce624e2d30627f371cb4fae8a6b Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 18:18:52 +0200 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=92=AC=20"No=20Tracking"=20is=20displ?= =?UTF-8?q?ayed=20as=20"Static"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - more precise state description --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 335bba5..0e594c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,7 +127,7 @@ pub enum AIMode { impl Display for AIMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AIMode::NoTracking => write!(f, "No Tracking"), + AIMode::NoTracking => write!(f, "Static"), AIMode::NormalTracking => write!(f, "Normal Tracking"), AIMode::UpperBody => write!(f, "Upper Body"), AIMode::CloseUp => write!(f, "Close-up"), From 138a9c6ec11ca56b57837bb516b60a4f7e7f9564 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 18:34:50 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=9A=A8=20Make=20lifetime=20in=20view(?= =?UTF-8?q?)=20explicit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added '_' to clarify that the returned Element lives as long as &self --- src/gui/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index 5ea7513..2402b38 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -140,7 +140,7 @@ impl Application for MainPanel { } } - fn view(&self) -> Element { + fn view(&'_ self) -> Element<'_, Message> { if self.camera.is_some() { let mut elements = column![ row![ From 1d773e8fe8092a1ba2e4dcd0a9d2f401ed208e69 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sat, 11 Oct 2025 18:42:51 +0200 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=93=9D=20Updated=20README-file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 1717a51..fe98a3e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Especially within the tech community, many users (rightly so :P) rely on Linux a Unfortunately, they currently miss out on the configuration options offered by the OBSBOT Center. To partially address this issue, [OBSBOT now provides an SDK](https://www.obsbot.com/de/sdk), which allows developers to replicate certain functionalities. +However, the availability of the SDK is still limited. +Fortunately, the community has since managed to implement several solutions on its own through [reverse engineering](https://github.com/taxfromdk/obsbot_tiny_reversing) and a lot of dedication. This project provides a graphical interface for some basic features. However, to make full use of all functions, a configuration must first be performed in the OBSBOT Center using one of the supported operating systems. @@ -18,6 +20,16 @@ A guide on how to do this quickly and easily on Linux can be found below. This repository is a fork of [Constantine Evans's "Tiny2"](https://github.com/cgevans/tiny2), which itself is substantially based on [samliddicott's meet4k package](https://github.com/samliddicott/meet4k). +## New features and improvements + +- Toggle for Sleep/Wake +- Updated Dependencies + +### Shortly planned features + +- Apply position-presets +- UI-Upgrade + ## Run the GUI To run the GUI, you have to build the project and then run the binary: