Skip to content
Open
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ which itself is substantially based on [samliddicott's meet4k package](https://g
- UI-Upgrade (WIP)
- Tests & Codecov check

### Supported Cameras

Tiny4Linux is developed and tested with the **OBSBOT Tiny 2**.
If no Tiny 2 is connected, an **OBSBOT Tiny 4K** is used instead ([#72](https://github.com/OpenFoxes/Tiny4Linux/issues/72)).
The Tiny 4K speaks an older protocol than the Tiny 2, so it understands a part of the commands
directly, a part only through its own commands, and some not at all:

| Feature | OBSBOT Tiny 4K |
| ----------------------------------------- | ------------------------------------------------------- |
| HDR on/off | ✅ works |
| Exposure mode `face` / `global` | ✅ works |
| Sleep / wake | ✅ works, using the older protocol of the Tiny 4K |
| Tracking `static`, `normal`, `upper-body` | ✅ works, using the older protocol of the Tiny 4K |
| Preset positions | ✅ works, recalls the positions stored by OBSBOT Center |
| Status (`info`) | ✅ sleep state, tracking mode and HDR are reported |
| Other tracking modes | ❌ the Tiny 4K does not have them, this is reported |
| Tracking speed | ❌ the Tiny 4K has no such setting, this is reported |
| Exposure mode `manual` | ❌ use the V4L2 exposure controls instead |

The Tiny 4K ignores settings while it is in standby (about two minutes without a video stream).
Start a video stream first, then the settings are applied immediately.

### Info on PTZ-/Gimbal-Controls

The main focus of the [original project](https://github.com/cgevans/tiny2) was the implementation of OBSBOT-specific functionalities on Linux.
Expand Down
123 changes: 98 additions & 25 deletions src/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn main() {

let args = Args::parse();

let mut camera = Camera::new("OBSBOT Tiny 2").ok();
let mut camera = Camera::detect().ok();

if camera.is_none() {
println!("{}", t!("shared.errors.no_camera"));
Expand Down Expand Up @@ -139,8 +139,20 @@ fn main() {

println!("{}:", t!("shared.info.camera_status"));
println!(" 💤 {}: {}", t!("shared.info.sleep_mode"), info.awake);
println!(" 🤖 {}: {}", t!("shared.info.ai_mode"), info.ai_mode);
println!(" 🏃 {}: {}", t!("shared.info.tracking_speed"), info.speed);
println!(
" 🤖 {}: {}",
t!("shared.info.ai_mode"),
camera.get_ai_mode().unwrap_or(AIMode::Unknown)
);
if camera.supports_tracking_speed() {
println!(" 🏃 {}: {}", t!("shared.info.tracking_speed"), info.speed);
} else {
println!(
" 🏃 {}: {}",
t!("shared.info.tracking_speed"),
t!("shared.info.not_available")
);
}
println!(" 💐 {}: {}", t!("shared.info.hdr"), info.hdr_on);
}
}
Expand Down Expand Up @@ -216,6 +228,26 @@ fn evaluate_sleep_arg(state: Option<OnOffArg>, camera: Camera) {
}
}

/// Applies an AI tracking mode, or reports that the connected camera does not have it.
///
/// The Tiny 4K only knows a part of the Tiny 2 modes, and silently ignoring the
/// rest is what this whole change is trying to get rid of (#72).
fn apply_ai_mode(camera: &Camera, mode: AIMode, message: &str) {
if !camera.supports_ai_mode(mode) {
eprintln!("{}", t!("shared.errors.unsupported_tracking_mode"));
std::process::exit(1);
}

println!("{message}");
camera.set_ai_mode(mode).unwrap();
}

/// Reports that the connected camera does not have a feature, and exits.
fn unsupported(message: &str) -> ! {
eprintln!("{message}");
std::process::exit(1);
}

fn evaluate_tracking_arg(tracking_mode: Option<TrackingArg>, camera: Camera) {
let response_setting_to = t!("cli.tracking_mode.response_setting_to");
let static_mode = t!("cli.tracking_mode.static");
Expand All @@ -231,44 +263,74 @@ fn evaluate_tracking_arg(tracking_mode: Option<TrackingArg>, camera: Camera) {

match tracking_mode {
Some(TrackingArg::Static) => {
println!("{} {}", response_setting_to, static_mode);
camera.set_ai_mode(AIMode::NoTracking).unwrap();
apply_ai_mode(
&camera,
AIMode::NoTracking,
&format!("{} {}", response_setting_to, static_mode),
);
}
Some(TrackingArg::Normal) => {
println!("{} {}", response_setting_to, normal);
camera.set_ai_mode(AIMode::NormalTracking).unwrap();
apply_ai_mode(
&camera,
AIMode::NormalTracking,
&format!("{} {}", response_setting_to, normal),
);
}
Some(TrackingArg::CloseUp) => {
println!("{} {}", response_setting_to, close_up);
camera.set_ai_mode(AIMode::CloseUp).unwrap();
apply_ai_mode(
&camera,
AIMode::CloseUp,
&format!("{} {}", response_setting_to, close_up),
);
}
Some(TrackingArg::UpperBody) => {
println!("{} {}", response_setting_to, upper_body);
camera.set_ai_mode(AIMode::UpperBody).unwrap();
apply_ai_mode(
&camera,
AIMode::UpperBody,
&format!("{} {}", response_setting_to, upper_body),
);
}
Some(TrackingArg::Headless) => {
println!("{} {}", response_setting_to, headless);
camera.set_ai_mode(AIMode::Headless).unwrap();
apply_ai_mode(
&camera,
AIMode::Headless,
&format!("{} {}", response_setting_to, headless),
);
}
Some(TrackingArg::LowerBody) => {
println!("{} {}", response_setting_to, lower_body);
camera.set_ai_mode(AIMode::LowerBody).unwrap();
apply_ai_mode(
&camera,
AIMode::LowerBody,
&format!("{} {}", response_setting_to, lower_body),
);
}
Some(TrackingArg::Desk) => {
println!("{} {}", response_setting_to, desk);
camera.set_ai_mode(AIMode::DeskMode).unwrap();
apply_ai_mode(
&camera,
AIMode::DeskMode,
&format!("{} {}", response_setting_to, desk),
);
}
Some(TrackingArg::Whiteboard) => {
println!("{} {}", response_setting_to, whiteboard);
camera.set_ai_mode(AIMode::Whiteboard).unwrap();
apply_ai_mode(
&camera,
AIMode::Whiteboard,
&format!("{} {}", response_setting_to, whiteboard),
);
}
Some(TrackingArg::Hand) => {
println!("{} {}", response_setting_to, hand);
camera.set_ai_mode(AIMode::Hand).unwrap();
apply_ai_mode(
&camera,
AIMode::Hand,
&format!("{} {}", response_setting_to, hand),
);
}
Some(TrackingArg::Group) => {
println!("{} {}", response_setting_to, group);
camera.set_ai_mode(AIMode::Group).unwrap();
apply_ai_mode(
&camera,
AIMode::Group,
&format!("{} {}", response_setting_to, group),
);
}
None => {
let options = [
Expand Down Expand Up @@ -331,6 +393,10 @@ fn evaluate_tracking_arg(tracking_mode: Option<TrackingArg>, camera: Camera) {
}

fn evaluate_speed_arg(speed: Option<TrackingSpeedArg>, camera: Camera) {
if !camera.supports_tracking_speed() {
unsupported(&t!("shared.errors.unsupported_tracking_speed"));
}

match speed {
Some(TrackingSpeedArg::Standard) => {
println!("{}", t!("cli.tracking_speed.response_to_standard"));
Expand Down Expand Up @@ -398,9 +464,12 @@ fn evaluate_preset_arg(position_id: Option<i8>, camera: Camera) {
position_id = position_id.unwrap()
),
);
camera
if camera
.goto_preset_position(position_id.unwrap() - 1)
.unwrap();
.is_err()
{
unsupported(&t!("shared.errors.unknown_preset_position"));
}
}

fn evaluate_hdr_arg(hdr_mode: Option<OnOffArg>, camera: Camera) {
Expand Down Expand Up @@ -447,6 +516,10 @@ fn evaluate_hdr_arg(hdr_mode: Option<OnOffArg>, camera: Camera) {
fn evaluate_exposure_arg(exposure_mode: Option<ExposureArg>, camera: Camera) {
match exposure_mode {
Some(ExposureArg::Manual) => {
if !camera.supports_manual_exposure() {
unsupported(&t!("shared.errors.unsupported_manual_exposure"));
}

println!("{}", t!("cli.exposure.response_to_manual"));
camera
.set_exposure_mode(tiny4linux::ExposureMode::Manual)
Expand Down
64 changes: 54 additions & 10 deletions src/gui/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,27 @@ struct MainPanel {

impl MainPanel {
fn init_state(window_mode: WindowMode) -> (Self, Task<Message>) {
let camera = Camera::new("OBSBOT Tiny 2").ok();
let camera = Camera::detect().ok();

let status = camera
.as_ref()
.and_then(|c| c.get_status().ok())
.unwrap_or_else(|| tiny4linux::CameraStatus::default());

// the Tiny 4K keeps its tracking mode outside the status buffer, so it has to
// be asked for separately to start with the right button selected (#72)
let tracking = camera
.as_ref()
.and_then(|c| c.get_ai_mode().ok())
.unwrap_or(status.ai_mode);

(
MainPanel {
camera,
main_window_id: None,
window_mode,
awake: status.awake,
tracking: status.ai_mode,
tracking,
tracking_speed: status.speed,
hdr_on: status.hdr_on,
debugging_on: false,
Expand All @@ -80,7 +87,7 @@ impl MainPanel {

fn update(&mut self, message: Message) -> Task<Message> {
if self.camera.is_none() {
self.camera = Camera::new("OBSBOT Tiny 2").ok();
self.camera = Camera::detect().ok();

if self.camera.is_none() {
return Task::none();
Expand Down Expand Up @@ -127,11 +134,21 @@ impl MainPanel {
Task::none()
}
Message::ChangeTracking(tracking_type) => {
if !camera.supports_ai_mode(tracking_type) {
eprintln!("{}", t!("shared.errors.unsupported_tracking_mode"));
return Task::none();
}

self.tracking = tracking_type;
camera.set_ai_mode(tracking_type).unwrap();
Task::none()
}
Message::ChangeTrackingSpeed(new_speed) => {
if !camera.supports_tracking_speed() {
eprintln!("{}", t!("shared.errors.unsupported_tracking_speed"));
return Task::none();
}

self.tracking_speed = new_speed;
camera.set_tracking_speed(new_speed).unwrap();
Task::none()
Expand All @@ -140,7 +157,9 @@ impl MainPanel {
self.tracking = AIMode::NoTracking;
self.awake = SleepMode::Awake;
camera.set_ai_mode(AIMode::NoTracking).unwrap();
camera.goto_preset_position(new_position).unwrap();
if camera.goto_preset_position(new_position).is_err() {
eprintln!("{}", t!("shared.errors.unknown_preset_position"));
}
Task::none()
}
Message::ChangeHDR(new_mode) => {
Expand All @@ -149,6 +168,11 @@ impl MainPanel {
Task::none()
}
Message::ChangeExposure(mode) => {
if mode == ExposureMode::Manual && !camera.supports_manual_exposure() {
eprintln!("{}", t!("shared.errors.unsupported_manual_exposure"));
return Task::none();
}

camera.set_exposure_mode(mode).unwrap();
Task::none()
}
Expand All @@ -167,21 +191,29 @@ impl MainPanel {
Task::none()
}
Message::SendCommand => {
let c = hex::decode(&self.text_input).unwrap();
camera.send_cmd(0x2, 0x6, &c).unwrap();
report_debug_result(
hex::decode(&self.text_input)
.ok()
.map(|c| camera.send_cmd(0x2, 0x6, &c).is_ok()),
);
Task::none()
}
Message::SendCommand02 => {
let c = hex::decode(&self.text_input_02).unwrap();
camera.send_cmd(0x2, 0x2, &c).unwrap();
report_debug_result(
hex::decode(&self.text_input_02)
.ok()
.map(|c| camera.send_cmd(0x2, 0x2, &c).is_ok()),
);
Task::none()
}
Message::HexDump => {
camera.dump().unwrap();
report_debug_result(Some(camera.dump().is_ok()));
Task::none()
}
Message::HexDump02 => {
camera.dump_02().unwrap();
// the Tiny 4K stalls a cold GET_CUR on selector 2, which used to take
// the whole window down through unwrap (#72)
report_debug_result(Some(camera.dump_02().is_ok()));
Task::none()
}
Message::CheckCamera => Task::none(),
Expand Down Expand Up @@ -293,3 +325,15 @@ fn main() -> iced::Result {
.subscription(MainPanel::subscription)
.run_with(move || MainPanel::init_state(start_mode))
}

/// Reports the outcome of a debug-area action instead of taking the window down.
///
/// `None` means the entered hex could not be parsed, `Some(false)` that the camera
/// rejected the request. Neither is a reason to panic in a GUI.
fn report_debug_result(outcome: Option<bool>) {
match outcome {
Some(true) => {}
Some(false) => eprintln!("{}", t!("shared.errors.debug_request_failed")),
None => eprintln!("{}", t!("shared.errors.debug_input_invalid")),
}
}
26 changes: 26 additions & 0 deletions src/gui/styles/button_unsupported.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: EUPL-1.2

use crate::styles::colors::COLOR_BACKGROUND_SECONDARY_DARK;
use iced::widget::button::{Status, Style};
use iced::{Background, Color, Theme};

/// Style for a control the connected camera does not have (#72).
///
/// iced dims a disabled button only slightly, which on the dark theme is easy to
/// miss. This fades it much further instead. It deliberately has no outline: an
/// outlined control reads as interactive, and the active buttons next to it are
/// solid fills, so a border here would give the unavailable control the stronger
/// visual cue of the two.
pub fn button_unsupported(_: &Theme, _: Status) -> Style {
Style {
background: Some(Background::Color(Color {
a: 0.30,
..COLOR_BACKGROUND_SECONDARY_DARK
})),
text_color: Color {
a: 0.28,
..Color::WHITE
},
..Default::default()
}
}
1 change: 1 addition & 0 deletions src/gui/styles/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: EUPL-1.2

pub mod button_non_styled;
pub mod button_unsupported;
pub mod colors;
pub mod general_area_style;
pub mod theme;
Expand Down
Loading