Skip to main content

a8mini_camera_rs/
control.rs

1use crate::{checksum, constants};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5/// Trait for camera commands
6pub trait Command {
7    fn to_bytes(&self) -> Vec<u8>;
8}
9
10/// Trait for HTTP API queries
11pub trait HTTPQuery {
12    fn to_string(&self) -> String;
13}
14
15/// Enums for hardcoded simple commands.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum A8MiniSimpleCommand {
18    AutoCenter = 0,   // handled ACK (sta)
19    RotateUp = 1,     // handled ACK (sta)
20    RotateDown = 2,   // handled ACK (sta)
21    RotateRight = 3,  // handled ACK (sta)
22    RotateLeft = 4,   // handled ACK (sta)
23    StopRotation = 5, // handled ACK (sta)
24    ZoomIn = 6,       // handled ACK (sta)
25    ZoomOut = 7,      // handled ACK (sta)
26    ZoomMax = 8,
27    MaxZoomInformation = 9,
28    FocusIn = 10,
29    FocusOut = 11,
30    TakePicture = 12, // no ACK
31    RecordVideo = 13, // no ACK
32    Rotate100100 = 14,
33    CameraInformation = 15,
34    AutoFocus = 16, // handled ACK (sta)
35    HardwareIDInformation = 17,
36    FirmwareVersionInformation = 18,
37    SetLockMode = 19,
38    SetFollowMode = 20,
39    SetFPVMode = 21,
40    AttitudeInformation = 22,
41    SetVideoOutputHDMI = 23,
42    SetVideoOutputCVBS = 24,
43    SetVideoOutputOff = 25,
44    LaserRangefinderInformation = 26,
45    RebootCamera = 27,
46    RebootGimbal = 28,
47    Resolution4k = 29,
48    Heartbeat = 30,
49    GimbalStatus = 31,
50}
51
52impl Command for A8MiniSimpleCommand {
53    fn to_bytes(&self) -> Vec<u8> {
54        constants::HARDCODED_COMMANDS[*self as usize].to_vec()
55    }
56}
57
58/// Enums for commands that require continuous values for data field.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum A8MiniComplexCommand {
61    SetYawPitchSpeed(i8, i8),
62    SetYawPitchAngle(i16, i16),
63    SetTimeUTC(u64),
64    GetCodecSpecs(u8),                        // TODO: WIP
65    SetCodecSpecs(u8, u8, u16, u16, u16, u8), // TODO: WIP
66    RequestGimbalDataStream(u8, u8),          // gimbal data stream
67}
68
69impl Command for A8MiniComplexCommand {
70    fn to_bytes(&self) -> Vec<u8> {
71        match *self {
72            A8MiniComplexCommand::SetYawPitchSpeed(v_yaw, v_pitch) => {
73                let mut byte_arr: Vec<u8> = vec![0x55, 0x66, 0x01, 0x02, 0x00, 0x00, 0x00, 0x07];
74
75                byte_arr.push(v_yaw.clamp(-100, 100) as u8);
76                byte_arr.push(v_pitch.clamp(-100, 100) as u8);
77
78                byte_arr.extend_from_slice(&checksum::crc16_calc(&byte_arr, 0));
79
80                byte_arr
81            }
82            A8MiniComplexCommand::SetYawPitchAngle(theta_yaw, theta_pitch) => {
83                let mut byte_arr: Vec<u8> = vec![0x55, 0x66, 0x01, 0x04, 0x00, 0x00, 0x00, 0x0e];
84
85                byte_arr.extend_from_slice(&theta_yaw.clamp(-1350, 1350).to_le_bytes());
86                byte_arr.extend_from_slice(&theta_pitch.clamp(-900, 250).to_le_bytes());
87
88                byte_arr.extend_from_slice(&checksum::crc16_calc(&byte_arr, 0));
89
90                byte_arr
91            }
92            A8MiniComplexCommand::SetTimeUTC(timestamp) => {
93                let mut byte_arr: Vec<u8> = vec![0x55, 0x66, 0x01, 0x04, 0x00, 0x00, 0x00, 0x30];
94
95                byte_arr.extend_from_slice(&timestamp.to_le_bytes());
96
97                byte_arr
98            }
99            A8MiniComplexCommand::GetCodecSpecs(stream_type) => {
100                let mut byte_arr: Vec<u8> = vec![0x55, 0x66, 0x01, 0x04, 0x00, 0x00, 0x00, 0x20];
101
102                byte_arr.extend_from_slice(&stream_type.clamp(0, 2).to_le_bytes());
103
104                byte_arr
105            }
106            A8MiniComplexCommand::SetCodecSpecs(
107                stream_type,
108                video_enc_type,
109                resolution_l,
110                resolution_h,
111                video_bitrate,
112                _,
113            ) => {
114                let mut byte_arr: Vec<u8> = vec![0x55, 0x66, 0x01, 0x04, 0x00, 0x00, 0x00, 0x21];
115
116                byte_arr.extend_from_slice(&stream_type.clamp(0, 2).to_le_bytes());
117                byte_arr.extend_from_slice(&video_enc_type.clamp(1, 2).to_le_bytes());
118
119                // TODO: make sure resolution_l and resolution_h are clamped to only 1920/1280 and 1080/720 respectively
120                byte_arr.extend_from_slice(&resolution_l.to_le_bytes());
121                byte_arr.extend_from_slice(&resolution_h.to_le_bytes());
122
123                // TODO: make sure video bitrate is reasonable
124                byte_arr.extend_from_slice(&video_bitrate.to_le_bytes());
125
126                byte_arr
127            }
128            // implementation for 0x25 Request Gimbal Data Stream
129            A8MiniComplexCommand::RequestGimbalDataStream(data_type, data_freq) => {
130                // Header (STX + CTRL + DataLen=2 + Seq=0 + CmdID=0x25)
131                let mut byte_arr: Vec<u8> = vec![0x55, 0x66, 0x01, 0x02, 0x00, 0x00, 0x00, 0x25];
132                
133                byte_arr.push(data_type);
134                byte_arr.push(data_freq);
135                
136                byte_arr.extend_from_slice(&checksum::crc16_calc(&byte_arr, 0));
137                byte_arr
138            }
139        }
140    }
141}
142
143/// Enums for simple HTTP queries
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum A8MiniSimpleHTTPQuery {
146    GetDirectoriesPhotos,
147    GetDirectoriesVideos,
148    GetMediaCountPhotos,
149    GetMediaCountVideos,
150}
151
152impl HTTPQuery for A8MiniSimpleHTTPQuery {
153    fn to_string(&self) -> String {
154        match *self {
155            A8MiniSimpleHTTPQuery::GetDirectoriesPhotos => "http://192.168.144.25:82/cgi-bin/media.cgi/api/v1/getdirectories?media_type=0".to_string(),
156            A8MiniSimpleHTTPQuery::GetDirectoriesVideos => "http://192.168.144.25:82/cgi-bin/media.cgi/api/v1/getdirectories?media_type=1".to_string(),
157            A8MiniSimpleHTTPQuery::GetMediaCountPhotos => "http://192.168.144.25:82/cgi-bin/media.cgi/api/v1/getmediacount?media_type=0&path=101SIYI_IMG".to_string(),
158            A8MiniSimpleHTTPQuery::GetMediaCountVideos => "http://192.168.144.25:82/cgi-bin/media.cgi/api/v1/getmediacount?media_type=1&path=100SIYI_VID".to_string(),
159        }
160    }
161}
162
163/// Enums for complex HTTP queries
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum A8MiniComplexHTTPQuery {
166    GetPhoto(u32),
167    GetVideo(u32),
168}
169
170impl HTTPQuery for A8MiniComplexHTTPQuery {
171    fn to_string(&self) -> String {
172        match *self {
173            A8MiniComplexHTTPQuery::GetPhoto(photo_ind) => format!(
174                "http://192.168.144.25:82/photo/101SIYI_IMG/IMG_{:0>4}.jpg",
175                photo_ind
176            ),
177            A8MiniComplexHTTPQuery::GetVideo(video_ind) => format!(
178                "http://192.168.144.25:82/photo/100SIYI_VID/REC_{:0>4}.mp4",
179                video_ind
180            ),
181        }
182    }
183}
184
185/// Response json format
186#[derive(Debug, Serialize, Deserialize)]
187pub struct HTTPResponse {
188    pub code: i32,
189    pub data: HTTPResponseData,
190    pub success: bool,
191    pub message: String,
192}
193
194/// Response json data format
195#[derive(Debug, Serialize, Deserialize)]
196pub struct HTTPResponseData {
197    pub media_type: i32,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub directories: Option<String>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub path: Option<String>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub start: Option<i32>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub count: Option<i32>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub list: Option<String>,
208}
209
210#[derive(Debug, PartialEq, Eq, Deserialize)]
211pub struct A8MiniFirmwareVersion {
212    // Camera Code Version (Bytes 8-11)
213    pub code_ver_byte0: u8, // Patch
214    pub code_ver_byte1: u8, // Minor
215    pub code_ver_byte2: u8, // Major
216    pub code_ver_byte3: u8, // Rev
217
218    // Gimbal Version (Bytes 12-15)
219    pub gimbal_ver_byte0: u8, // Patch (e.g. 4)
220    pub gimbal_ver_byte1: u8, // Minor (e.g. 4)
221    pub gimbal_ver_byte2: u8, // Major (e.g. 0)
222    pub gimbal_ver_byte3: u8, // Rev   (e.g. 115)
223}
224
225impl fmt::Display for A8MiniFirmwareVersion {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        // Display format: Major.Minor.Patch
228        write!(
229            f,
230            "FIRMWARE VERSION:\n\tCamera: {}.{}.{}\n\tGimbal: {}.{}.{} (Build {})",
231            // Camera
232            self.code_ver_byte2, self.code_ver_byte1, self.code_ver_byte0,
233            // Gimbal: Byte2 is Major (0), Byte1 is Minor (4), Byte0 is Patch (4)
234            self.gimbal_ver_byte2, self.gimbal_ver_byte1, self.gimbal_ver_byte0, self.gimbal_ver_byte3
235        )
236    }
237}
238/// Camera attitude information
239#[derive(Debug, PartialEq, Eq, Deserialize)]
240pub struct A8MiniAttitude {
241    pub theta_yaw: i16,
242    pub theta_pitch: i16,
243    pub theta_roll: i16,
244    pub v_yaw: i16,
245    pub v_pitch: i16,
246    pub v_roll: i16,
247}
248
249impl fmt::Display for A8MiniAttitude {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        let yaw_deg = self.theta_yaw as f32 / 10.0;
252        let pitch_deg = self.theta_pitch as f32 / 10.0;
253        let roll_deg = self.theta_roll as f32 / 10.0;
254
255        write!(
256            f,
257            "GIMBAL ATTITUDE:\n\tYaw:   {:.1}°\n\tPitch: {:.1}°\n\tRoll:  {:.1}°\n\t(Speeds: Y={}, P={}, R={})",
258            yaw_deg, pitch_deg, roll_deg, self.v_yaw, self.v_pitch, self.v_roll
259        )
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_complex_command_creation_angle() {
269        let computed_command = A8MiniComplexCommand::SetYawPitchAngle(130, -20).to_bytes();
270        let expected_command: [u8; 14] = [
271            0x55, 0x66, 0x01, 0x04, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x82, 0xff, 0xec, 0x8f, 0xad,
272        ];
273        assert_eq!(computed_command, expected_command);
274    }
275
276    #[test]
277    fn test_complex_command_creation_speed() {
278        let computed_command = A8MiniComplexCommand::SetYawPitchSpeed(104, -20).to_bytes();
279        let expected_command: [u8; 12] = [
280            0x55, 0x66, 0x01, 0x02, 0x00, 0x00, 0x00, 0x07, 0x64, 0xec, 0xbd, 0xdf,
281        ];
282        assert_eq!(computed_command, expected_command);
283    }
284
285    #[test]
286    fn test_byte_deserialization() {
287        let attitude_bytes: &[u8] = &[
288            0x28, 0x00, 0x32, 0x00, 0x3c, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00,
289        ];
290
291        //little endian deserialize
292        let computed_attitude_info: A8MiniAttitude = bincode::deserialize(attitude_bytes).unwrap();
293
294        let expected_attitude_info = A8MiniAttitude {
295            theta_yaw: 40,
296            theta_pitch: 50,
297            theta_roll: 60,
298            v_yaw: 4,
299            v_pitch: 5,
300            v_roll: 6,
301        };
302
303        assert_eq!(computed_attitude_info, expected_attitude_info);
304    }
305}