diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85d1407..87524b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: with: persist-credentials: false - run: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --component clippy --no-self-update - - run: cargo clippy --all-targets --locked -- -D warnings + - run: cargo clippy --workspace --all-targets --locked -- -D warnings test: needs: changes @@ -79,7 +79,7 @@ jobs: with: persist-credentials: false - run: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --no-self-update - - run: cargo test --locked + - run: cargo test --workspace --locked # Builds and tests with the oldest Rust that Cargo.toml's rust-version claims. msrv: @@ -94,7 +94,7 @@ jobs: msrv=$(sed -n 's/^rust-version = "\(.*\)"/\1/p' Cargo.toml) test -n "$msrv" rustup toolchain install "$msrv" --profile minimal --no-self-update - cargo "+$msrv" test --locked + cargo "+$msrv" test --workspace --locked # The one check the main ruleset requires. Code changes need fmt, clippy, test and msrv to # succeed; docs-only changes need them skipped. Anything else fails. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4db498c..f4d928d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,7 @@ on: - scripts/** - Cargo.toml - Cargo.lock + - crates/*/Cargo.toml - README.md - CHANGELOG.md - LICENSE @@ -115,13 +116,13 @@ jobs: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --component rustfmt --component clippy --no-self-update - run: cargo fmt --check - - run: cargo clippy --all-targets --locked -- -D warnings - - run: cargo test --locked + - run: cargo clippy --workspace --all-targets --locked -- -D warnings + - run: cargo test --workspace --locked - run: | msrv=$(sed -n 's/^rust-version = "\(.*\)"/\1/p' Cargo.toml) test -n "$msrv" rustup toolchain install "$msrv" --profile minimal --no-self-update - cargo "+$msrv" test --locked + cargo "+$msrv" test --workspace --locked # One job per package format and architecture: a failure reruns only that job. binary: @@ -200,7 +201,7 @@ jobs: mkdir dist # no rebuild: package the binary from the binary job (artifacts drop the mode) chmod +x target/release/obsbot4linux - cargo deb --no-build --deb-revision "" -o "dist/obsbot4linux_${ver}_$DEB.deb" + cargo deb -p obsbot4linux --no-build --deb-revision "" -o "dist/obsbot4linux_${ver}_$DEB.deb" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: dist-deb-${{ matrix.arch }} @@ -237,7 +238,7 @@ jobs: ver=$VER mkdir dist chmod +x target/release/obsbot4linux - cargo generate-rpm -o "dist/obsbot4linux-$ver-1.$ARCH.rpm" + cargo generate-rpm -p crates/obsbot4linux -o "dist/obsbot4linux-$ver-1.$ARCH.rpm" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: dist-rpm-${{ matrix.arch }} @@ -276,10 +277,60 @@ jobs: path: dist/* if-no-files-found: error + # Lists the files of every package and compares the list with packaging/contents.txt, so that + # a change to what a package holds shows up in review. A release needs it too. + contents: + needs: [version, binary, deb, rpm, arch] + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + path: dist + pattern: dist-* + merge-multiple: true + - run: sudo apt-get install -y --no-install-recommends libarchive-tools + # [profile.release] strip = true must reach every package (cargo ignores a member + # manifest's profile, and the file list alone would not show it) + - name: The packaged binaries are stripped + run: | + for pkg in dist/obsbot4linux*; do + case $pkg in + *.deb) bsdtar -xOf "$pkg" 'data.tar*' > payload ;; + *) cp "$pkg" payload ;; + esac + bin=$(bsdtar -tf payload | grep -E '(usr/bin|-linux)/obsbot4linux$') + bsdtar -xOf payload "$bin" > obsbot4linux.bin + info=$(file -b obsbot4linux.bin) + echo "$pkg: $info" + case $info in + *', stripped') ;; + *) echo "::error::$pkg ships an unstripped binary"; exit 1 ;; + esac + done + rm -f payload obsbot4linux.bin + - name: Compare the package contents + env: + VER: ${{ needs.version.outputs.ver }} + run: | + scripts/package-contents.sh dist "$VER" > contents.txt + cat contents.txt + diff -u packaging/contents.txt contents.txt + # the list itself, also when it differs, to update packaging/contents.txt from + - if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: package-contents + path: contents.txt + if-no-files-found: error + # Tags only: a pull request builds the packages above and publishes nothing. release: if: startsWith(github.ref, 'refs/tags/') - needs: [binary, deb, rpm, arch] + needs: [binary, deb, rpm, arch, contents] runs-on: ubuntu-22.04 timeout-minutes: 15 permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3217627..d82229f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to OBSBot4Linux. The format follows - The repository moved to github.com/hellwand/obsbot4linux on 2026-11-12; the old address redirects (the link change itself landed in #109). - README and package text now say "most settings OBSBOT Center offers" and link `docs/center-parity.md` for the gaps; they also state the glibc 2.34 floor, how to switch to the socket units while the widget's daemon runs, to restart the daemon after an upgrade (socket users: `systemctl --user restart obsbot4linux.service`; a daemon started by `watch` ends with the shell's unit), that `zoom F --speed S` and `zoom stop` leave the AI mode alone, the `device busy` window of the on-air probe on uvcvideo up to 6.16, and the order of auto and manual image controls (#121). +- The source is a cargo workspace now: `crates/obsbot-device` holds the camera access, + `crates/obsbot4linux` the CLI and the daemon. The program, its output and the packages do not + change. ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c54bd13..64fc003 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,8 @@ Every change goes through an issue and a pull request, also the maintainer's: 1. Open an issue: what is wrong or missing, how to reproduce it, which camera and firmware. 2. Open a pull request against `main` whose description starts with `Fixes #`. -3. CI must be green (`cargo fmt --check`, `cargo clippy --all-targets --locked -- -D warnings`, - `cargo test`, and `cargo test` again with the `rust-version` from `Cargo.toml`). +3. CI must be green (`cargo fmt --check`, `cargo clippy --workspace --all-targets --locked -- -D warnings`, + `cargo test --workspace`, and `cargo test --workspace` again with the `rust-version` from `Cargo.toml`). Changes that only touch docs (Markdown files, `LICENSE`, `docs/`) skip those four; the `required` check still reports. No changelog entry is enforced per pull request; the release tag needs its `CHANGELOG.md` section. diff --git a/Cargo.lock b/Cargo.lock index 32c3f13..6ec3088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,7 +165,7 @@ dependencies = [ ] [[package]] -name = "obsbot4linux" +name = "obsbot-device" version = "0.13.2" dependencies = [ "clap", @@ -176,6 +176,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "obsbot4linux" +version = "0.13.2" +dependencies = [ + "clap", + "nix", + "obsbot-device", + "serde", + "serde_json", +] + [[package]] name = "once_cell_polyfill" version = "1.70.2" diff --git a/Cargo.toml b/Cargo.toml index a71a6e4..c853df5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,57 +1,25 @@ -[package] -name = "obsbot4linux" +[workspace] +resolver = "3" +members = ["crates/obsbot-device", "crates/obsbot4linux"] + +[workspace.package] version = "0.13.2" edition = "2024" rust-version = "1.88" -description = "Control OBSBOT webcams on Linux from the console and the DMS widget" authors = ["Thomas Kroll"] license = "GPL-3.0-or-later" repository = "https://github.com/hellwand/obsbot4linux" homepage = "https://github.com/hellwand/obsbot4linux" -readme = "README.md" publish = false -[lib] -name = "obsbot4linux" -path = "src/lib.rs" - -[[bin]] -name = "obsbot4linux" -path = "src/main.rs" - -[dependencies] +[workspace.dependencies] +obsbot-device = { path = "crates/obsbot-device" } clap = { version = "4", features = ["derive"] } nix = { version = "0.31", features = ["fs", "ioctl"] } serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" +hex = "0.4" [profile.release] strip = true - -[dev-dependencies] -hex = "0.4" - -[package.metadata.deb] -maintainer = "Thomas Kroll " -section = "video" -extended-description = "Most settings OBSBOT Center offers, from the console and from the DMS bar widget.\nWhat is missing: https://github.com/hellwand/obsbot4linux/blob/main/docs/center-parity.md\nThe obsbot4linux daemon (systemd user socket) owns the camera state." -depends = "$auto" -license-file = ["LICENSE", "0"] -assets = [ - ["target/release/obsbot4linux", "usr/bin/", "755"], - ["packaging/systemd/obsbot4linux.service", "usr/lib/systemd/user/", "644"], - ["packaging/systemd/obsbot4linux.socket", "usr/lib/systemd/user/", "644"], - ["README.md", "usr/share/doc/obsbot4linux/", "644"], - ["CHANGELOG.md", "usr/share/doc/obsbot4linux/", "644"], -] - -[package.metadata.generate-rpm] -assets = [ - { source = "target/release/obsbot4linux", dest = "/usr/bin/obsbot4linux", mode = "755" }, - { source = "packaging/systemd/obsbot4linux.service", dest = "/usr/lib/systemd/user/obsbot4linux.service", mode = "644" }, - { source = "packaging/systemd/obsbot4linux.socket", dest = "/usr/lib/systemd/user/obsbot4linux.socket", mode = "644" }, - { source = "README.md", dest = "/usr/share/doc/obsbot4linux/README.md", mode = "644", doc = true }, - { source = "CHANGELOG.md", dest = "/usr/share/doc/obsbot4linux/CHANGELOG.md", mode = "644", doc = true }, - { source = "LICENSE", dest = "/usr/share/licenses/obsbot4linux/LICENSE", mode = "644" }, -] diff --git a/README.md b/README.md index 05c120d..7f93e6c 100644 --- a/README.md +++ b/README.md @@ -407,7 +407,7 @@ it does not arm the flag but consumes one that a recall or `0x6604` armed earlie goes Static on arrival; afterwards tracking may stay suspended, since the view's actions name no AI mode, as with `startup go`. Not seen so far: tracking stayed on after a `zone view go` in AI normal. The flag part is still to be confirmed. -Adding a model: an entry in `MODELS` in `src/models.rs` (name, USB id, by-id and card name +Adding a model: an entry in `MODELS` in `crates/obsbot-device/src/registry.rs` (name, USB id, by-id and card name patterns) makes it detected. Its commands and status decoding differ per camera and today exist for the Tiny 2 Lite only; the plan is one module per model behind the same commands, once a camera is at hand to capture and test (see [CONTRIBUTING.md](CONTRIBUTING.md)). diff --git a/crates/obsbot-device/Cargo.toml b/crates/obsbot-device/Cargo.toml new file mode 100644 index 0000000..747c50b --- /dev/null +++ b/crates/obsbot-device/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "obsbot-device" +description = "Camera access for obsbot4linux: transport, frame codec and the camera models" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish.workspace = true + +[dependencies] +clap.workspace = true +nix.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true + +[dev-dependencies] +hex.workspace = true diff --git a/src/camera.rs b/crates/obsbot-device/src/camera.rs similarity index 100% rename from src/camera.rs rename to crates/obsbot-device/src/camera.rs diff --git a/crates/obsbot-device/src/enumeration.rs b/crates/obsbot-device/src/enumeration.rs new file mode 100644 index 0000000..2271f0b --- /dev/null +++ b/crates/obsbot-device/src/enumeration.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Derived from Tiny4Linux (EUPL-1.2), used under EUPL-1.2 Article 5. + +//! Finding a supported camera without talking to it: `/dev/v4l/by-id` links first, the V4L2 +//! card name as fallback. + +use crate::registry::{self, Model}; +use nix::ioctl_read; +use std::fs::{self, File}; +use std::os::fd::AsRawFd; +use std::path::PathBuf; + +const V4L2_CAP_META_CAPTURE: u32 = 0x0080_0000; + +#[repr(C)] +#[derive(Default)] +struct V4l2Capability { + driver: [u8; 16], + card: [u8; 32], + bus_info: [u8; 32], + version: u32, + capabilities: u32, + device_caps: u32, + reserved: [u32; 3], +} + +ioctl_read!(vidioc_querycap, b'V', 0, V4l2Capability); + +/// A camera found by enumeration, before any command is sent to it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Detected { + pub model: &'static Model, + /// The V4L2 node to stream from: the stable `/dev/v4l/by-id/...-video-index0` link when + /// there is one, else `/dev/videoN`. + pub device: PathBuf, +} + +fn card_name(file: &File) -> Option<(String, u32)> { + let mut cap = V4l2Capability::default(); + unsafe { vidioc_querycap(file.as_raw_fd(), &mut cap) }.ok()?; + let end = cap + .card + .iter() + .position(|b| *b == 0) + .unwrap_or(cap.card.len()); + Some(( + String::from_utf8_lossy(&cap.card[..end]).into_owned(), + cap.device_caps, + )) +} + +/// Finds a supported camera without talking to it: by-id symlink first, V4L2 card name +/// (VIDIOC_QUERYCAP, a read-only ioctl) as fallback. +pub fn detect() -> Option { + let by_id = fs::read_dir("/dev/v4l/by-id") + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .collect(); + if let Some(detected) = first_by_id(by_id) { + return Some(detected); + } + + let mut paths: Vec = fs::read_dir("/dev") + .ok()? + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .is_some_and(|n| n.to_string_lossy().starts_with("video")) + }) + .collect(); + paths.sort(); + for path in paths { + if let Ok(file) = File::open(&path) + && let Some((card, caps)) = card_name(&file) + && caps & V4L2_CAP_META_CAPTURE == 0 + && let Some(model) = registry::from_card(&card) + { + return Some(Detected { + model, + device: path, + }); + } + } + None +} + +/// The first supported camera among the by-id links, sorted: `read_dir` order is unspecified, +/// so with two cameras the controlled one could change between runs. +fn first_by_id(mut links: Vec) -> Option { + links.sort(); + links.into_iter().find_map(|device| { + let model = registry::from_by_id(&device.file_name()?.to_string_lossy())?; + Some(Detected { model, device }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn by_id_detection_does_not_depend_on_directory_order() { + let link = |serial: &str| { + PathBuf::from(format!( + "/dev/v4l/by-id/usb-Remo_Tech_Co.__Ltd._OBSBOT_Tiny_2_Lite_{serial}-video-index0" + )) + }; + let other = PathBuf::from("/dev/v4l/by-id/usb-Logitech_BRIO-video-index0"); + let picked = first_by_id(vec![other.clone(), link("B"), link("A")]).unwrap(); + assert_eq!(picked.device, link("A")); + let picked = first_by_id(vec![link("A"), link("B"), other]).unwrap(); + assert_eq!(picked.device, link("A")); + assert_eq!(first_by_id(Vec::new()), None); + } +} diff --git a/src/error.rs b/crates/obsbot-device/src/error.rs similarity index 100% rename from src/error.rs rename to crates/obsbot-device/src/error.rs diff --git a/src/frame.rs b/crates/obsbot-device/src/family/frame.rs similarity index 100% rename from src/frame.rs rename to crates/obsbot-device/src/family/frame.rs diff --git a/crates/obsbot-device/src/family/mod.rs b/crates/obsbot-device/src/family/mod.rs new file mode 100644 index 0000000..b025515 --- /dev/null +++ b/crates/obsbot-device/src/family/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Building blocks that are not tied to one camera model: the frame codec and the on-air probe. + +pub mod frame; +pub mod streaming; diff --git a/src/streaming.rs b/crates/obsbot-device/src/family/streaming.rs similarity index 100% rename from src/streaming.rs rename to crates/obsbot-device/src/family/streaming.rs diff --git a/crates/obsbot-device/src/lib.rs b/crates/obsbot-device/src/lib.rs new file mode 100644 index 0000000..a28deb9 --- /dev/null +++ b/crates/obsbot-device/src/lib.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Control library for the OBSBOT Tiny 2 Lite. + +pub mod camera; +pub mod error; +pub mod state; + +pub mod family; +pub use family::{frame, streaming}; + +pub mod enumeration; +pub mod registry; +pub mod transport; +/// The 0.13 path of the device module, until Task 7 of the groundwork plan removes it. +pub mod device { + pub use crate::enumeration::{Detected, detect}; + pub use crate::transport::uvc::{CtrlRange, Device}; +} + +pub mod models; +pub use models::tiny2lite::{commands, presets, status}; diff --git a/crates/obsbot-device/src/models/mod.rs b/crates/obsbot-device/src/models/mod.rs new file mode 100644 index 0000000..af6caea --- /dev/null +++ b/crates/obsbot-device/src/models/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The camera models, one module each. + +pub mod tiny2lite; diff --git a/src/commands.rs b/crates/obsbot-device/src/models/tiny2lite/commands.rs similarity index 100% rename from src/commands.rs rename to crates/obsbot-device/src/models/tiny2lite/commands.rs diff --git a/crates/obsbot-device/src/models/tiny2lite/mod.rs b/crates/obsbot-device/src/models/tiny2lite/mod.rs new file mode 100644 index 0000000..fcf999b --- /dev/null +++ b/crates/obsbot-device/src/models/tiny2lite/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! The OBSBOT Tiny 2 Lite (USB 3564:fef9). + +pub mod commands; +pub mod presets; +pub mod status; diff --git a/src/presets.rs b/crates/obsbot-device/src/models/tiny2lite/presets.rs similarity index 100% rename from src/presets.rs rename to crates/obsbot-device/src/models/tiny2lite/presets.rs diff --git a/src/status.rs b/crates/obsbot-device/src/models/tiny2lite/status.rs similarity index 100% rename from src/status.rs rename to crates/obsbot-device/src/models/tiny2lite/status.rs diff --git a/src/models.rs b/crates/obsbot-device/src/registry.rs similarity index 100% rename from src/models.rs rename to crates/obsbot-device/src/registry.rs diff --git a/src/state.rs b/crates/obsbot-device/src/state.rs similarity index 100% rename from src/state.rs rename to crates/obsbot-device/src/state.rs diff --git a/crates/obsbot-device/src/transport/mod.rs b/crates/obsbot-device/src/transport/mod.rs new file mode 100644 index 0000000..c013ef5 --- /dev/null +++ b/crates/obsbot-device/src/transport/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Access to the camera's USB device: extension-unit queries and V4L2 controls. + +pub mod uvc; diff --git a/src/device.rs b/crates/obsbot-device/src/transport/uvc.rs similarity index 68% rename from src/device.rs rename to crates/obsbot-device/src/transport/uvc.rs index 6f427d0..2384621 100644 --- a/src/device.rs +++ b/crates/obsbot-device/src/transport/uvc.rs @@ -9,14 +9,13 @@ //! calls) take turns: the vendor unit has one answer buffer, and answers are matched by sequence //! number only. flock is advisory; streaming applications do not take it and are not held up. +use crate::enumeration::{Detected, detect}; use crate::error::{Error, Result}; -use crate::models::{self, Model}; use nix::errno::Errno; use nix::fcntl::{Flock, FlockArg}; -use nix::{ioctl_read, ioctl_readwrite}; -use std::fs::{self, File}; +use nix::ioctl_readwrite; +use std::fs::File; use std::os::fd::AsRawFd; -use std::path::PathBuf; use std::time::{Duration, Instant}; /// How long `Device::open` waits for another obsbot4linux process to release the camera. @@ -29,7 +28,6 @@ const UVC_GET_LEN: u8 = 0x85; /// Size of every vendor buffer on unit 2. pub const XU_SIZE: usize = 60; -const V4L2_CAP_META_CAPTURE: u32 = 0x0080_0000; const V4L2_CTRL_FLAG_DISABLED: u32 = 0x0001; #[repr(C)] @@ -41,18 +39,6 @@ struct UvcXuControlQuery { data: *mut u8, } -#[repr(C)] -#[derive(Default)] -struct V4l2Capability { - driver: [u8; 16], - card: [u8; 32], - bus_info: [u8; 32], - version: u32, - capabilities: u32, - device_caps: u32, - reserved: [u32; 3], -} - #[repr(C)] #[derive(Default)] struct V4l2Control { @@ -75,7 +61,6 @@ struct V4l2QueryCtrl { } ioctl_readwrite!(uvcioc_ctrl_query, b'u', 0x21, UvcXuControlQuery); -ioctl_read!(vidioc_querycap, b'V', 0, V4l2Capability); ioctl_readwrite!(vidioc_g_ctrl, b'V', 27, V4l2Control); ioctl_readwrite!(vidioc_s_ctrl, b'V', 28, V4l2Control); ioctl_readwrite!(vidioc_queryctrl, b'V', 36, V4l2QueryCtrl); @@ -89,15 +74,6 @@ pub struct CtrlRange { pub default: i32, } -/// A camera found by enumeration, before any command is sent to it. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Detected { - pub model: &'static Model, - /// The V4L2 node to stream from: the stable `/dev/v4l/by-id/...-video-index0` link when - /// there is one, else `/dev/videoN`. - pub device: PathBuf, -} - pub struct Device { file: Flock, detected: Detected, @@ -119,68 +95,6 @@ fn lock_exclusive(mut file: File, wait: Duration) -> Result> { } } -fn card_name(file: &File) -> Option<(String, u32)> { - let mut cap = V4l2Capability::default(); - unsafe { vidioc_querycap(file.as_raw_fd(), &mut cap) }.ok()?; - let end = cap - .card - .iter() - .position(|b| *b == 0) - .unwrap_or(cap.card.len()); - Some(( - String::from_utf8_lossy(&cap.card[..end]).into_owned(), - cap.device_caps, - )) -} - -/// Finds a supported camera without talking to it: by-id symlink first, V4L2 card name -/// (VIDIOC_QUERYCAP, a read-only ioctl) as fallback. -pub fn detect() -> Option { - let by_id = fs::read_dir("/dev/v4l/by-id") - .into_iter() - .flatten() - .flatten() - .map(|e| e.path()) - .collect(); - if let Some(detected) = first_by_id(by_id) { - return Some(detected); - } - - let mut paths: Vec = fs::read_dir("/dev") - .ok()? - .flatten() - .map(|e| e.path()) - .filter(|p| { - p.file_name() - .is_some_and(|n| n.to_string_lossy().starts_with("video")) - }) - .collect(); - paths.sort(); - for path in paths { - if let Ok(file) = File::open(&path) - && let Some((card, caps)) = card_name(&file) - && caps & V4L2_CAP_META_CAPTURE == 0 - && let Some(model) = models::from_card(&card) - { - return Some(Detected { - model, - device: path, - }); - } - } - None -} - -/// The first supported camera among the by-id links, sorted: `read_dir` order is unspecified, -/// so with two cameras the controlled one could change between runs. -fn first_by_id(mut links: Vec) -> Option { - links.sort(); - links.into_iter().find_map(|device| { - let model = models::from_by_id(&device.file_name()?.to_string_lossy())?; - Some(Detected { model, device }) - }) -} - /// The kernel fails GET_CUR and SET_CUR with ENOBUFS unless the buffer is exactly the /// selector's length, and both always pass [`XU_SIZE`] bytes. fn check_len(selector: u8, len: usize) -> Result<()> { @@ -276,6 +190,7 @@ impl Device { #[cfg(test)] mod tests { use super::*; + use std::fs; #[test] fn a_second_handle_waits_for_the_first() { @@ -308,19 +223,4 @@ mod tests { assert!(matches!(check_len(2, 32), Err(Error::Unsupported(_)))); assert!(matches!(check_len(2, 64), Err(Error::Unsupported(_)))); } - - #[test] - fn by_id_detection_does_not_depend_on_directory_order() { - let link = |serial: &str| { - PathBuf::from(format!( - "/dev/v4l/by-id/usb-Remo_Tech_Co.__Ltd._OBSBOT_Tiny_2_Lite_{serial}-video-index0" - )) - }; - let other = PathBuf::from("/dev/v4l/by-id/usb-Logitech_BRIO-video-index0"); - let picked = first_by_id(vec![other.clone(), link("B"), link("A")]).unwrap(); - assert_eq!(picked.device, link("A")); - let picked = first_by_id(vec![link("A"), link("B"), other]).unwrap(); - assert_eq!(picked.device, link("A")); - assert_eq!(first_by_id(Vec::new()), None); - } } diff --git a/crates/obsbot-device/tests/byte_guard.rs b/crates/obsbot-device/tests/byte_guard.rs new file mode 100644 index 0000000..3eb07c1 --- /dev/null +++ b/crates/obsbot-device/tests/byte_guard.rs @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Byte guard of the groundwork stage (rebuild plan, stage G): every frame and short command the +//! builders emit for a fixed list of calls, one line per call, compared with +//! `tests/fixtures/byte-guard.txt`. That file was written from the 0.13 code before the +//! workspace move, so a move that changes one byte that would go to the camera fails here. +//! +//! `BYTE_GUARD_BLESS=1 cargo test --test byte_guard` writes the file: once when this test is +//! added, and on `main` in a change that alters what a builder sends on purpose (that change +//! lists the changed lines). A move never writes it; every other run compares. + +use clap::ValueEnum; +use obsbot_device::commands::{self as cmd, *}; +use obsbot_device::frame::{self, FRAME_SIZE}; +use obsbot_device::presets::{self, Pose, StartupAi}; +use std::fmt::Display; + +const FIXTURE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/byte-guard.txt"); +const SEQ: u16 = 0x1234; +const SEQS: [u16; 4] = [SEQ, SEQ + 1, SEQ + 2, SEQ + 3]; + +/// How one builder result is written into the fixture. +trait Show { + fn show(&self) -> String; +} + +impl Show for [u8; N] { + fn show(&self) -> String { + hex::encode(self) + } +} + +impl Show for [[u8; N]; M] { + fn show(&self) -> String { + self.iter().map(hex::encode).collect::>().join(" ") + } +} + +impl Show for u8 { + fn show(&self) -> String { + self.to_string() + } +} + +impl Show for u16 { + fn show(&self) -> String { + format!("{self:#06x}") + } +} + +impl Show for bool { + fn show(&self) -> String { + self.to_string() + } +} + +impl Show for Option { + fn show(&self) -> String { + self.as_ref().map_or("none".into(), Show::show) + } +} + +impl Show for Result { + fn show(&self) -> String { + match self { + Ok(v) => v.show(), + Err(e) => format!("error: {e}"), + } + } +} + +fn rec(out: &mut Vec, label: String, value: impl Show) { + out.push(format!("{label} = {}", value.show())); +} + +/// Poses at and beyond every bound the pose builders check, and an asymmetric one. +const POSES: [(f32, f32, f32); 10] = [ + (10.5, -5.25, 2.0), + (0.0, 0.0, 1.0), + (-150.0, -70.0, 1.0), + (150.0, 90.0, 4.0), + (150.1, 0.0, 1.0), + (0.0, 90.5, 1.0), + (0.0, -70.5, 1.0), + (0.0, 0.0, 4.01), + (0.0, 0.0, 0.99), + (f32::NAN, 0.0, 1.0), +]; + +fn poses() -> impl Iterator { + POSES + .into_iter() + .map(|(pan, tilt, zoom)| Pose { pan, tilt, zoom }) +} + +/// Area ranges (min, max) in the camera's angles, at and beyond the bounds, and wrong ways round. +const RANGES: [(f32, f32); 11] = [ + (-45.0, 45.0), + (-150.0, 150.0), + (-150.5, 0.0), + (0.0, 150.5), + (-90.0, 70.0), + (-90.5, 0.0), + (0.0, 70.5), + (-30.0, 10.0), + (10.0, 10.0), + (10.0, -10.0), + (f32::NAN, 1.0), +]; + +type Short3 = fn(bool) -> [u8; 3]; +type Framed = fn(bool, u16) -> [u8; FRAME_SIZE]; +type SeqOnly = fn(u16) -> [u8; FRAME_SIZE]; + +fn record() -> Vec { + let mut out = Vec::new(); + let o = &mut out; + + // frame codec + for seq in [0x0000, 0x0001, SEQ, 0xffff] { + let q = frame::query_frame(0x04, 0x0104, seq); + rec( + o, + format!("frame::query_frame(0x04, 0x0104, {seq:#06x})"), + q, + ); + let f = frame::frame(0x02, 0xa0c2, &[1, 0, 0, 0], seq); + rec( + o, + format!("frame::frame(0x02, 0xa0c2, [1 0 0 0], {seq:#06x})"), + f, + ); + let w = frame::query_frame_with(0x04, 0x1504, &[7, 0, 0, 0], seq); + rec( + o, + format!("frame::query_frame_with(0x04, 0x1504, [7 0 0 0], {seq:#06x})"), + w, + ); + } + let full = frame::frame(0x04, 0x3e44, &[0xa5; frame::MAX_DATA], SEQ); + rec( + o, + "frame::frame(0x04, 0x3e44, [0xa5; MAX_DATA])".into(), + full, + ); + rec( + o, + "frame::crc16_usb(\"123456789\")".into(), + frame::crc16_usb(b"123456789"), + ); + + // short commands (selector 6) + let short3: [(&str, Short3); 6] = [ + ("hdr", cmd::hdr), + ("led_blink", cmd::led_blink), + ("mirror", cmd::mirror), + ("mic_in_sleep", cmd::mic_in_sleep), + ("auto_gain", cmd::auto_gain), + ("microphone", cmd::microphone), + ]; + for (name, f) in short3 { + for on in [false, true] { + rec(o, format!("cmd::{name}({on})"), f(on)); + } + } + for on in [false, true] { + rec( + o, + format!("cmd::sleep_background_mirror({on})"), + cmd::sleep_background_mirror(on), + ); + } + for m in AiMode::value_variants() { + rec(o, format!("cmd::ai_mode({m:?})"), cmd::ai_mode(*m)); + rec( + o, + format!("cmd::default_work_mode({m:?})"), + cmd::default_work_mode(*m), + ); + } + for s in [i16::MIN, -600, -1, 0, 1, 600, i16::MAX] { + rec(o, format!("cmd::auto_sleep({s})"), cmd::auto_sleep(s)); + } + for s in [0u16, 1, 600, 32767, 32768, u16::MAX] { + rec( + o, + format!("cmd::auto_sleep_off({s})"), + cmd::auto_sleep_off(s), + ); + } + for b in SleepBackground::value_variants() { + rec( + o, + format!("cmd::sleep_background({b:?})"), + cmd::sleep_background(*b), + ); + } + for b in 0..=4u8 { + rec(o, format!("cmd::led({b})"), cmd::led(b)); + } + for p in [0u8, 1, 50, 100, 101, 255] { + rec(o, format!("cmd::wake_zoom({p})"), cmd::wake_zoom(p)); + } + for f in [0.99f32, 1.0, 1.5, 2.5, 4.0, 4.01, f32::NAN] { + rec( + o, + format!("cmd::wake_zoom_percent({f})"), + cmd::wake_zoom_percent(f), + ); + } + for v in FieldOfView::value_variants() { + rec( + o, + format!("cmd::field_of_view({v:?})"), + cmd::field_of_view(*v), + ); + } + for v in NoiseReduction::value_variants() { + rec( + o, + format!("cmd::noise_reduction({v:?})"), + cmd::noise_reduction(*v), + ); + } + for v in PickupDistance::value_variants() { + rec( + o, + format!("cmd::pickup_distance({v:?})"), + cmd::pickup_distance(*v), + ); + } + for v in ExposureMode::value_variants() { + rec( + o, + format!("cmd::exposure_mode_select({v:?})"), + cmd::exposure_mode_select(*v), + ); + } + rec(o, "cmd::startup_go()".into(), cmd::startup_go()); + + // framed commands (selector 2) + let framed: [(&str, Framed); 9] = [ + ("invert", cmd::invert), + ("zone_tracking", cmd::zone_tracking), + ("hand_pan", cmd::hand_pan), + ("hand_tilt", cmd::hand_tilt), + ("hand_gimbal", cmd::hand_gimbal), + ("autofocus_mode", cmd::autofocus_mode), + ("zone_follow", cmd::zone_follow), + ("sleep", cmd::sleep), + ("auto_zoom", cmd::auto_zoom), + ]; + for (name, f) in framed { + for on in [false, true] { + rec(o, format!("cmd::{name}({on})"), f(on, SEQ)); + } + } + let seq_only: [(&str, SeqOnly); 6] = [ + ("zone_return_go", cmd::zone_return_go), + ("hand_return_go", cmd::hand_return_go), + ("hand_return_reset", cmd::hand_return_reset), + ("factory_reset", cmd::factory_reset), + ("gimbal_stop", cmd::gimbal_stop), + ("zoom_stop", cmd::zoom_stop), + ]; + for (name, f) in seq_only { + rec(o, format!("cmd::{name}()"), f(SEQ)); + } + for v in TrackingSpeed::value_variants() { + rec( + o, + format!("cmd::tracking_speed({v:?})"), + cmd::tracking_speed(*v, SEQ), + ); + } + for v in SpeedMode::value_variants() { + rec( + o, + format!("cmd::speed_mode({v:?})"), + cmd::speed_mode(*v, SEQ), + ); + } + for v in FramingSize::value_variants() { + rec( + o, + format!("cmd::framing_size({v:?})"), + cmd::framing_size(*v, SEQ), + ); + } + for v in AutoZoomSpeed::value_variants() { + rec( + o, + format!("cmd::auto_zoom_speed({v:?})"), + cmd::auto_zoom_speed(*v, SEQ), + ); + } + for v in PartMode::value_variants() { + rec(o, format!("cmd::part_mode({v:?})"), cmd::part_mode(*v, SEQ)); + } + for v in HandSide::value_variants() { + rec(o, format!("cmd::hand_side({v:?})"), cmd::hand_side(*v, SEQ)); + } + for v in AutofocusMode::value_variants() { + rec( + o, + format!("cmd::autofocus_mode_select({v:?})"), + cmd::autofocus_mode_select(*v, SEQ), + ); + } + for g in Gesture::value_variants() { + for on in [false, true] { + rec( + o, + format!("cmd::gesture({g:?}, {on})"), + cmd::gesture(*g, on, SEQ), + ); + } + } + for r in [0.99f32, 1.0, 2.5, 4.0, 4.01, f32::NAN] { + rec( + o, + format!("cmd::gesture_zoom_ratio({r})"), + cmd::gesture_zoom_ratio(r, SEQ), + ); + } + for s in [-1.0f32, -0.0, 0.0, 2.5, 600.0, f32::INFINITY, f32::NAN] { + rec( + o, + format!("cmd::hand_return_after({s})"), + cmd::hand_return_after(s, SEQ), + ); + } + for v in [0u8, 1, 50, 100, 101, 255] { + rec( + o, + format!("cmd::audio_volume({v})"), + cmd::audio_volume(v, SEQ), + ); + } + for x in [-0.01f32, 0.0, 0.25, 1.0, 1.01] { + for y in [-0.01f32, 0.0, 0.75, 1.0, 1.01] { + rec( + o, + format!("cmd::focus_point({x}, {y})"), + cmd::focus_point(x, y, SEQ), + ); + rec( + o, + format!("cmd::view_center({x}, {y})"), + cmd::view_center(x, y, SEQ), + ); + } + } + for r in RANGES { + for (yaw, pitch) in [(r, (-30.0, 10.0)), ((-45.0, 45.0), r)] { + rec( + o, + format!("cmd::zone_area({yaw:?}, {pitch:?})"), + cmd::zone_area(yaw, pitch, SEQS), + ); + rec( + o, + format!("cmd::hand_area({yaw:?}, {pitch:?})"), + cmd::hand_area(yaw, pitch, SEQS), + ); + } + } + rec( + o, + "cmd::hand_area_reset()".into(), + cmd::hand_area_reset(SEQS), + ); + let axes = [ + None, + Some(-150.0f32), + Some(-70.0), + Some(0.0), + Some(12.5), + Some(90.0), + Some(150.0), + Some(150.5), + Some(-90.5), + ]; + for yaw in axes { + for pitch in axes { + rec( + o, + format!("cmd::gimbal_pose({yaw:?}, {pitch:?})"), + cmd::gimbal_pose(yaw, pitch, SEQ), + ); + } + } + for (yaw, pitch) in [ + (None, None), + (Some(12.5f32), None), + (None, Some(-5.0f32)), + (Some(12.5), Some(-5.0)), + (Some(150.5), Some(0.0)), + ] { + for speed in [0.99f32, 1.0, 45.0, 180.0, 180.5, f32::NAN] { + let f = cmd::gimbal_pose_speed(yaw, pitch, speed, SEQ); + rec( + o, + format!("cmd::gimbal_pose_speed({yaw:?}, {pitch:?}, {speed})"), + f, + ); + } + } + for pan in [-178.5f32, -178.0, -0.0, 0.0, 12.5, 178.0, 178.5, f32::NAN] { + for tilt in [-178.5f32, -178.0, -0.0, 0.0, 5.0, 178.0, 178.5] { + let f = cmd::gimbal_rate(pan, tilt, false, false, SEQ); + rec( + o, + format!("cmd::gimbal_rate({pan}, {tilt}, false, false)"), + f, + ); + } + } + for (mirror, invert) in [(true, false), (false, true), (true, true)] { + let f = cmd::gimbal_rate(12.5, 5.0, mirror, invert, SEQ); + rec( + o, + format!("cmd::gimbal_rate(12.5, 5, {mirror}, {invert})"), + f, + ); + } + for speed in [0i32, 1, 10, 30, 50, i32::MAX] { + for ratio in [0.99f32, 1.0, 2.5, 4.0, 4.01] { + rec( + o, + format!("cmd::zoom_to({speed}, {ratio})"), + cmd::zoom_to(speed, ratio, SEQ), + ); + } + } + for vertical in [false, true] { + for f in [-0.17f32, -0.16, -0.0, 0.0, 0.1, 0.16, 0.17, f32::NAN] { + let b = cmd::framing_offset(vertical, f, SEQ); + rec(o, format!("cmd::framing_offset({vertical}, {f})"), b); + } + } + for pan in [-150.5f32, -150.0, 0.0, 150.0, 150.5] { + for tilt in [-70.5f32, -70.0, 0.0, 90.0, 90.5] { + rec( + o, + format!("cmd::gimbal_angles_ok({pan}, {tilt})"), + cmd::gimbal_angles_ok(pan, tilt), + ); + } + } + + // presets, zone views, start pose and return positions + let names = [ + "", + "Desk", + "Bereich1", + "äöü", + "12345678901234567890123456789012", + "123456789012345678901234567890123", + ]; + for slot in [0u8, 1, 2, 3, 255] { + for p in poses() { + rec( + o, + format!("presets::create({slot}, {p:?})"), + presets::create(slot, p, SEQ), + ); + rec( + o, + format!("presets::update({slot}, {p:?})"), + presets::update(slot, p, SEQ), + ); + } + for name in names { + rec( + o, + format!("presets::rename({slot}, {name:?})"), + presets::rename(slot, name, SEQ), + ); + } + rec( + o, + format!("presets::delete({slot})"), + presets::delete(slot, SEQ, SEQ + 1), + ); + rec( + o, + format!("presets::recall({slot})"), + presets::recall(slot, SEQ), + ); + } + for id in [0u32, 1, 7, 255, u32::MAX] { + for p in poses() { + rec( + o, + format!("presets::view_add({id}, {p:?})"), + presets::view_add(id, p, SEQ), + ); + } + for name in names { + rec( + o, + format!("presets::view_rename({id}, {name:?})"), + presets::view_rename(id, name, SEQ), + ); + } + rec( + o, + format!("presets::view_remove({id})"), + presets::view_remove(id, SEQ), + ); + rec( + o, + format!("presets::view_go({id})"), + presets::view_go(id, SEQ), + ); + for command in [0x1504u16, 0x1444] { + let q = presets::view_query(command, id, SEQ); + rec(o, format!("presets::view_query({command:#06x}, {id})"), q); + } + } + for source in [0u32, 1, 2, 3, 4] { + for p in poses() { + let f = presets::startup_pose(source, p, SEQ); + rec(o, format!("presets::startup_pose({source}, {p:?})"), f); + } + } + for p in poses() { + rec( + o, + format!("presets::zone_return_set({p:?})"), + presets::zone_return_set(p, SEQ), + ); + rec( + o, + format!("presets::hand_return_set({p:?})"), + presets::hand_return_set(p, SEQ), + ); + } + for ai in StartupAi::value_variants() { + let f = presets::startup_picture_settings(ai.byte(), SEQ); + rec(o, format!("presets::startup_picture_settings({ai:?})"), f); + } + let block: Vec = (0u8..48).collect(); + for len in [0usize, 39, 40, 48] { + for ai in [0x00u8, 0xfe, 0xff] { + let f = presets::startup_with_ai(&block[..len], ai, SEQ); + rec( + o, + format!("presets::startup_with_ai(0..{len}, {ai:#04x})"), + f, + ); + } + } + out +} + +#[test] +fn builders_emit_the_recorded_bytes() { + let now = record().join("\n") + "\n"; + if std::env::var_os("BYTE_GUARD_BLESS").is_some() { + std::fs::write(FIXTURE, &now).unwrap(); + return; + } + let recorded = std::fs::read_to_string(FIXTURE).expect("tests/fixtures/byte-guard.txt"); + let (now, recorded): (Vec<&str>, Vec<&str>) = + (now.lines().collect(), recorded.lines().collect()); + assert_eq!( + now.len(), + recorded.len(), + "the number of recorded calls changed" + ); + for (n, r) in now.iter().zip(&recorded) { + assert_eq!(n, r, "a builder emits other bytes than before the move"); + } +} diff --git a/crates/obsbot-device/tests/fixtures/byte-guard.txt b/crates/obsbot-device/tests/fixtures/byte-guard.txt new file mode 100644 index 0000000..eb511c1 --- /dev/null +++ b/crates/obsbot-device/tests/fixtures/byte-guard.txt @@ -0,0 +1,831 @@ +frame::query_frame(0x04, 0x0104, 0x0000) = aa0100000c0085940a040401000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::frame(0x02, 0xa0c2, [1 0 0 0], 0x0000) = aa2500000c00497d0a02c2a00400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame_with(0x04, 0x1504, [7 0 0 0], 0x0000) = aa2100000c002e5b0a0404150400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame(0x04, 0x0104, 0x0001) = aa0101000c00d4510a040401000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::frame(0x02, 0xa0c2, [1 0 0 0], 0x0001) = aa2501000c0018b80a02c2a00400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame_with(0x04, 0x1504, [7 0 0 0], 0x0001) = aa2101000c007f9e0a0404150400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame(0x04, 0x0104, 0x1234) = aa0134120c00101e0a040401000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::frame(0x02, 0xa0c2, [1 0 0 0], 0x1234) = aa2534120c00dcf70a02c2a00400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame_with(0x04, 0x1504, [7 0 0 0], 0x1234) = aa2134120c00bbd10a0404150400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame(0x04, 0x0104, 0xffff) = aa01ffff0c00f5930a040401000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::frame(0x02, 0xa0c2, [1 0 0 0], 0xffff) = aa25ffff0c00397a0a02c2a00400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::query_frame_with(0x04, 0x1504, [7 0 0 0], 0xffff) = aa21ffff0c005e5c0a0404150400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +frame::frame(0x04, 0x3e44, [0xa5; MAX_DATA]) = aa2534120c00df3e0a04443e2c000901a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5 +frame::crc16_usb("123456789") = 0xb4c8 +cmd::hdr(false) = 010100 +cmd::hdr(true) = 010101 +cmd::led_blink(false) = 180100 +cmd::led_blink(true) = 180101 +cmd::mirror(false) = 140100 +cmd::mirror(true) = 140101 +cmd::mic_in_sleep(false) = 130100 +cmd::mic_in_sleep(true) = 130101 +cmd::auto_gain(false) = 170100 +cmd::auto_gain(true) = 170101 +cmd::microphone(false) = 1c0100 +cmd::microphone(true) = 1c0101 +cmd::sleep_background_mirror(false) = 0e020200 +cmd::sleep_background_mirror(true) = 0e020201 +cmd::ai_mode(Static) = 16020000 +cmd::default_work_mode(Static) = 190100 +cmd::ai_mode(Normal) = 16020200 +cmd::default_work_mode(Normal) = 190120 +cmd::ai_mode(UpperBody) = 16020201 +cmd::default_work_mode(UpperBody) = 190121 +cmd::ai_mode(CloseUp) = 16020202 +cmd::default_work_mode(CloseUp) = 190122 +cmd::ai_mode(Headless) = 16020203 +cmd::default_work_mode(Headless) = 190123 +cmd::ai_mode(LowerBody) = 16020204 +cmd::default_work_mode(LowerBody) = 190124 +cmd::ai_mode(Hand) = 16020300 +cmd::default_work_mode(Hand) = 190130 +cmd::ai_mode(Group) = 16020100 +cmd::default_work_mode(Group) = 190110 +cmd::auto_sleep(-32768) = 0b020080 +cmd::auto_sleep(-600) = 0b02a8fd +cmd::auto_sleep(-1) = 0b02ffff +cmd::auto_sleep(0) = 0b020000 +cmd::auto_sleep(1) = 0b020100 +cmd::auto_sleep(600) = 0b025802 +cmd::auto_sleep(32767) = 0b02ff7f +cmd::auto_sleep_off(0) = 0b02a8fd +cmd::auto_sleep_off(1) = 0b02ffff +cmd::auto_sleep_off(600) = 0b02a8fd +cmd::auto_sleep_off(32767) = 0b020180 +cmd::auto_sleep_off(32768) = error: invalid value: auto-sleep time is at most 32767 s +cmd::auto_sleep_off(65535) = error: invalid value: auto-sleep time is at most 32767 s +cmd::sleep_background(Black) = 0e020000 +cmd::sleep_background(Image) = 0e020001 +cmd::led(0) = 1a0100 +cmd::led(1) = 1a0101 +cmd::led(2) = 1a0102 +cmd::led(3) = 1a0103 +cmd::led(4) = error: invalid value: LED brightness is 0-3 +cmd::wake_zoom(0) = 15026400 +cmd::wake_zoom(1) = 15026401 +cmd::wake_zoom(50) = 15026432 +cmd::wake_zoom(100) = 15026464 +cmd::wake_zoom(101) = error: invalid value: wake-up zoom is 0-100 percent +cmd::wake_zoom(255) = error: invalid value: wake-up zoom is 0-100 percent +cmd::wake_zoom_percent(0.99) = error: invalid value: wake-up zoom factor is 1.0-4.0 +cmd::wake_zoom_percent(1) = 0 +cmd::wake_zoom_percent(1.5) = 17 +cmd::wake_zoom_percent(2.5) = 50 +cmd::wake_zoom_percent(4) = 100 +cmd::wake_zoom_percent(4.01) = error: invalid value: wake-up zoom factor is 1.0-4.0 +cmd::wake_zoom_percent(NaN) = error: invalid value: wake-up zoom factor is 1.0-4.0 +cmd::field_of_view(Wide) = 040100 +cmd::field_of_view(Medium) = 040101 +cmd::field_of_view(Narrow) = 040102 +cmd::noise_reduction(Off) = 0a0100 +cmd::noise_reduction(Weak) = 0a0101 +cmd::noise_reduction(Medium) = 0a0102 +cmd::noise_reduction(Strong) = 0a0103 +cmd::pickup_distance(Near) = 1b0100 +cmd::pickup_distance(Standard) = 1b0101 +cmd::pickup_distance(Far) = 1b0102 +cmd::exposure_mode_select(Global) = 030100 +cmd::exposure_mode_select(Face) = 030101 +cmd::startup_go() = 000101 +cmd::invert(false) = aa2534120c004f3d0a04843b0100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::invert(true) = aa2534120c004f3d0a04843b010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_tracking(false) = aa2534120c003ee80a04c4080100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_tracking(true) = aa2534120c003ee80a04c408010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_pan(false) = aa2534120c001f370a0444230100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_pan(true) = aa2534120c001f370a044423010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_tilt(false) = aa2534120c007ef70a04c4230100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_tilt(true) = aa2534120c007ef70a04c423010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_gimbal(false) = aa2534120c00bef40a04c4260100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_gimbal(true) = aa2534120c00bef40a04c426010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::autofocus_mode(false) = aa2534120c000c990a0202360400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::autofocus_mode(true) = aa2534120c000c990a0202360400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_follow(false) = aa2534120c00ef1a0a04044e0100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_follow(true) = aa2534120c00ef1a0a04044e010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::sleep(false) = aa2534120c00dcf70a02c2a00400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::sleep(true) = aa2534120c00dcf70a02c2a00400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::auto_zoom(false) = aa2534120c00dee90a04440a0100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::auto_zoom(true) = aa2534120c00dee90a04440a010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_return_go() = aa2534120c001f1a0a04444f0000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_return_go() = aa2534120c00cf350a0484250000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_return_reset() = aa2534120c009f350a0444250000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::factory_reset() = aa2534120c008d310a0202a8010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_stop() = aa2534120c002ec40a0404670000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_stop() = aa2534120c003ec00a04c4680000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::tracking_speed(Standard) = aa2534120c003f2b0a04c40c0100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::tracking_speed(Headroom) = aa2534120c003f2b0a04c40c010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::tracking_speed(Sport) = aa2534120c003f2b0a04c40c010067fe0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::speed_mode(Lazy) = aa2534120c009ee80a0444090100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::speed_mode(Slow) = aa2534120c009ee80a044409010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::speed_mode(Normal) = aa2534120c009ee80a044409010067fe0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::speed_mode(Fast) = aa2534120c009ee80a0444090100a63e0300000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::speed_mode(VeryFast) = aa2534120c009ee80a0444090100e7fc0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::speed_mode(Auto) = aa2534120c009ee80a0444090100263c0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_size(LongShot) = aa2534120c00bf290a04c40a0100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_size(MediumLongShot) = aa2534120c00bf290a04c40a010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_size(MediumShot) = aa2534120c00bf290a04c40a010067fe0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_size(MediumCloseUp) = aa2534120c00bf290a04c40a0100a63e0300000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_size(CloseUp) = aa2534120c00bf290a04c40a0100e7fc0400000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_size(BigCloseUp) = aa2534120c00bf290a04c40a0100263c0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::auto_zoom_speed(Slow) = aa2534120c001f290a04440b0100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::auto_zoom_speed(Medium) = aa2534120c001f290a04440b010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::auto_zoom_speed(Fast) = aa2534120c001f290a04440b010067fe0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::part_mode(Normal) = aa2534120c00cf210a0484150100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::part_mode(HeadRemoval) = aa2534120c00cf210a048415010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::part_mode(LowerBody) = aa2534120c00cf210a048415010067fe0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_side(Right) = aa2534120c00def70a0444220100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_side(Left) = aa2534120c00def70a044422010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::autofocus_mode_select(Continuous) = aa2534120c00bc980a0242340400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::autofocus_mode_select(Single) = aa2534120c00bc980a0242340400bfbf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::autofocus_mode_select(Manual) = aa2534120c00bc980a0242340400be430300000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(TargetLock, false) = aa2534120c003f3a0a04c4300100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(TargetLock, true) = aa2534120c003f3a0a04c430010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(Zoom, false) = aa2534120c009f3a0a0444310100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(Zoom, true) = aa2534120c009f3a0a044431010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(DynamicZoom, false) = aa2534120c001efb0a0444330100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(DynamicZoom, true) = aa2534120c001efb0a044433010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(DirectionChange, false) = aa2534120c007f3b0a04c4330100e63f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture(DirectionChange, true) = aa2534120c007f3b0a04c433010027ff0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture_zoom_ratio(0.99) = error: invalid value: gesture zoom ratio is 1.0-4.0 +cmd::gesture_zoom_ratio(1) = aa2534120c00df3b0a04443204009fd70000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture_zoom_ratio(2.5) = aa2534120c00df3b0a0444320400a6370000204000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture_zoom_ratio(4) = aa2534120c00df3b0a0444320400de370000804000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gesture_zoom_ratio(4.01) = error: invalid value: gesture zoom ratio is 1.0-4.0 +cmd::gesture_zoom_ratio(NaN) = error: invalid value: gesture zoom ratio is 1.0-4.0 +cmd::hand_return_after(-1) = error: invalid value: hand return time is 0 seconds or more +cmd::hand_return_after(-0) = aa2534120c005ef50a0444240400bfa70000008000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_return_after(0) = aa2534120c005ef50a0444240400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_return_after(2.5) = aa2534120c005ef50a0444240400a6370000204000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_return_after(600) = aa2534120c005ef50a0444240400b0540000164400000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_return_after(inf) = error: invalid value: hand return time is 0 seconds or more +cmd::hand_return_after(NaN) = error: invalid value: hand return time is 0 seconds or more +cmd::audio_volume(0) = aa2534120c004cef0a0202810200fe060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::audio_volume(1) = aa2534120c004cef0a0202810200ff960100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::audio_volume(50) = aa2534120c004cef0a0202810200eb663200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::audio_volume(100) = aa2534120c004cef0a0202810200d4c66400000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::audio_volume(101) = error: invalid value: microphone volume is 0-100 +cmd::audio_volume(255) = error: invalid value: microphone volume is 0-100 +cmd::focus_point(-0.01, -0.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(-0.01, -0.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(-0.01, 0) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(-0.01, 0) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(-0.01, 0.75) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(-0.01, 0.75) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(-0.01, 1) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(-0.01, 1) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(-0.01, 1.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(-0.01, 1.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(0, -0.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(0, -0.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(0, 0) = aa2534120c002d580a0282351400df600000000000000000010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(0, 0) = aa2534120c009eed0a04440508007a220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(0, 0.75) = aa2534120c002d580a0282351400cb1f000000000000403f010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(0, 0.75) = aa2534120c009eed0a04440508000bf2000000000000403f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(0, 1) = aa2534120c002d580a02823514000bdf000000000000803f010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(0, 1) = aa2534120c009eed0a04440508005bf2000000000000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(0, 1.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(0, 1.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(0.25, -0.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(0.25, -0.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(0.25, 0) = aa2534120c002d580a028235140086e40000803e00000000010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(0.25, 0) = aa2534120c009eed0a04440508004c270000803e00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(0.25, 0.75) = aa2534120c002d580a0282351400929b0000803e0000403f010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(0.25, 0.75) = aa2534120c009eed0a04440508003df70000803e0000403f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(0.25, 1) = aa2534120c002d580a0282351400525b0000803e0000803f010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(0.25, 1) = aa2534120c009eed0a04440508006df70000803e0000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(0.25, 1.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(0.25, 1.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1, -0.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1, -0.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1, 0) = aa2534120c002d580a0282351400d7740000803f00000000010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(1, 0) = aa2534120c009eed0a044405080071e70000803f00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(1, 0.75) = aa2534120c002d580a0282351400c30b0000803f0000403f010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(1, 0.75) = aa2534120c009eed0a044405080000370000803f0000403f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(1, 1) = aa2534120c002d580a028235140003cb0000803f0000803f010000006400000064000000000000000000000000000000000000000000000000000000 +cmd::view_center(1, 1) = aa2534120c009eed0a044405080050370000803f0000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::focus_point(1, 1.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1, 1.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1.01, -0.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1.01, -0.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1.01, 0) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1.01, 0) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1.01, 0.75) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1.01, 0.75) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1.01, 1) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1.01, 1) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::focus_point(1.01, 1.01) = error: invalid value: focus point x and y are 0.0-1.0 +cmd::view_center(1.01, 1.01) = error: invalid value: centre point x and y are 0.0-1.0 +cmd::zone_area((-45.0, 45.0), (-30.0, 10.0)) = aa2534120c006edb0a04044c04002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-45.0, 45.0), (-30.0, 10.0)) = aa2534120c005f360a04442004002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c420040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-45.0, 45.0), (-45.0, 45.0)) = aa2534120c006edb0a04044c04002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-45.0, 45.0), (-45.0, 45.0)) = aa2534120c005f360a04442004002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c420040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-150.0, 150.0), (-30.0, 10.0)) = aa2534120c006edb0a04044c0400f036000016c300000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c0400f1960000164300000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-150.0, 150.0), (-30.0, 10.0)) = aa2534120c005f360a0444200400f036000016c300000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c4200400f1960000164300000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-45.0, 45.0), (-150.0, 150.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (-150.0, 150.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-150.5, 0.0), (-30.0, 10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-150.5, 0.0), (-30.0, 10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-45.0, 45.0), (-150.5, 0.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (-150.5, 0.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((0.0, 150.5), (-30.0, 10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((0.0, 150.5), (-30.0, 10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-45.0, 45.0), (0.0, 150.5)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (0.0, 150.5)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-90.0, 70.0), (-30.0, 10.0)) = aa2534120c006edb0a04044c040048960000b4c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c04005af600008c4200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-90.0, 70.0), (-30.0, 10.0)) = aa2534120c005f360a044420040048960000b4c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c42004005af600008c4200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-45.0, 45.0), (-90.0, 70.0)) = aa2534120c006edb0a04044c04002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d040048960000b4c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d04005af600008c4200000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-45.0, 45.0), (-90.0, 70.0)) = aa2534120c005f360a04442004002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c420040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a044421040048960000b4c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c42104005af600008c4200000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-90.5, 0.0), (-30.0, 10.0)) = aa2534120c006edb0a04044c040049060000b5c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c0400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-90.5, 0.0), (-30.0, 10.0)) = aa2534120c005f360a044420040049060000b5c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c4200400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-45.0, 45.0), (-90.5, 0.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (-90.5, 0.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((0.0, 70.5), (-30.0, 10.0)) = aa2534120c006edb0a04044c0400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c04005b6600008d4200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((0.0, 70.5), (-30.0, 10.0)) = aa2534120c005f360a0444200400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c42004005b6600008d4200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-45.0, 45.0), (0.0, 70.5)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (0.0, 70.5)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-30.0, 10.0), (-30.0, 10.0)) = aa2534120c006edb0a04044c04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-30.0, 10.0), (-30.0, 10.0)) = aa2534120c005f360a04442004003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c420040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((-45.0, 45.0), (-30.0, 10.0)) = aa2534120c006edb0a04044c04002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c005ede0a04844c040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c000ed10a04044d04003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c003ed40a04844d040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::hand_area((-45.0, 45.0), (-30.0, 10.0)) = aa2534120c005f360a04442004002956000034c200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c006f330a04c420040028f60000344200000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c003f3c0a04442104003b970000f0c100000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c000f390a04c421040067f70000204100000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zone_area((10.0, 10.0), (-30.0, 10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((10.0, 10.0), (-30.0, 10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-45.0, 45.0), (10.0, 10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (10.0, 10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((10.0, -10.0), (-30.0, 10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((10.0, -10.0), (-30.0, 10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-45.0, 45.0), (10.0, -10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (10.0, -10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((NaN, 1.0), (-30.0, 10.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((NaN, 1.0), (-30.0, 10.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::zone_area((-45.0, 45.0), (NaN, 1.0)) = error: invalid value: zone area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area((-45.0, 45.0), (NaN, 1.0)) = error: invalid value: hand area: min must be below max, pan within -150..150, tilt within -70..90 +cmd::hand_area_reset() = aa2534120c00fef50a04c4250000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c00bf310a0404260000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2536120c007efe0a0444260000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2537120c007f3b0a0484260000ffdb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(None, None) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(None, Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(None, Some(-70.0)) = aa2534120c005f050a0444640c0040ea00007ac400008cc200007ac40000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(None, Some(0.0)) = aa2534120c005f050a0444640c00263700007ac40000000000007ac40000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(None, Some(12.5)) = aa2534120c005f050a0444640c0015b000007ac40000484100007ac40000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(None, Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(None, Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(None, Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(None, Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-150.0), None) = aa2534120c005f050a0444640c00b0be00007ac400007ac4000016c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-150.0), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-150.0), Some(-70.0)) = aa2534120c005f050a0444640c002c2800007ac400008cc2000016c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-150.0), Some(0.0)) = aa2534120c005f050a0444640c004af500007ac400000000000016c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-150.0), Some(12.5)) = aa2534120c005f050a0444640c00797200007ac400004841000016c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-150.0), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-150.0), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-150.0), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-150.0), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-70.0), None) = aa2534120c005f050a0444640c001bde00007ac400007ac400008cc20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-70.0), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-70.0), Some(-70.0)) = aa2534120c005f050a0444640c00874800007ac400008cc200008cc20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-70.0), Some(0.0)) = aa2534120c005f050a0444640c00e19500007ac40000000000008cc20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-70.0), Some(12.5)) = aa2534120c005f050a0444640c00d21200007ac40000484100008cc20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-70.0), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-70.0), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-70.0), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-70.0), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(0.0), None) = aa2534120c005f050a0444640c00fe8f00007ac400007ac4000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(0.0), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(0.0), Some(-70.0)) = aa2534120c005f050a0444640c00621900007ac400008cc2000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(0.0), Some(0.0)) = aa2534120c005f050a0444640c0004c400007ac400000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(0.0), Some(12.5)) = aa2534120c005f050a0444640c00374300007ac400004841000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(0.0), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(0.0), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(0.0), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(0.0), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(12.5), None) = aa2534120c005f050a0444640c0008bf00007ac400007ac4000048410000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(12.5), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(12.5), Some(-70.0)) = aa2534120c005f050a0444640c00942900007ac400008cc2000048410000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(12.5), Some(0.0)) = aa2534120c005f050a0444640c00f2f400007ac400000000000048410000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(12.5), Some(12.5)) = aa2534120c005f050a0444640c00c17300007ac400004841000048410000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(12.5), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(12.5), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(12.5), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(12.5), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(90.0), None) = aa2534120c005f050a0444640c0009be00007ac400007ac40000b4420000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(90.0), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(90.0), Some(-70.0)) = aa2534120c005f050a0444640c00952800007ac400008cc20000b4420000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(90.0), Some(0.0)) = aa2534120c005f050a0444640c00f3f500007ac4000000000000b4420000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(90.0), Some(12.5)) = aa2534120c005f050a0444640c00c07200007ac4000048410000b4420000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(90.0), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(90.0), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(90.0), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(90.0), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.0), None) = aa2534120c005f050a0444640c00b11e00007ac400007ac4000016430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(150.0), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.0), Some(-70.0)) = aa2534120c005f050a0444640c002d8800007ac400008cc2000016430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(150.0), Some(0.0)) = aa2534120c005f050a0444640c004b5500007ac400000000000016430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(150.0), Some(12.5)) = aa2534120c005f050a0444640c0078d200007ac400004841000016430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(150.0), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.0), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.0), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.0), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), None) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(-70.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(0.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(12.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(150.5), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-90.5), None) = aa2534120c005f050a0444640c00098e00007ac400007ac40000b5c20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-90.5), Some(-150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-90.5), Some(-70.0)) = aa2534120c005f050a0444640c00951800007ac400008cc20000b5c20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-90.5), Some(0.0)) = aa2534120c005f050a0444640c00f3c500007ac4000000000000b5c20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-90.5), Some(12.5)) = aa2534120c005f050a0444640c00c04200007ac4000048410000b5c20000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_pose(Some(-90.5), Some(90.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-90.5), Some(150.0)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-90.5), Some(150.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose(Some(-90.5), Some(-90.5)) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(None, None, 0.99) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(None, None, 1) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(None, None, 45) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(None, None, 180) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(None, None, 180.5) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(None, None, NaN) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(12.5), None, 0.99) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(12.5), None, 1) = aa2534120c009ec50a0444651800bcaf0000803f0000803f0000803f00007ac400007ac4000048410000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(Some(12.5), None, 45) = aa2534120c009ec50a04446518002b5e00003442000034420000344200007ac400007ac4000048410000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(Some(12.5), None, 180) = aa2534120c009ec50a0444651800689e00003443000034430000344300007ac400007ac4000048410000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(Some(12.5), None, 180.5) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(12.5), None, NaN) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(None, Some(-5.0), 0.99) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(None, Some(-5.0), 1) = aa2534120c009ec50a04446518008a960000803f0000803f0000803f00007ac40000a0c000007ac40000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(None, Some(-5.0), 45) = aa2534120c009ec50a04446518001d6700003442000034420000344200007ac40000a0c000007ac40000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(None, Some(-5.0), 180) = aa2534120c009ec50a04446518005ea700003443000034430000344300007ac40000a0c000007ac40000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(None, Some(-5.0), 180.5) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(None, Some(-5.0), NaN) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(12.5), Some(-5.0), 0.99) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(12.5), Some(-5.0), 1) = aa2534120c009ec50a04446518005e550000803f0000803f0000803f00007ac40000a0c0000048410000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(Some(12.5), Some(-5.0), 45) = aa2534120c009ec50a0444651800c9a400003442000034420000344200007ac40000a0c0000048410000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(Some(12.5), Some(-5.0), 180) = aa2534120c009ec50a04446518008a6400003443000034430000344300007ac40000a0c0000048410000000000000000000000000000000000000000 +cmd::gimbal_pose_speed(Some(12.5), Some(-5.0), 180.5) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(12.5), Some(-5.0), NaN) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(150.5), Some(0.0), 0.99) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(150.5), Some(0.0), 1) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(Some(150.5), Some(0.0), 45) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(Some(150.5), Some(0.0), 180) = error: invalid value: gimbal pose needs pan -150..150 and/or tilt -70..90 +cmd::gimbal_pose_speed(Some(150.5), Some(0.0), 180.5) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_pose_speed(Some(150.5), Some(0.0), NaN) = error: invalid value: gimbal speed is 1-180 degrees per second +cmd::gimbal_rate(-178.5, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178.5, -178, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178.5, -0, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178.5, 0, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178.5, 5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178.5, 178, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178.5, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-178, -178, false, false) = aa2534120c000f050a0484640c0051460000000000003243000032430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-178, -0, false, false) = aa2534120c000f050a0484640c00109b0000000000000000000032430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-178, 0, false, false) = aa2534120c000f050a0484640c00109b0000000000000000000032430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-178, 5, false, false) = aa2534120c000f050a0484640c00082a000000000000a0c0000032430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-178, 178, false, false) = aa2534120c000f050a0484640c00509800000000000032c3000032430000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-178, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-0, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(-0, -178, false, false) = aa2534120c000f050a0484640c0005d70000000000003243000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-0, -0, false, false) = aa2534120c000f050a0484640c00440a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-0, 0, false, false) = aa2534120c000f050a0484640c00440a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-0, 5, false, false) = aa2534120c000f050a0484640c005cbb000000000000a0c0000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-0, 178, false, false) = aa2534120c000f050a0484640c00040900000000000032c3000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(-0, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(0, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(0, -178, false, false) = aa2534120c000f050a0484640c0005d70000000000003243000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(0, -0, false, false) = aa2534120c000f050a0484640c00440a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(0, 0, false, false) = aa2534120c000f050a0484640c00440a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(0, 5, false, false) = aa2534120c000f050a0484640c005cbb000000000000a0c0000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(0, 178, false, false) = aa2534120c000f050a0484640c00040900000000000032c3000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(0, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(12.5, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(12.5, -178, false, false) = aa2534120c000f050a0484640c00f2470000000000003243000048c10000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, -0, false, false) = aa2534120c000f050a0484640c00b39a0000000000000000000048c10000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, 0, false, false) = aa2534120c000f050a0484640c00b39a0000000000000000000048c10000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, 5, false, false) = aa2534120c000f050a0484640c00ab2b000000000000a0c0000048c10000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, 178, false, false) = aa2534120c000f050a0484640c00f39900000000000032c3000048c10000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178, -178, false, false) = aa2534120c000f050a0484640c0050e60000000000003243000032c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(178, -0, false, false) = aa2534120c000f050a0484640c00113b0000000000000000000032c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(178, 0, false, false) = aa2534120c000f050a0484640c00113b0000000000000000000032c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(178, 5, false, false) = aa2534120c000f050a0484640c00098a000000000000a0c0000032c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(178, 178, false, false) = aa2534120c000f050a0484640c00513800000000000032c3000032c30000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(178, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, -178, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, -0, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, 0, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, 5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, 178, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(178.5, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, -178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, -178, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, -0, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, 0, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, 5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, 178, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(NaN, 178.5, false, false) = error: invalid value: gimbal rate is -178 to 178 degrees per second +cmd::gimbal_rate(12.5, 5, true, false) = aa2534120c000f050a0484640c00aa8b000000000000a0c0000048410000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, 5, false, true) = aa2534120c000f050a0484640c00aa8b000000000000a0c0000048410000000000000000000000000000000000000000000000000000000000000000 +cmd::gimbal_rate(12.5, 5, true, true) = aa2534120c000f050a0484640c00ab2b000000000000a0c0000048c10000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(0, 0.99) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(0, 1) = error: invalid value: zoom speed must be positive +cmd::zoom_to(0, 2.5) = error: invalid value: zoom speed must be positive +cmd::zoom_to(0, 4) = error: invalid value: zoom speed must be positive +cmd::zoom_to(0, 4.01) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(1, 0.99) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(1, 1) = aa2534120c000f000a04846808009a3e010000000000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(1, 2.5) = aa2534120c000f000a0484680800a3de0100000000002040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(1, 4) = aa2534120c000f000a0484680800dbde0100000000008040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(1, 4.01) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(10, 0.99) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(10, 1) = aa2534120c000f000a0484680800db8d0a0000000000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(10, 2.5) = aa2534120c000f000a0484680800e26d0a00000000002040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(10, 4) = aa2534120c000f000a04846808009a6d0a00000000008040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(10, 4.01) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(30, 0.99) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(30, 1) = aa2534120c000f000a0484680800db721e0000000000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(30, 2.5) = aa2534120c000f000a0484680800e2921e00000000002040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(30, 4) = aa2534120c000f000a04846808009a921e00000000008040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(30, 4.01) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(50, 0.99) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(50, 1) = aa2534120c000f000a0484680800d93f320000000000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(50, 2.5) = aa2534120c000f000a0484680800e0df3200000000002040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(50, 4) = aa2534120c000f000a048468080098df3200000000008040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(50, 4.01) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(2147483647, 0.99) = error: invalid value: zoom factor is 1.0-4.0 +cmd::zoom_to(2147483647, 1) = aa2534120c000f000a04846808001a3cffffff7f0000803f000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(2147483647, 2.5) = aa2534120c000f000a048468080023dcffffff7f00002040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(2147483647, 4) = aa2534120c000f000a04846808005bdcffffff7f00008040000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::zoom_to(2147483647, 4.01) = error: invalid value: zoom factor is 1.0-4.0 +cmd::framing_offset(false, -0.17) = error: invalid value: framing offset is -0.16 to 0.16 of the frame +cmd::framing_offset(false, -0.16) = aa2534120c007ee90a04c40b040094a70ad723be00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(false, -0) = aa2534120c007ee90a04c40b0400bfa70000008000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(false, 0) = aa2534120c007ee90a04c40b0400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(false, 0.1) = aa2534120c007ee90a04c40b0400d445cdcccc3d00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(false, 0.16) = aa2534120c007ee90a04c40b040095070ad7233e00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(false, 0.17) = error: invalid value: framing offset is -0.16 to 0.16 of the frame +cmd::framing_offset(false, NaN) = error: invalid value: framing offset is -0.16 to 0.16 of the frame +cmd::framing_offset(true, -0.17) = error: invalid value: framing offset is -0.16 to 0.16 of the frame +cmd::framing_offset(true, -0.16) = aa2534120c005eeb0a04440c040094a70ad723be00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(true, -0) = aa2534120c005eeb0a04440c0400bfa70000008000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(true, 0) = aa2534120c005eeb0a04440c0400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(true, 0.1) = aa2534120c005eeb0a04440c0400d445cdcccc3d00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(true, 0.16) = aa2534120c005eeb0a04440c040095070ad7233e00000000000000000000000000000000000000000000000000000000000000000000000000000000 +cmd::framing_offset(true, 0.17) = error: invalid value: framing offset is -0.16 to 0.16 of the frame +cmd::framing_offset(true, NaN) = error: invalid value: framing offset is -0.16 to 0.16 of the frame +cmd::gimbal_angles_ok(-150.5, -70.5) = false +cmd::gimbal_angles_ok(-150.5, -70) = false +cmd::gimbal_angles_ok(-150.5, 0) = false +cmd::gimbal_angles_ok(-150.5, 90) = false +cmd::gimbal_angles_ok(-150.5, 90.5) = false +cmd::gimbal_angles_ok(-150, -70.5) = false +cmd::gimbal_angles_ok(-150, -70) = true +cmd::gimbal_angles_ok(-150, 0) = true +cmd::gimbal_angles_ok(-150, 90) = true +cmd::gimbal_angles_ok(-150, 90.5) = false +cmd::gimbal_angles_ok(0, -70.5) = false +cmd::gimbal_angles_ok(0, -70) = true +cmd::gimbal_angles_ok(0, 0) = true +cmd::gimbal_angles_ok(0, 90) = true +cmd::gimbal_angles_ok(0, 90.5) = false +cmd::gimbal_angles_ok(150, -70.5) = false +cmd::gimbal_angles_ok(150, -70) = true +cmd::gimbal_angles_ok(150, 0) = true +cmd::gimbal_angles_ok(150, 90) = true +cmd::gimbal_angles_ok(150, 90.5) = false +cmd::gimbal_angles_ok(150.5, -70.5) = false +cmd::gimbal_angles_ok(150.5, -70) = false +cmd::gimbal_angles_ok(150.5, 0) = false +cmd::gimbal_angles_ok(150.5, 90) = false +cmd::gimbal_angles_ok(150.5, 90.5) = false +presets::create(0, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c009efc0a0444391800f3ad00000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::update(0, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00eefe0a04043e1800f3ad00000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::create(0, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c009efc0a044439180047e3000000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::update(0, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00eefe0a04043e180047e3000000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::create(0, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c009efc0a044439180077ad00000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::update(0, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00eefe0a04043e180077ad00000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::create(0, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c009efc0a0444391800c5b600000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::update(0, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00eefe0a04043e1800c5b600000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::create(0, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(0, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(0, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(0, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(0, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(0, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(0, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(0, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(0, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(0, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(0, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(0, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::rename(0, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::rename(0, "Desk") = aa2534120c008efd0a04843a08001a12000000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::rename(0, "Bereich1") = aa2534120c008efd0a04843a0c00b1a90000000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::rename(0, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::rename(0, "12345678901234567890123456789012") = aa2534120c008efd0a04843a2400c1ee0000000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::rename(0, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::delete(0) = aa2534120c00cefc0a0484390400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c009efa0a04843d2c002a5f00000000feffffffffffffff80ffffffffffffff00000000ffffffff00ffffffffffffffff00000000000000 +presets::recall(0) = aa2534120c00ff3c0a04c4391400d6fb000000000000803f0000803f0000803f0000803f000000000000000000000000000000000000000000000000 +presets::create(1, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c009efc0a0444391800ae3801000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::update(1, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00eefe0a04043e1800ae3801000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::create(1, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c009efc0a04443918001a76010000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::update(1, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00eefe0a04043e18001a76010000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::create(1, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c009efc0a04443918002a3801000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::update(1, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00eefe0a04043e18002a3801000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::create(1, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c009efc0a0444391800982301000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::update(1, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00eefe0a04043e1800982301000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::create(1, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(1, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(1, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(1, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(1, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(1, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(1, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(1, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(1, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(1, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(1, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(1, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::rename(1, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::rename(1, "Desk") = aa2534120c008efd0a04843a0800dbde010000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::rename(1, "Bereich1") = aa2534120c008efd0a04843a0c004c6a0100000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::rename(1, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::rename(1, "12345678901234567890123456789012") = aa2534120c008efd0a04843a240000330100000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::rename(1, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::delete(1) = aa2534120c00cefc0a0484390400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c009efa0a04843d2c002a5f00000000feffffffffffffff80ffffffffffffff00000000ffffffff00ffffffffffffffff00000000000000 +presets::recall(1) = aa2534120c00ff3c0a04c4391400eb2a010000000000803f0000803f0000803f0000803f000000000000000000000000000000000000000000000000 +presets::create(2, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c009efc0a04443918004ac702000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::update(2, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00eefe0a04043e18004ac702000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::create(2, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c009efc0a0444391800fe89020000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::update(2, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00eefe0a04043e1800fe89020000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::create(2, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c009efc0a0444391800cec702000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::update(2, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00eefe0a04043e1800cec702000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::create(2, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c009efc0a04443918007cdc02000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::update(2, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00eefe0a04043e18007cdc02000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::create(2, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(2, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(2, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(2, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(2, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(2, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(2, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(2, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(2, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(2, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::create(2, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::update(2, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::rename(2, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::rename(2, "Desk") = aa2534120c008efd0a04843a08009bcb020000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::rename(2, "Bereich1") = aa2534120c008efd0a04843a0c00486e0200000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::rename(2, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::rename(2, "12345678901234567890123456789012") = aa2534120c008efd0a04843a240040150200000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::rename(2, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::delete(2) = aa2534120c00cefc0a0484390400bfbf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000 aa2535120c009efa0a04843d2c002a5f00000000feffffffffffffff80ffffffffffffff00000000ffffffff00ffffffffffffffff00000000000000 +presets::recall(2) = aa2534120c00ff3c0a04c4391400af19020000000000803f0000803f0000803f0000803f000000000000000000000000000000000000000000000000 +presets::create(3, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset slot is 0-2 +presets::create(3, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(3, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::rename(3, "") = error: invalid value: preset slot is 0-2 +presets::rename(3, "Desk") = error: invalid value: preset slot is 0-2 +presets::rename(3, "Bereich1") = error: invalid value: preset slot is 0-2 +presets::rename(3, "äöü") = error: invalid value: preset slot is 0-2 +presets::rename(3, "12345678901234567890123456789012") = error: invalid value: preset slot is 0-2 +presets::rename(3, "123456789012345678901234567890123") = error: invalid value: preset slot is 0-2 +presets::delete(3) = error: invalid value: preset slot is 0-2 +presets::recall(3) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: preset slot is 0-2 +presets::create(255, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::update(255, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: preset slot is 0-2 +presets::rename(255, "") = error: invalid value: preset slot is 0-2 +presets::rename(255, "Desk") = error: invalid value: preset slot is 0-2 +presets::rename(255, "Bereich1") = error: invalid value: preset slot is 0-2 +presets::rename(255, "äöü") = error: invalid value: preset slot is 0-2 +presets::rename(255, "12345678901234567890123456789012") = error: invalid value: preset slot is 0-2 +presets::rename(255, "123456789012345678901234567890123") = error: invalid value: preset slot is 0-2 +presets::delete(255) = error: invalid value: preset slot is 0-2 +presets::recall(255) = error: invalid value: preset slot is 0-2 +presets::view_add(0, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c004f230a0484131800f3ad00000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::view_add(0, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c004f230a048413180047e3000000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(0, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c004f230a048413180077ad00000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(0, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c004f230a0484131800c5b600000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::view_add(0, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(0, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(0, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(0, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(0, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(0, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_rename(0, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(0, "Desk") = aa2534120c009f210a04441508001a12000000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(0, "Bereich1") = aa2534120c009f210a0444150c00b1a90000000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(0, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(0, "12345678901234567890123456789012") = aa2534120c009f210a0444152400c1ee0000000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::view_rename(0, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_remove(0) = aa2534120c007ee30a04c4130400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_go(0) = aa2534120c00eee00a0404160400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1504, 0) = aa2134120c00bbd10a0404150400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1444, 0) = aa2134120c004bd10a0444140400be070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_add(1, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c004f230a0484131800ae3801000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::view_add(1, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c004f230a04841318001a76010000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(1, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c004f230a04841318002a3801000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(1, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c004f230a0484131800982301000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::view_add(1, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(1, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(1, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(1, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(1, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(1, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_rename(1, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(1, "Desk") = aa2534120c009f210a0444150800dbde010000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(1, "Bereich1") = aa2534120c009f210a0444150c004c6a0100000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(1, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(1, "12345678901234567890123456789012") = aa2534120c009f210a044415240000330100000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::view_rename(1, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_remove(1) = aa2534120c007ee30a04c4130400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_go(1) = aa2534120c00eee00a0404160400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1504, 1) = aa2134120c00bbd10a0404150400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1444, 1) = aa2134120c004bd10a0444140400bffb0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_add(7, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c004f230a0484131800658707000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::view_add(7, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c004f230a0484131800d1c9070000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(7, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c004f230a0484131800e18707000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(7, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c004f230a0484131800539c07000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::view_add(7, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(7, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(7, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(7, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(7, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(7, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_rename(7, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(7, "Desk") = aa2534120c009f210a04441508005bf4070000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(7, "Bereich1") = aa2534120c009f210a0444150c0044620700000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(7, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(7, "12345678901234567890123456789012") = aa2534120c009f210a0444152400807f0700000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::view_rename(7, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_remove(7) = aa2534120c007ee30a04c4130400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_go(7) = aa2534120c00eee00a0404160400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1504, 7) = aa2134120c00bbd10a0404150400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1444, 7) = aa2134120c004bd10a0444140400bf730700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_add(255, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c004f230a048413180083aaff000000000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::view_add(255, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c004f230a048413180037e4ff0000000000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(255, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c004f230a048413180007aaff000000000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(255, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c004f230a0484131800b5b1ff000000000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::view_add(255, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(255, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(255, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(255, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(255, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(255, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_rename(255, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(255, "Desk") = aa2534120c009f210a04441508005516ff0000004465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(255, "Bereich1") = aa2534120c009f210a0444150c00e6bcff00000042657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(255, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(255, "12345678901234567890123456789012") = aa2534120c009f210a04441524009ae5ff00000031323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::view_rename(255, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_remove(255) = aa2534120c007ee30a04c41304008e13ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_go(255) = aa2534120c00eee00a04041604008e13ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1504, 255) = aa2134120c00bbd10a04041504008e13ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1444, 255) = aa2134120c004bd10a04441404008e13ff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_add(4294967295, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c004f230a0484131800d251ffffffff000028410000a840000000000000004000007ac40000000000000000000000000000000000000000 +presets::view_add(4294967295, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c004f230a0484131800661fffffffff0000000000000000000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(4294967295, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c004f230a04841318005651ffffffff000016c300008c42000000000000803f00007ac40000000000000000000000000000000000000000 +presets::view_add(4294967295, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c004f230a0484131800e44affffffff000016430000b4c2000000000000804000007ac40000000000000000000000000000000000000000 +presets::view_add(4294967295, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(4294967295, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(4294967295, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(4294967295, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(4294967295, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_add(4294967295, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: zone view pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::view_rename(4294967295, "") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(4294967295, "Desk") = aa2534120c009f210a04441508005a02ffffffff4465736b000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(4294967295, "Bereich1") = aa2534120c009f210a0444150c00a5acffffffff42657265696368310000000000000000000000000000000000000000000000000000000000000000 +presets::view_rename(4294967295, "äöü") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_rename(4294967295, "12345678901234567890123456789012") = aa2534120c009f210a044415240081c1ffffffff31323334353637383930313233343536373839303132333435363738393031320000000000000000 +presets::view_rename(4294967295, "123456789012345678901234567890123") = error: invalid value: name must be 1-32 printable ASCII characters +presets::view_remove(4294967295) = aa2534120c007ee30a04c4130400bf93ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_go(4294967295) = aa2534120c00eee00a0404160400bf93ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1504, 4294967295) = aa2134120c00bbd10a0404150400bf93ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::view_query(0x1444, 4294967295) = aa2134120c004bd10a0444140400bf93ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000 +presets::startup_pose(0, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00befe0a04c43e1800d15e00000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::startup_pose(0, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e18006510000000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(0, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e1800555e00000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(0, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00befe0a04c43e1800e74500000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::startup_pose(0, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(0, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(0, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(0, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(0, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(0, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(1, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00befe0a04c43e18008ccb01000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::startup_pose(1, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e18003885010000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(1, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e180008cb01000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(1, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00befe0a04c43e1800bad001000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::startup_pose(1, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(1, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(1, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(1, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(1, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(1, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(2, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00befe0a04c43e1800683402000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::startup_pose(2, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e1800dc7a020000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(2, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e1800ec3402000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(2, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00befe0a04c43e18005e2f02000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::startup_pose(2, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(2, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(2, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(2, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(2, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(2, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(3, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00befe0a04c43e180035a103000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::startup_pose(3, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e180081ef030000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(3, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e1800b1a103000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(3, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00befe0a04c43e180003ba03000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::startup_pose(3, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(3, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(3, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(3, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(3, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(3, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(4, Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c00befe0a04c43e1800a38b04000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::startup_pose(4, Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e180017c5040000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(4, Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c00befe0a04c43e1800278b04000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::startup_pose(4, Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c00befe0a04c43e1800959004000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::startup_pose(4, Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(4, Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(4, Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(4, Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(4, Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_pose(4, Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: start pose needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::zone_return_set(Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c008eda0a04844e1800d15e00000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::hand_return_set(Pose { pan: 10.5, tilt: -5.25, zoom: 2.0 }) = aa2534120c003f350a04c4241800d15e00000000000028410000a8400000000000000040000000000000000000000000000000000000000000000000 +presets::zone_return_set(Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c008eda0a04844e18006510000000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::hand_return_set(Pose { pan: 0.0, tilt: 0.0, zoom: 1.0 }) = aa2534120c003f350a04c42418006510000000000000000000000000000000000000803f000000000000000000000000000000000000000000000000 +presets::zone_return_set(Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c008eda0a04844e1800555e00000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::hand_return_set(Pose { pan: -150.0, tilt: -70.0, zoom: 1.0 }) = aa2534120c003f350a04c4241800555e00000000000016c300008c42000000000000803f000000000000000000000000000000000000000000000000 +presets::zone_return_set(Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c008eda0a04844e1800e74500000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::hand_return_set(Pose { pan: 150.0, tilt: 90.0, zoom: 4.0 }) = aa2534120c003f350a04c4241800e74500000000000016430000b4c20000000000008040000000000000000000000000000000000000000000000000 +presets::zone_return_set(Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::hand_return_set(Pose { pan: 150.1, tilt: 0.0, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::zone_return_set(Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::hand_return_set(Pose { pan: 0.0, tilt: 90.5, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::zone_return_set(Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::hand_return_set(Pose { pan: 0.0, tilt: -70.5, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::zone_return_set(Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::hand_return_set(Pose { pan: 0.0, tilt: 0.0, zoom: 4.01 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::zone_return_set(Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::hand_return_set(Pose { pan: 0.0, tilt: 0.0, zoom: 0.99 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::zone_return_set(Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::hand_return_set(Pose { pan: NaN, tilt: 0.0, zoom: 1.0 }) = error: invalid value: return position needs pan -150..150, tilt -70..90 and zoom 1.0-4.0 +presets::startup_picture_settings(Static) = aa2534120c00df3e0a04443e2800e012ffffffffffffffff80ffffffffffffff00000000ffffffff0000000000000000000000000000000000000000 +presets::startup_picture_settings(Normal) = aa2534120c00df3e0a04443e280082f400ffffffffffffff80ffffffffffffff00000000ffffffff0000000000000000000000000000000000000000 +presets::startup_picture_settings(UpperBody) = aa2534120c00df3e0a04443e28002f3201ffffffffffffff80ffffffffffffff00000000ffffffff0000000000000000000000000000000000000000 +presets::startup_picture_settings(Headless) = aa2534120c00df3e0a04443e280076ff03ffffffffffffff80ffffffffffffff00000000ffffffff0000000000000000000000000000000000000000 +presets::startup_picture_settings(Group) = aa2534120c00df3e0a04443e28004ec709ffffffffffffff80ffffffffffffff00000000ffffffff0000000000000000000000000000000000000000 +presets::startup_picture_settings(None) = aa2534120c00df3e0a04443e28004dd4feffffffffffffff80ffffffffffffff00000000ffffffff0000000000000000000000000000000000000000 +presets::startup_with_ai(0..0, 0x00) = none +presets::startup_with_ai(0..0, 0xfe) = none +presets::startup_with_ai(0..0, 0xff) = none +presets::startup_with_ai(0..39, 0x00) = none +presets::startup_with_ai(0..39, 0xfe) = none +presets::startup_with_ai(0..39, 0xff) = none +presets::startup_with_ai(0..40, 0x00) = aa2534120c00df3e0a04443e2800187d000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262700000000 +presets::startup_with_ai(0..40, 0xfe) = aa2534120c00df3e0a04443e2800d75dfe0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262700000000 +presets::startup_with_ai(0..40, 0xff) = aa2534120c00df3e0a04443e28007a9bff0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262700000000 +presets::startup_with_ai(0..48, 0x00) = aa2534120c00df3e0a04443e2800187d000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262700000000 +presets::startup_with_ai(0..48, 0xfe) = aa2534120c00df3e0a04443e2800d75dfe0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262700000000 +presets::startup_with_ai(0..48, 0xff) = aa2534120c00df3e0a04443e28007a9bff0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262700000000 diff --git a/crates/obsbot4linux/Cargo.toml b/crates/obsbot4linux/Cargo.toml new file mode 100644 index 0000000..a5e8385 --- /dev/null +++ b/crates/obsbot4linux/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "obsbot4linux" +description = "Control OBSBOT webcams on Linux from the console and the DMS widget" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish.workspace = true +readme = "../../README.md" + +[[bin]] +name = "obsbot4linux" +path = "src/main.rs" + +[dependencies] +obsbot-device.workspace = true +clap.workspace = true +nix.workspace = true +serde.workspace = true +serde_json.workspace = true + +[package.metadata.deb] +maintainer = "Thomas Kroll " +section = "video" +extended-description = "Most settings OBSBOT Center offers, from the console and from the DMS bar widget.\nWhat is missing: https://github.com/hellwand/obsbot4linux/blob/main/docs/center-parity.md\nThe obsbot4linux daemon (systemd user socket) owns the camera state." +depends = "$auto" +license-file = ["../../LICENSE", "0"] +assets = [ + ["target/release/obsbot4linux", "usr/bin/", "755"], + ["../../packaging/systemd/obsbot4linux.service", "usr/lib/systemd/user/", "644"], + ["../../packaging/systemd/obsbot4linux.socket", "usr/lib/systemd/user/", "644"], + ["../../README.md", "usr/share/doc/obsbot4linux/", "644"], + ["../../CHANGELOG.md", "usr/share/doc/obsbot4linux/", "644"], +] + +[package.metadata.generate-rpm] +assets = [ + { source = "target/release/obsbot4linux", dest = "/usr/bin/obsbot4linux", mode = "755" }, + { source = "../../packaging/systemd/obsbot4linux.service", dest = "/usr/lib/systemd/user/obsbot4linux.service", mode = "644" }, + { source = "../../packaging/systemd/obsbot4linux.socket", dest = "/usr/lib/systemd/user/obsbot4linux.socket", mode = "644" }, + { source = "../../README.md", dest = "/usr/share/doc/obsbot4linux/README.md", mode = "644", doc = true }, + { source = "../../CHANGELOG.md", dest = "/usr/share/doc/obsbot4linux/CHANGELOG.md", mode = "644", doc = true }, + { source = "../../LICENSE", dest = "/usr/share/licenses/obsbot4linux/LICENSE", mode = "644" }, +] diff --git a/src/cli.rs b/crates/obsbot4linux/src/cli.rs similarity index 99% rename from src/cli.rs rename to crates/obsbot4linux/src/cli.rs index 1869c42..e2813d4 100644 --- a/src/cli.rs +++ b/crates/obsbot4linux/src/cli.rs @@ -5,11 +5,11 @@ use std::io::Write; use clap::{Parser, Subcommand, ValueEnum}; -use obsbot4linux::camera::{Camera, Identity}; -use obsbot4linux::commands::*; -use obsbot4linux::device::Detected; -use obsbot4linux::error::{Error, Result}; -use obsbot4linux::presets::{Pose, StartupAi}; +use obsbot_device::camera::{Camera, Identity}; +use obsbot_device::commands::*; +use obsbot_device::device::Detected; +use obsbot_device::error::{Error, Result}; +use obsbot_device::presets::{Pose, StartupAi}; use serde_json::{Value, json}; #[derive(Parser)] @@ -971,7 +971,7 @@ pub fn run() -> i32 { /// `obsbot4linux detect`: exit 0 and the model (and device) of a supported camera, exit 1 and /// no output without one. Never goes through the daemon. fn detect(json: bool) -> i32 { - let Some(found) = obsbot4linux::device::detect() else { + let Some(found) = obsbot_device::device::detect() else { return 1; }; let (value, text) = detect_output(&found); @@ -996,7 +996,7 @@ fn report(json: bool, result: Result<()>) -> i32 { #[cfg(test)] mod tests { use super::*; - use obsbot4linux::models::MODELS; + use obsbot_device::registry::MODELS; fn parse(args: &str) -> std::result::Result { Cli::try_parse_from(std::iter::once("obsbot4linux").chain(args.split(' '))) diff --git a/src/client.rs b/crates/obsbot4linux/src/client.rs similarity index 97% rename from src/client.rs rename to crates/obsbot4linux/src/client.rs index ac5675f..5705b96 100644 --- a/src/client.rs +++ b/crates/obsbot4linux/src/client.rs @@ -4,7 +4,7 @@ use crate::cli::Cli; use crate::proto::{Response, socket_path}; -use obsbot4linux::error::{Error, Result}; +use obsbot_device::error::{Error, Result}; use serde_json::{Value, json}; use std::fs::OpenOptions; use std::io::{BufRead, BufReader, Write}; @@ -55,7 +55,7 @@ fn connect_or_start() -> Result { } let exe = std::env::current_exe()?; // its errors go next to the state file; without a log they are lost, not fatal - let log = obsbot4linux::state::path().with_file_name("daemon.log"); + let log = obsbot_device::state::path().with_file_name("daemon.log"); let stderr = log .parent() .map_or(Ok(()), std::fs::create_dir_all) diff --git a/src/daemon.rs b/crates/obsbot4linux/src/daemon.rs similarity index 99% rename from src/daemon.rs rename to crates/obsbot4linux/src/daemon.rs index d0831d0..b380563 100644 --- a/src/daemon.rs +++ b/crates/obsbot4linux/src/daemon.rs @@ -10,10 +10,10 @@ use clap::Parser; use nix::errno::Errno; use nix::fcntl::{Flock, FlockArg}; use nix::sys::stat::{Mode, umask}; -use obsbot4linux::camera::{CAPABILITIES, Camera, Identity, ZoneInfo}; -use obsbot4linux::error::{Error, Result}; -use obsbot4linux::state; -use obsbot4linux::streaming; +use obsbot_device::camera::{CAPABILITIES, Camera, Identity, ZoneInfo}; +use obsbot_device::error::{Error, Result}; +use obsbot_device::state; +use obsbot_device::streaming; use serde_json::{Value, json}; use std::collections::HashSet; use std::fs::{File, OpenOptions}; diff --git a/src/main.rs b/crates/obsbot4linux/src/main.rs similarity index 100% rename from src/main.rs rename to crates/obsbot4linux/src/main.rs diff --git a/src/proto.rs b/crates/obsbot4linux/src/proto.rs similarity index 100% rename from src/proto.rs rename to crates/obsbot4linux/src/proto.rs diff --git a/crates/obsbot4linux/tests/cli_surface.rs b/crates/obsbot4linux/tests/cli_surface.rs new file mode 100644 index 0000000..dcfadcc --- /dev/null +++ b/crates/obsbot4linux/tests/cli_surface.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +//! CLI surface guard of the groundwork stage (rebuild plan, stage G): the `--help` text of the +//! CLI and of every subcommand, compared with `tests/fixtures/cli-surface.txt`, which was +//! written from the 0.13 code before the workspace move, and `--version`. Only `--help` and +//! `--version` run: nothing opens a device or a socket. +//! +//! `CLI_SURFACE_BLESS=1 cargo test --test cli_surface` writes the file: once when this test is +//! added, and on `main` in a change that alters a help text on purpose. A move never writes it; +//! every other run compares. + +use std::process::Command; + +const FIXTURE: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/cli-surface.txt" +); + +fn run(args: &[&str]) -> String { + let out = Command::new(env!("CARGO_BIN_EXE_obsbot4linux")) + .args(args) + .output() + .expect("obsbot4linux runs"); + assert!(out.status.success(), "obsbot4linux {args:?} failed"); + String::from_utf8(out.stdout).expect("UTF-8 output") +} + +/// The names in the "Commands:" section of a help text: the lines indented by exactly two +/// spaces (a longer indent continues a description). +fn subcommands(help: &str) -> Vec { + help.lines() + .skip_while(|l| *l != "Commands:") + .skip(1) + .take_while(|l| l.starts_with(' ')) + .filter(|l| !l.starts_with(" ")) + .filter_map(|l| l.split_whitespace().next()) + .filter(|name| *name != "help") + .map(String::from) + .collect() +} + +fn walk(path: &mut Vec, out: &mut String) { + let mut args: Vec<&str> = path.iter().map(String::as_str).collect(); + args.push("--help"); + let help = run(&args); + out.push_str(&format!("=== obsbot4linux {}\n{help}", args.join(" "))); + for sub in subcommands(&help) { + path.push(sub); + walk(path, out); + path.pop(); + } +} + +#[test] +fn help_texts_are_unchanged() { + let mut out = String::new(); + walk(&mut Vec::new(), &mut out); + // a hidden subcommand is not listed under "Commands:" + walk(&mut vec!["gimbal".into(), "center".into()], &mut out); + if std::env::var_os("CLI_SURFACE_BLESS").is_some() { + std::fs::write(FIXTURE, &out).unwrap(); + return; + } + let recorded = std::fs::read_to_string(FIXTURE).expect("tests/fixtures/cli-surface.txt"); + assert_eq!(out, recorded, "a help text of the CLI changed"); +} + +#[test] +fn version_is_the_package_version() { + // the workspace move must keep the version the binary reports + assert_eq!( + run(&["--version"]), + format!("obsbot4linux {}\n", env!("CARGO_PKG_VERSION")) + ); +} diff --git a/crates/obsbot4linux/tests/fixtures/cli-surface.txt b/crates/obsbot4linux/tests/fixtures/cli-surface.txt new file mode 100644 index 0000000..2dcd923 --- /dev/null +++ b/crates/obsbot4linux/tests/fixtures/cli-surface.txt @@ -0,0 +1,1126 @@ +=== obsbot4linux --help +Control the OBSBOT Tiny 2 Lite + +Usage: obsbot4linux [OPTIONS] + +Commands: + daemon Run the daemon: owns the camera, serves the socket, broadcasts state changes + watch Stream state events as JSON lines and forward JSON requests from stdin (for widgets) + live-pose Ask the daemon to poll the gimbal pose for this connection (a `watch` request; the daemon polls it every second only while a connection wants it). Not usable as a one-off call + detect Exit 0 and print the model (--json: model and video device) when a supported camera is connected, exit 1 with no output when not. Enumeration only: no daemon, no camera command + status Show all settings (the camera's status block, tracking readback, zoom, image controls, remembered state) + sleep Put the camera to sleep + wake Wake the camera, or set the stored wake-up zoom (`wake zoom`) or the tracking mode at power-on (`wake mode`) + tracking AI tracking mode + speed Tracking type: standard, sport (fixed fast gain) or headroom (more room above the head) + speed-mode Fine tracking speed, from lazy to very-fast, or auto. Ignored while `speed sport` is set or the part mode is not normal + framing Framing options of the human tracking; an AI mode change overwrites them + hdr HDR + exposure Exposure: automatic metering over the whole picture (global) or on faces (face), or manual, optionally with the shutter time and gain (`image gain` sets the gain too; `image exposure_time` is refused unless the exposure is already manual) + zoom Zoom factor 1.0-4.0; with --speed the camera zooms smoothly instead of jumping + gimbal Move the gimbal to an absolute pose in degrees (switches tracking to static first), or `gimbal stop|rate` + fov Field of view (ignored by the camera while it sleeps) + led Status LED: brightness 0 (off) to 3, or `led blink on|off` + mirror Mirror the image + invert Invert view and gimbal (reverses the gimbal's pan direction) + auto-sleep Auto-sleep: "off" or a time in seconds + sleep-background Picture shown while the camera sleeps + sleep-background-mirror Mirror the sleep background + audio Microphone settings + gesture Gesture control + hand Hand tracking: which hand, return position, area, axes + zone Zone tracking + focus Focus: autofocus mode, continuous autofocus, manual focus + startup Initial state: the pose the camera moves to when it powers on + image UVC image control, e.g. "image brightness 128" (names are in "status") + preset Presets stored in the camera (slots 0-2) + factory-reset Reset the camera to factory settings (it restarts and deletes its presets) + help Print this message or the help of the given subcommand(s) + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help + -V, --version Print version +=== obsbot4linux daemon --help +Run the daemon: owns the camera, serves the socket, broadcasts state changes + +Usage: obsbot4linux daemon [OPTIONS] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux watch --help +Stream state events as JSON lines and forward JSON requests from stdin (for widgets) + +Usage: obsbot4linux watch [OPTIONS] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux live-pose --help +Ask the daemon to poll the gimbal pose for this connection (a `watch` request; the daemon polls it every second only while a connection wants it). Not usable as a one-off call + +Usage: obsbot4linux live-pose [OPTIONS] + +Arguments: + [possible values: on, off] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux detect --help +Exit 0 and print the model (--json: model and video device) when a supported camera is connected, exit 1 with no output when not. Enumeration only: no daemon, no camera command + +Usage: obsbot4linux detect [OPTIONS] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux status --help +Show all settings (the camera's status block, tracking readback, zoom, image controls, remembered state) + +Usage: obsbot4linux status [OPTIONS] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux sleep --help +Put the camera to sleep + +Usage: obsbot4linux sleep [OPTIONS] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux wake --help +Wake the camera, or set the stored wake-up zoom (`wake zoom`) or the tracking mode at power-on (`wake mode`) + +Usage: obsbot4linux wake [OPTIONS] [COMMAND] + +Commands: + zoom Stored wake-up zoom, factor 1.0-4.0 (the camera stores 0-100 percent). Effect unknown on the Tiny 2 Lite: no change at power-on or on waking from sleep + mode Tracking mode the camera starts in at power-on, not on waking from sleep (the current mode stays) + help Print this message or the help of the given subcommand(s) + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux wake zoom --help +Stored wake-up zoom, factor 1.0-4.0 (the camera stores 0-100 percent). Effect unknown on the Tiny 2 Lite: no change at power-on or on waking from sleep + +Usage: obsbot4linux wake zoom [OPTIONS] + +Arguments: + + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux wake mode --help +Tracking mode the camera starts in at power-on, not on waking from sleep (the current mode stays) + +Usage: obsbot4linux wake mode [OPTIONS] + +Arguments: + [possible values: static, normal, upper-body, close-up, headless, lower-body, hand, group] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux tracking --help +AI tracking mode + +Usage: obsbot4linux tracking [OPTIONS] + +Arguments: + [possible values: static, normal, upper-body, close-up, headless, lower-body, hand, group] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux speed --help +Tracking type: standard, sport (fixed fast gain) or headroom (more room above the head) + +Usage: obsbot4linux speed [OPTIONS] + +Arguments: + [possible values: standard, headroom, sport] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux speed-mode --help +Fine tracking speed, from lazy to very-fast, or auto. Ignored while `speed sport` is set or the part mode is not normal + +Usage: obsbot4linux speed-mode [OPTIONS] + +Arguments: + [possible values: lazy, slow, normal, fast, very-fast, auto] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux framing --help +Framing options of the human tracking; an AI mode change overwrites them + +Usage: obsbot4linux framing [OPTIONS] + +Commands: + auto-zoom Auto zoom of the human tracking on or off + size Size of the auto zoom framing + zoom-speed How fast the auto zoom follows + offset Shift the framing by a fraction of the frame, -0.16 to 0.16 + part Which part of the body the framing follows + help Print this message or the help of the given subcommand(s) + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux framing auto-zoom --help +Auto zoom of the human tracking on or off + +Usage: obsbot4linux framing auto-zoom [OPTIONS] + +Arguments: + [possible values: on, off] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux framing size --help +Size of the auto zoom framing + +Usage: obsbot4linux framing size [OPTIONS] + +Arguments: + [possible values: long-shot, medium-long-shot, medium-shot, medium-close-up, close-up, big-close-up] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux framing zoom-speed --help +How fast the auto zoom follows + +Usage: obsbot4linux framing zoom-speed [OPTIONS] + +Arguments: + [possible values: slow, medium, fast] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux framing offset --help +Shift the framing by a fraction of the frame, -0.16 to 0.16 + +Usage: obsbot4linux framing offset [OPTIONS] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --x + --direct Do not use a running daemon; talk to the camera directly + --y + -h, --help Print help +=== obsbot4linux framing part --help +Which part of the body the framing follows + +Usage: obsbot4linux framing part [OPTIONS] + +Arguments: + [possible values: normal, head-removal, lower-body] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux hdr --help +HDR + +Usage: obsbot4linux hdr [OPTIONS] + +Arguments: + [possible values: on, off] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --direct Do not use a running daemon; talk to the camera directly + -h, --help Print help +=== obsbot4linux exposure --help +Exposure: automatic metering over the whole picture (global) or on faces (face), or manual, optionally with the shutter time and gain (`image gain` sets the gain too; `image exposure_time` is refused unless the exposure is already manual) + +Usage: obsbot4linux exposure [OPTIONS] + +Arguments: + [possible values: global, face, manual] + +Options: + --json Print machine-readable JSON (errors too, as {"ok":false,"error":...}) + --time