Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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

Expand Down
68 changes: 52 additions & 16 deletions src/gui/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -29,6 +30,7 @@ struct MainPanel {
camera: Option<Camera>,
awake: SleepMode,
tracking: AIMode,
tracking_speed: TrackingSpeed,
hdr_on: bool,
debugging_on: bool,
text_input: String,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -254,29 +262,57 @@ 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()),
self.debugging_on,
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()
})
Expand Down Expand Up @@ -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()
Expand Down
61 changes: 61 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct Camera {
pub struct CameraStatus {
pub awake: SleepMode,
pub ai_mode: AIMode,
pub speed: TrackingSpeed,
pub hdr_on: bool,
}

Expand All @@ -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),
}
}
Expand Down Expand Up @@ -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
}
Expand All @@ -87,6 +97,7 @@ impl CameraStatus {
CameraStatus {
awake: SleepMode::Unknown,
ai_mode: AIMode::Unknown,
speed: TrackingSpeed::Standard,
hdr_on: false,
}
}
Expand Down Expand Up @@ -162,6 +173,21 @@ impl TryFrom<i32> 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,
Expand Down Expand Up @@ -196,6 +222,8 @@ pub trait OBSBotWebCam {
fn get_sleep_mode(&self) -> Result<SleepMode, Error>;
fn set_ai_mode(&self, mode: AIMode) -> Result<(), Error>;
fn get_ai_mode(&self) -> Result<AIMode, Error>;
fn get_tracking_speed(&self) -> Result<TrackingSpeed, Error>;
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>;
}
Expand Down Expand Up @@ -258,6 +286,39 @@ impl OBSBotWebCam for Camera {
Ok(self.get_status()?.ai_mode)
}

fn get_tracking_speed(&self) -> Result<TrackingSpeed, Error> {
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]
Expand Down