Skip to main content

skydroid_protocol/
command.rs

1//! Command builders for the #TP camera protocol. Allocation-free.
2use crate::hex::{byte2hex, int2hex, num_to_hex8, short2hex};
3pub trait Command { fn len(&self) -> usize; fn write(&self, out: &mut [u8]) -> Option<usize>; }
4#[inline] fn put(out: &mut [u8], b: &[u8]) -> Option<usize> { let n = b.len(); if out.len() < n { return None; } out[..n].copy_from_slice(b); Some(n) }
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Ptz { Up, Down, Left, Right, Stop, XAdd, XReduce, YAdd, YReduce, ZAdd, ZReduce, FocusAdd, FocusReduce, HorizontalCalibration, VerticalCalibration, Calibration, FollowSwitch, BackMid, Inversion, Hoisting, LockHead, ClearAdjust, Follow }
7impl Command for Ptz {
8    fn len(&self) -> usize { 12 }
9    fn write(&self, out: &mut [u8]) -> Option<usize> {
10        let code: &[u8] = match self { Ptz::Up => b"00", Ptz::Down => b"01", Ptz::Left => b"02", Ptz::Right => b"03", Ptz::Stop => b"04", Ptz::XAdd => b"05", Ptz::XReduce => b"06", Ptz::YAdd => b"07", Ptz::YReduce => b"08", Ptz::ZAdd => b"09", Ptz::ZReduce => b"0A", Ptz::FocusAdd => b"0B", Ptz::FocusReduce => b"0C", Ptz::HorizontalCalibration => b"0D", Ptz::VerticalCalibration => b"0E", Ptz::Calibration => b"0F", Ptz::FollowSwitch => b"10", Ptz::BackMid => b"11", Ptz::Inversion => b"12", Ptz::Hoisting => b"13", Ptz::LockHead => b"14", Ptz::ClearAdjust => b"00", Ptz::Follow => b"00" };
11        let mut tmp = [0u8; 12]; tmp[..10].copy_from_slice(b"#TPUG2wPTZ"); tmp[10..].copy_from_slice(code); put(out, &tmp)
12    }
13}
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Move { PitchUp, PitchDown, PanRight, PanLeft }
16impl Command for Move { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { let s: &[u8; 12] = match self { Move::PitchUp => b"#TPUG2wGSY10", Move::PitchDown => b"#TPUG2wGSYF0", Move::PanRight => b"#TPUG2wGSP10", Move::PanLeft => b"#TPUG2wGSPF0" }; put(out, s) } }
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Axis { Pan, Pitch }
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct SpeedMove { pub axis: Axis, pub speed: i32 }
21impl SpeedMove { pub fn new(axis: Axis, speed: i32) -> Self { Self { axis, speed: speed.clamp(-99, 99) } } }
22impl Command for SpeedMove { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { let p: &[u8] = match self.axis { Axis::Pan => b"#TPUG2wGSP", Axis::Pitch => b"#TPUG2wGSY" }; let h = int2hex(self.speed); if out.len() < 12 { return None; } out[..10].copy_from_slice(p); out[10] = h[0]; out[11] = h[1]; Some(12) } }
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AngleAxis { Pitch, Roll, Yaw }
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct AngleControl { pub axis: AngleAxis, pub degrees: i32 }
27impl AngleControl { pub fn new(axis: AngleAxis, degrees: i32) -> Self { Self { axis, degrees } } }
28impl Command for AngleControl {
29    fn len(&self) -> usize { 16 }
30    fn write(&self, out: &mut [u8]) -> Option<usize> {
31        let code: &[u8] = match self.axis { AngleAxis::Pitch => b"GAP", AngleAxis::Roll => b"GAR", AngleAxis::Yaw => b"GAY" };
32        let scaled = (self.degrees * 100).clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16;
33        let h = short2hex(scaled);
34        if out.len() < 16 { return None; }
35        out[0..7].copy_from_slice(b"#TPUG6w"); out[7..10].copy_from_slice(code); out[10..14].copy_from_slice(&h); out[14..16].copy_from_slice(b"10"); Some(16)
36    }
37}
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Zoom { Stop, In, Out }
40impl Command for Zoom { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { let s: &[u8; 12] = match self { Zoom::Stop => b"#TPUM2wZMC00", Zoom::In => b"#TPUM2wZMC01", Zoom::Out => b"#TPUM2wZMC02" }; put(out, s) } }
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Focus { Stop, Add, Reduce }
43impl Command for Focus { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { let s: &[u8; 12] = match self { Focus::Stop => b"#TPUM2wFCC00", Focus::Add => b"#TPUM2wFCC01", Focus::Reduce => b"#TPUM2wFCC02" }; put(out, s) } }
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Record { Stop, Start, Flip }
46impl Command for Record { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { let s: &[u8; 12] = match self { Record::Stop => b"#TPUD2wREC00", Record::Start => b"#TPUD2wREC01", Record::Flip => b"#TPUD2wREC0A" }; put(out, s) } }
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Capture { TakePicture }
49impl Command for Capture { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { put(out, b"#TPUD2wCAP01") } }
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Query { Version, VideoConfig, RecordState, VideoEffect, Ip, Gateway }
52impl Command for Query { fn len(&self) -> usize { 12 } fn write(&self, out: &mut [u8]) -> Option<usize> { let s: &[u8; 12] = match self { Query::Version => b"#TPUD2rVER00", Query::VideoConfig => b"#TPUD2rVOM00", Query::RecordState => b"#TPUD2rREC00", Query::VideoEffect => b"#TPUD2rIQE00", Query::Ip => b"#TPUD2rIPV00", Query::Gateway => b"#TPUD2rGTW00" }; put(out, s) } }
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum System<'a> { Reset, SetTime { hhmmss: &'a str, ddmmyy: &'a str }, SetGateway(&'a str), SetIp(&'a str), ResetIpAndGateway { ip: &'a str, gateway: &'a str }, Raw(&'a [u8]) }
55impl<'a> Command for System<'a> {
56    fn len(&self) -> usize { match self {
57        System::Reset => 12,
58        System::SetTime { hhmmss, ddmmyy } => 11 + hhmmss.len() + 3 + ddmmyy.len(),
59        System::SetGateway(gw) => 10 + gw.len(),
60        System::SetIp(ip) => 10 + ip.len(),
61        System::ResetIpAndGateway { ip, gateway } => 17 + ip.len() + 8 + gateway.len(),
62        System::Raw(b) => b.len(),
63    } }
64    fn write(&self, out: &mut [u8]) -> Option<usize> { match self {
65        System::Reset => put(out, b"#TPUD2wRTF01"),
66        System::SetTime { hhmmss, ddmmyy } => { let n = 11 + hhmmss.len() + 3 + ddmmyy.len(); if out.len() < n { return None; } let mut i = 0; i += put(&mut out[i..], b"#TPUDFwTIM")?; i += put(&mut out[i..], hhmmss.as_bytes())?; i += put(&mut out[i..], b".00")?; put(&mut out[i..], ddmmyy.as_bytes())?; Some(n) }
67        System::SetGateway(gw) => { let n = 10 + gw.len(); if out.len() < n { return None; } let mut i = 0; i += put(&mut out[i..], b"#TPUDCwGTW")?; put(&mut out[i..], gw.as_bytes())?; Some(n) }
68        System::SetIp(ip) => { let h = int2hex(ip.len() as i32); let n = 10 + ip.len(); if out.len() < n { return None; } let mut i = 0; i += put(&mut out[i..], b"#TPUD")?; out[i] = h[1]; i += 1; i += put(&mut out[i..], b"wIPV")?; put(&mut out[i..], ip.as_bytes())?; Some(n) }
69        System::ResetIpAndGateway { ip, gateway } => { let n = 17 + ip.len() + 8 + gateway.len(); if out.len() < n { return None; } let mut i = 0; i += put(&mut out[i..], b"#TPUD2wRST0163ip:")?; i += put(&mut out[i..], ip.as_bytes())?; i += put(&mut out[i..], b"gateway:")?; put(&mut out[i..], gateway.as_bytes())?; Some(n) }
70        System::Raw(b) => put(out, b),
71    } }
72}
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct VideoConfig { pub hdr: bool, pub mirror: bool, pub fps: u8, pub bitrate_byte: u8, pub value: u16 }
75impl VideoConfig { pub fn new(hdr: bool, mirror: bool, fps: u8, bitrate_byte: u8, value: u16) -> Self { Self { hdr, mirror, fps, bitrate_byte, value } } }
76impl Command for VideoConfig { fn len(&self) -> usize { 21 } fn write(&self, out: &mut [u8]) -> Option<usize> {
77    let hdr = if self.hdr { b'1' } else { b'0' }; let mirror = if self.mirror { b'1' } else { b'0' };
78    let fps = byte2hex(self.fps); let bitrate = byte2hex(self.bitrate_byte); let value = num_to_hex8(self.value);
79    if out.len() < 21 { return None; }
80    out[0..10].copy_from_slice(b"#TPUDBwVOM");
81    out[10] = hdr; out[11] = mirror; out[12] = fps[0]; out[13] = fps[1]; out[14] = bitrate[0]; out[15] = bitrate[1];
82    out[16] = value[0]; out[17] = value[1]; out[18] = value[2]; out[19] = value[3]; out[20] = b'1'; Some(21)
83} }
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct VideoEffectConfig { pub tone: u8, pub brightness: u8, pub saturation: u8, pub contrast: u8, pub sharpness: u8, pub style: u8 }
86impl VideoEffectConfig { pub fn new(tone: u8, brightness: u8, saturation: u8, contrast: u8, sharpness: u8, style: u8) -> Self { Self { tone, brightness, saturation, contrast, sharpness, style } } }
87fn style_digits(s: u8) -> usize { if s >= 10 { 2 } else { 1 } }
88impl Command for VideoEffectConfig {
89    fn len(&self) -> usize { 10 + 10 + style_digits(self.style) }
90    fn write(&self, out: &mut [u8]) -> Option<usize> {
91        let n = self.len();
92        if out.len() < n { return None; }
93        out[0..10].copy_from_slice(b"#TPUDBwIQE");
94        let vals = [int2hex(i32::from(self.tone)), int2hex(i32::from(self.brightness)), int2hex(i32::from(self.saturation)), int2hex(i32::from(self.contrast)), int2hex(i32::from(self.sharpness))];
95        for (i, v) in vals.iter().enumerate() { out[10 + i * 2] = v[0]; out[11 + i * 2] = v[1]; }
96        let style = self.style; let mut i = 20;
97        if style >= 10 { out[i] = b'0' + style / 10; i += 1; }
98        out[i] = b'0' + style % 10;
99        Some(n)
100    }
101}
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::packet;
106    fn verify_cmd<C: Command>(c: &C, expect: &[u8]) {
107        let mut buf = [0u8; 64];
108        let n = c.len();
109        assert_eq!(c.write(&mut buf).unwrap(), n);
110        assert!(buf[..n].starts_with(expect), "prefix mismatch");
111        let mut pkt = [0u8; 64];
112        let pn = packet::build(&buf[..n], &mut pkt).unwrap();
113        assert!(packet::verify(&pkt[..pn]));
114    }
115    #[test] fn ptz() { verify_cmd(&Ptz::Left, b"#TPUG2wPTZ02"); verify_cmd(&Ptz::Up, b"#TPUG2wPTZ00"); verify_cmd(&Ptz::Stop, b"#TPUG2wPTZ04"); }
116    #[test] fn mv() { verify_cmd(&Move::PitchUp, b"#TPUG2wGSY10"); verify_cmd(&Move::PanLeft, b"#TPUG2wGSPF0"); }
117    #[test] fn speed() { verify_cmd(&SpeedMove::new(Axis::Pitch, 16), b"#TPUG2wGSY10"); verify_cmd(&SpeedMove::new(Axis::Pitch, -16), b"#TPUG2wGSYF0"); }
118    #[test] fn angle() { verify_cmd(&AngleControl::new(AngleAxis::Pitch, 90), b"#TPUG6wGAP232810"); verify_cmd(&AngleControl::new(AngleAxis::Yaw, 0), b"#TPUG6wGAY000010"); verify_cmd(&AngleControl::new(AngleAxis::Roll, -90), b"#TPUG6wGARDCD810"); }
119    #[test] fn zoomfocus() { verify_cmd(&Zoom::In, b"#TPUM2wZMC01"); verify_cmd(&Focus::Add, b"#TPUM2wFCC01"); verify_cmd(&Record::Start, b"#TPUD2wREC01"); verify_cmd(&Capture::TakePicture, b"#TPUD2wCAP01"); }
120    #[test] fn quer() { verify_cmd(&Query::Version, b"#TPUD2rVER00"); verify_cmd(&Query::VideoConfig, b"#TPUD2rVOM00"); }
121    #[test] fn sys() {
122        verify_cmd(&System::Reset, b"#TPUD2wRTF01");
123        verify_cmd(&System::SetTime { hhmmss: "123456", ddmmyy: "010225" }, b"#TPUDFwTIM123456.00010225");
124        verify_cmd(&System::SetGateway("192.168.144.1"), b"#TPUDCwGTW192.168.144.1");
125        verify_cmd(&System::SetIp("192.168.144.108"), b"#TPUDFwIPV192.168.144.108");
126        verify_cmd(&System::ResetIpAndGateway { ip: "1.2.3.4", gateway: "1.2.3.5" }, b"#TPUD2wRST0163ip:1.2.3.4gateway:1.2.3.5");
127    }
128    #[test] fn vid() {
129        // VideoConfig: "#TPUDBwVOM" + hdr(1) + mirror(0) + fps 0x0F + bitrate 0x01 + value 0x0123 + "1"
130        verify_cmd(&VideoConfig::new(true, false, 15, 0x01, 0x0123), b"#TPUDBwVOM10");
131        // VideoEffectConfig: "#TPUDBwIQE" + 00 01 02 03 04 + style decimal "5"
132        verify_cmd(&VideoEffectConfig::new(0, 1, 2, 3, 4, 5), b"#TPUDBwIQE0001020304");
133    }
134    #[test] fn small() { let mut b = [0u8; 4]; assert_eq!(Ptz::Left.write(&mut b), None); }
135}