From 2c53cfea4b29045b10a9900b56ceb9b22fead540 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sun, 12 Oct 2025 12:41:50 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extracted=20Generation?= =?UTF-8?q?=20of=2002-Commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - added builder for commands - moved Sleep, TrackingSpeed and ExposureModeType - moved ExposureModeType from constants into functions --- src/lib.rs | 192 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 146 insertions(+), 46 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 887e73e..1aac034 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,15 +7,6 @@ use std::{fmt::Display, io}; use thiserror::Error; use usbio::UvcUsbIo; -const AUTO_EXP_CMD: [u8; 18] = [ - 0xaa, 0x25, 0x16, 0x00, 0x0c, 0x00, 0x58, 0x91, 0x0a, 0x02, 0x82, 0x29, 0x05, 0x00, 0xb2, 0xaf, - 0x02, 0x04, -]; -const MANUAL_EXP_CMD: [u8; 18] = [ - 0xaa, 0x25, 0x15, 0x00, 0x0c, 0x00, 0xa8, 0x9e, 0x0a, 0x02, 0x82, 0x29, 0x05, 0x00, 0xf9, 0x27, - 0x01, 0x32, -]; - #[derive(Error, Debug)] pub enum Error { #[error("value of {1} is not supported for {0}")] @@ -195,6 +186,12 @@ pub enum ExposureMode { Face, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ExposureModeType { + Auto, + Manual, +} + pub enum TrackingMode { Headroom, Standard, @@ -226,6 +223,7 @@ pub trait OBSBotWebCam { 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>; + fn set_exposure_mode_type(&self, mode: ExposureModeType) -> Result<(), Error>; } impl OBSBotWebCam for Camera { @@ -234,27 +232,28 @@ impl OBSBotWebCam for Camera { 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]; + const FUNCTION_GROUP_SLEEP: [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::Awake => ( + [0xa5, 0x00], + [0x5f, 0xef], + [0xbe, 0x07, 0x00, 0x00, 0x00, 0x00], + ), + SleepMode::Sleep => ( + [0x42, 0x00], + [0xea, 0x63], + [0xbf, 0xfb, 0x01, 0x00, 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(); + let cmd = Command02::new() + .function_group(FUNCTION_GROUP_SLEEP) + .sequence_nr(sequence_nr) + .checksum(checksum) + .command(command) + .build(); self.get_status()?.awake = mode; @@ -291,28 +290,34 @@ impl OBSBotWebCam for Camera { } 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]; + const FUNCTION_GROUP_TRACKING_SPEED: [u8; 6] = [0x0a, 0x04, 0xc4, 0x0c, 0x01, 0x00]; + + let appendix: [u8; 16] = { + let mut a = [0x00; 16]; + a[..4].fill(0x00); + a + }; 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]), + TrackingSpeed::Standard => ( + [0x20, 0x00], + [0xab, 0xcb], + [0xe6, 0x3f, 0x00, 0x00, 0x00, 0x00], + ), + TrackingSpeed::Sport => ( + [0x21, 0x00], + [0xfa, 0x0e], + [0x67, 0xfe, 0x02, 0x00, 0x00, 0x00], + ), }; - 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(); + let cmd = Command02::new() + .function_group(FUNCTION_GROUP_TRACKING_SPEED) + .sequence_nr(sequence_nr) + .checksum(checksum) + .command(command) + .appendix(appendix) + .build(); self.get_status()?.speed = speed; @@ -331,19 +336,47 @@ impl OBSBotWebCam for Camera { fn set_exposure_mode(&self, mode: ExposureMode) -> Result<(), Error> { match mode { ExposureMode::Manual => { - self.send_cmd(0x2, 0x2, &MANUAL_EXP_CMD)?; + self.set_exposure_mode_type(ExposureModeType::Manual)?; } ExposureMode::Global => { - self.send_cmd(0x2, 0x2, &AUTO_EXP_CMD)?; + self.set_exposure_mode_type(ExposureModeType::Auto)?; self.send_cmd(0x2, 0x6, &[0x03, 0x01, 0x00])?; } ExposureMode::Face => { - self.send_cmd(0x2, 0x2, &AUTO_EXP_CMD)?; + self.set_exposure_mode_type(ExposureModeType::Auto)?; self.send_cmd(0x2, 0x6, &[0x03, 0x01, 0x01])?; } }; Ok(()) } + + fn set_exposure_mode_type(&self, mode: ExposureModeType) -> Result<(), Error> { + const FUNCTION_GROUP_EXPOSURE_MODE_TYPE: [u8; 6] = [0x0a, 0x02, 0x82, 0x29, 0x05, 0x00]; + + let (sequence_nr, checksum, command) = match mode { + ExposureModeType::Auto => ( + [0x16, 0x00], + [0x58, 0x91], + [0xb2, 0xaf, 0x02, 0x04, 0x00, 0x00], + ), + ExposureModeType::Manual => ( + [0x15, 0x00], + [0xa8, 0x9e], + [0xf9, 0x27, 0x01, 0x32, 0x00, 0x00], + ), + }; + + let command = Command02::new() + .function_group(FUNCTION_GROUP_EXPOSURE_MODE_TYPE) + .sequence_nr(sequence_nr) + .checksum(checksum) + .command(command) + .build(); + + self.send_cmd(0x2, 0x2, &command)?; + + Ok(()) + } } impl Camera { @@ -437,3 +470,70 @@ impl Camera { self.handle.io(unit, selector, query, data) } } + +pub struct Command02 { + pub function_group: Option<[u8; 6]>, + pub sequence_nr: Option<[u8; 2]>, + pub checksum: Option<[u8; 2]>, + pub command: Option<[u8; 6]>, + pub appendix: Option<[u8; 16]>, +} + +impl Command02 { + pub fn new() -> Self { + Self { + function_group: None, + sequence_nr: None, + checksum: None, + command: None, + appendix: None, + } + } + + pub fn function_group(mut self, function_group: [u8; 6]) -> Self { + self.function_group = Some(function_group); + self + } + + pub fn sequence_nr(mut self, sequence_number: [u8; 2]) -> Self { + self.sequence_nr = Some(sequence_number); + self + } + + pub fn checksum(mut self, checksum: [u8; 2]) -> Self { + self.checksum = Some(checksum); + self + } + + pub fn command(mut self, cmd: [u8; 6]) -> Self { + self.command = Some(cmd); + self + } + + pub fn appendix(mut self, app: [u8; 16]) -> Self { + self.appendix = Some(app); + self + } + + pub fn build(self) -> [u8; 36] { + const FRAME_ID: [u8; 2] = [0xaa, 0x25]; + const SEGMENT_SIZE: [u8; 2] = [0x0c, 0x00]; + + [ + FRAME_ID.as_slice(), + self.sequence_nr + .expect("sequence_nr is required") + .as_slice(), + SEGMENT_SIZE.as_slice(), + self.checksum.expect("checksum is required").as_slice(), + self.function_group + .expect("function_group is required") + .as_slice(), + self.command.expect("command is required").as_slice(), + self.appendix.unwrap_or([0x00; 16]).as_slice(), + ] + .concat() + .try_into() + .unwrap() + } +} From e1b4b48efdc2dd543142066594b26c5d2fd6fcf4 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sun, 12 Oct 2025 13:55:26 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20#14:=20Controls=20for=20Presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setter call for preset positions - integrated preset controls in GUI --- src/gui/main.rs | 27 ++++++++++++++++++++++++++- src/lib.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index 64d0bc0..3c6383f 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -14,6 +14,7 @@ enum Message { ChangeSleeping(bool), ChangeTracking(AIMode), ChangeTrackingSpeed(TrackingSpeed), + ChangePresetPosition(i8), ChangeHDR(bool), ChangeExposure(ExposureMode), ChangeDebugging(bool), @@ -109,6 +110,10 @@ impl Application for MainPanel { camera.set_tracking_speed(new_speed).unwrap(); Command::none() } + Message::ChangePresetPosition(new_position) => { + camera.goto_preset_position(new_position).unwrap(); + Command::none() + } Message::ChangeHDR(new_mode) => { self.hdr_on = new_mode; camera.set_hdr_mode(new_mode).unwrap(); @@ -282,6 +287,26 @@ impl Application for MainPanel { ] .spacing(10) .padding([10, 0, 10, 0]), + column![ + text("Presets: "), + row![ + button("1") + .on_press(Message::ChangePresetPosition(0)) + .width(Length::Fill) + .style(Button::Secondary), + button("2") + .on_press(Message::ChangePresetPosition(1)) + .width(Length::Fill) + .style(Button::Secondary), + button("3") + .on_press(Message::ChangePresetPosition(2)) + .width(Length::Fill) + .style(Button::Secondary), + ] + .spacing(10) + ] + .spacing(10) + .padding([10, 0, 10, 0]), row![ text("Tracking Speed: "), button("Standard") @@ -370,7 +395,7 @@ impl Application for MainPanel { fn main() -> iced::Result { MainPanel::run(Settings { window: window::Settings { - size: Size::from([300, 720]), + size: Size::from([300, 900]), resizable: false, decorations: true, ..Default::default() diff --git a/src/lib.rs b/src/lib.rs index 1aac034..a3e5aeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,7 @@ 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 goto_preset_position(&self, preset_nr: i8) -> Result<(), Error>; fn get_tracking_speed(&self) -> Result; fn set_tracking_speed(&self, speed: TrackingSpeed) -> Result<(), Error>; fn set_hdr_mode(&self, mode: bool) -> Result<(), Error>; @@ -285,6 +286,49 @@ impl OBSBotWebCam for Camera { Ok(self.get_status()?.ai_mode) } + fn goto_preset_position(&self, preset_nr: i8) -> Result<(), Error> { + if preset_nr < 0 || preset_nr > 3 { + return Err(Error::InvalidSetting); + } + + const FUNCTION_GROUP_PRESETS: [u8; 6] = [0x0a, 0x04, 0xc4, 0x39, 0x14, 0x00]; + + let (sequence_nr, checksum, command) = match preset_nr { + 0 => ( + [0x20, 0x00], + [0x6b, 0xdc], + [0xd6, 0xfb, 0x00, 0x00, 0x00, 0x00], + ), + 1 => ( + [0x1a, 0x00], + [0x4b, 0x03], + [0xeb, 0x2a, 0x01, 0x00, 0x00, 0x00], + ), + 2 => ( + [0x26, 0x00], + [0x8b, 0xc3], + [0xaf, 0x19, 0x02, 0x00, 0x00, 0x00], + ), + _ => panic!(), + }; + + let cmd = Command02::new() + .function_group(FUNCTION_GROUP_PRESETS) + .sequence_nr(sequence_nr) + .checksum(checksum) + .command(command) + .appendix({ + let mut arr = [0u8; 16]; + for i in 0..4 { + arr[i * 4..(i + 1) * 4].copy_from_slice(&[0x00, 0x00, 0x80, 0x3f]); + } + arr + }) + .build(); + + self.send_cmd(0x2, 0x2, &cmd) + } + fn get_tracking_speed(&self) -> Result { Ok(self.get_status()?.speed) } From 8d644ca0d17fae22b9430ba3a7728227e8360192 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sun, 12 Oct 2025 14:25:50 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=9A=B8=20#14:=20Automatically=20switc?= =?UTF-8?q?h=20to=20static=20on=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - presets define static positions - therefore, the AI-Mode is automatically changed --- src/gui/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gui/main.rs b/src/gui/main.rs index 3c6383f..f05a55e 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -111,6 +111,8 @@ impl Application for MainPanel { Command::none() } Message::ChangePresetPosition(new_position) => { + self.tracking = AIMode::NoTracking; + camera.set_ai_mode(AIMode::NoTracking).unwrap(); camera.goto_preset_position(new_position).unwrap(); Command::none() } From e26af2981fe57f5404fe80fa868bf40b25fa97b9 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sun, 12 Oct 2025 14:27:22 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20Removed=20unnecessary=20comm?= =?UTF-8?q?and=20in=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9e7b3aa..7840983 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,6 @@ which itself is substantially based on [samliddicott's meet4k package](https://g To run the GUI, you have to build the project and then run the binary: ```shell -cargo build - cargo run --package tiny4linux --bin obsbot-gui ``` From db1525d18bdd46e5abe1d9cc7a4a69069781deff Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sun, 12 Oct 2025 14:28:41 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9D=20#14:=20Added=20preset=20cont?= =?UTF-8?q?rol=20in=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7840983..8ae6950 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ which itself is substantially based on [samliddicott's meet4k package](https://g - Toggle for **Sleep/Wake** - Controls for the **tracking speed** +- Controls for **preset positions** - Updated Dependencies ### Shortly planned features -- Apply position-presets - UI-Upgrade (WIP) - Tests & Codecov check