vex_v5_serial/commands/
file.rs

1use std::{
2    io::Write,
3    str::FromStr,
4    time::{Duration, SystemTime, UNIX_EPOCH},
5};
6
7use flate2::{Compression, GzBuilder};
8use log::{debug, trace};
9
10use crate::{Connection, ConnectionType};
11
12use vex_cdc::{
13    FixedString, VEX_CRC32, Version,
14    cdc2::file::{
15        ExtensionType, FileDataReadPacket, FileDataReadPayload, FileDataReadReplyPacket,
16        FileDataWritePacket, FileDataWritePayload, FileDataWriteReplyPacket, FileExitAction,
17        FileInitOption, FileLinkPacket, FileLinkPayload, FileLinkReplyPacket, FileMetadata,
18        FileTransferExitPacket, FileTransferExitReplyPacket, FileTransferInitializePacket,
19        FileTransferInitializePayload, FileTransferInitializeReplyPacket, FileTransferOperation,
20        FileTransferTarget, FileVendor,
21    },
22};
23
24use super::Command;
25
26/// The epoch of the serial protocol's timestamps.
27pub const J2000_EPOCH: u64 = 946684800;
28
29pub fn j2000_timestamp() -> i32 {
30    let unix_timestamp_secs = SystemTime::now()
31        .duration_since(UNIX_EPOCH)
32        .expect("Time went backwards")
33        .as_secs();
34
35    (unix_timestamp_secs - J2000_EPOCH) as i32
36}
37
38pub const PROS_HOT_BIN_LOAD_ADDR: u32 = 0x7800000;
39pub const USER_PROGRAM_LOAD_ADDR: u32 = 0x3800000;
40
41pub struct DownloadFile {
42    pub file_name: FixedString<23>,
43    pub size: u32,
44    pub vendor: FileVendor,
45    pub target: FileTransferTarget,
46    pub address: u32,
47
48    pub progress_callback: Option<Box<dyn FnMut(f32) + Send>>,
49}
50impl Command for DownloadFile {
51    type Output = Vec<u8>;
52
53    async fn execute<C: Connection + ?Sized>(
54        mut self,
55        connection: &mut C,
56    ) -> Result<Self::Output, C::Error> {
57        let transfer_response = connection
58            .handshake::<FileTransferInitializeReplyPacket>(
59                Duration::from_millis(500),
60                5,
61                FileTransferInitializePacket::new(FileTransferInitializePayload {
62                    operation: FileTransferOperation::Read,
63                    target: self.target,
64                    vendor: self.vendor,
65                    options: FileInitOption::None,
66                    file_size: self.size,
67                    write_file_crc: 0,
68                    load_address: self.address,
69                    metadata: FileMetadata {
70                        extension: FixedString::from_str("ini").unwrap(),
71                        extension_type: ExtensionType::EncryptedBinary,
72                        timestamp: 0,
73                        version: Version {
74                            major: 1,
75                            minor: 0,
76                            build: 0,
77                            beta: 0,
78                        },
79                    },
80                    file_name: self.file_name,
81                }),
82            )
83            .await?;
84        let transfer_response = transfer_response.payload?;
85
86        let max_chunk_size = connection
87            .connection_type()
88            .max_chunk_size(transfer_response.window_size);
89
90        let mut data = Vec::with_capacity(transfer_response.file_size as usize);
91        let mut offset = 0;
92        loop {
93            let read = connection
94                .handshake::<FileDataReadReplyPacket>(
95                    Duration::from_millis(500),
96                    5,
97                    FileDataReadPacket::new(FileDataReadPayload {
98                        address: self.address + offset,
99                        size: max_chunk_size,
100                    }),
101                )
102                .await?;
103
104            let (_, chunk_data) = read.payload.unwrap()?;
105            offset += chunk_data.len() as u32;
106            let progress = (offset as f32 / transfer_response.file_size as f32) * 100.0;
107
108            if let Some(callback) = &mut self.progress_callback {
109                callback(progress);
110            }
111
112            if transfer_response.file_size <= offset {
113                // Since data is returned in fixed-size chunks read from flash, VEXos will sometimes read
114                // past the end of the file in the last chunk, returning whatever garbled nonsense happens
115                // to be stored next in QSPI. This is a feature™️, and something we need to handle ourselves.
116                let eof = chunk_data.len() - (offset - transfer_response.file_size) as usize;
117                data.extend(&chunk_data[0..eof]);
118                break; // we're done here
119            } else {
120                data.extend(chunk_data);
121            }
122        }
123
124        Ok(data)
125    }
126}
127
128pub struct LinkedFile {
129    pub file_name: FixedString<23>,
130    pub vendor: FileVendor,
131}
132
133pub struct UploadFile<'a> {
134    pub file_name: FixedString<23>,
135    pub metadata: FileMetadata,
136    pub vendor: FileVendor,
137    pub data: &'a [u8],
138    pub target: FileTransferTarget,
139    pub load_address: u32,
140    pub linked_file: Option<LinkedFile>,
141    pub after_upload: FileExitAction,
142
143    pub progress_callback: Option<Box<dyn FnMut(f32) + Send + 'a>>,
144}
145impl Command for UploadFile<'_> {
146    type Output = ();
147    async fn execute<C: Connection + ?Sized>(
148        mut self,
149        connection: &mut C,
150    ) -> Result<Self::Output, C::Error> {
151        debug!("Uploading file: {}", self.file_name);
152        let crc = VEX_CRC32.checksum(&self.data);
153
154        let transfer_response = connection
155            .handshake::<FileTransferInitializeReplyPacket>(
156                Duration::from_millis(500),
157                5,
158                FileTransferInitializePacket::new(FileTransferInitializePayload {
159                    operation: FileTransferOperation::Write,
160                    target: self.target,
161                    vendor: self.vendor,
162                    options: FileInitOption::Overwrite,
163                    file_size: self.data.len() as u32,
164                    load_address: self.load_address,
165                    write_file_crc: crc,
166                    metadata: self.metadata,
167                    file_name: self.file_name.clone(),
168                }),
169            )
170            .await?;
171        debug!("transfer init responded");
172        let transfer_response = transfer_response.payload?;
173
174        if let Some(linked_file) = self.linked_file {
175            connection
176                .handshake::<FileLinkReplyPacket>(
177                    Duration::from_millis(500),
178                    5,
179                    FileLinkPacket::new(FileLinkPayload {
180                        vendor: linked_file.vendor,
181                        reserved: 0,
182                        required_file: linked_file.file_name,
183                    }),
184                )
185                .await?
186                .payload?;
187        }
188
189        let window_size = transfer_response.window_size;
190
191        // The maximum packet size is 244 bytes for bluetooth
192        let max_chunk_size = connection.connection_type().max_chunk_size(window_size);
193        debug!("max_chunk_size: {}", max_chunk_size);
194
195        let mut offset = 0;
196        for chunk in self.data.chunks(max_chunk_size as _) {
197            let chunk = if chunk.len() < max_chunk_size as _ && chunk.len() % 4 != 0 {
198                let mut new_chunk = Vec::new();
199                new_chunk.extend_from_slice(chunk);
200                new_chunk.resize(chunk.len() + (4 - chunk.len() % 4), 0);
201                new_chunk
202            } else {
203                chunk.to_vec()
204            };
205            trace!("sending chunk of size: {}", chunk.len());
206            let progress = (offset as f32 / self.data.len() as f32) * 100.0;
207            if let Some(callback) = &mut self.progress_callback {
208                callback(progress);
209            }
210
211            let packet = FileDataWritePacket::new(FileDataWritePayload {
212                address: (self.load_address + offset) as _,
213                chunk_data: chunk.clone(),
214            });
215
216            // On bluetooth, we dont wait for the reply
217            if connection.connection_type() == ConnectionType::Bluetooth {
218                connection.send(packet).await?;
219            } else {
220                connection
221                    .handshake::<FileDataWriteReplyPacket>(Duration::from_millis(500), 5, packet)
222                    .await?
223                    .payload?;
224            }
225
226            offset += chunk.len() as u32;
227        }
228        if let Some(callback) = &mut self.progress_callback {
229            callback(100.0);
230        }
231
232        connection
233            .handshake::<FileTransferExitReplyPacket>(
234                Duration::from_millis(1000),
235                5,
236                FileTransferExitPacket::new(self.after_upload),
237            )
238            .await?
239            .payload?;
240
241        debug!("Successfully uploaded file: {}", self.file_name);
242        Ok(())
243    }
244}
245
246#[derive(Debug)]
247pub enum ProgramData {
248    Monolith(Vec<u8>),
249    HotCold {
250        hot: Option<Vec<u8>>,
251        cold: Option<Vec<u8>>,
252    },
253}
254
255pub struct UploadProgram<'a> {
256    pub name: String,
257    pub description: String,
258    pub icon: String,
259    pub program_type: String,
260    /// 0-indexed slot
261    pub slot: u8,
262    pub compress: bool,
263    pub data: ProgramData,
264    pub after_upload: FileExitAction,
265
266    /// Called when progress has been made on the ini file.
267    ///
268    /// 100.0 should be considered a finished upload.
269    pub ini_callback: Option<Box<dyn FnMut(f32) + Send + 'a>>,
270    /// Called when progress has been made on the monolith/hot binary
271    ///
272    /// 100.0 should be considered a finished upload.
273    pub bin_callback: Option<Box<dyn FnMut(f32) + Send + 'a>>,
274    /// Called when progress has been made on the cold library binary
275    ///
276    /// 100.0 should be considered a finished upload.
277    pub lib_callback: Option<Box<dyn FnMut(f32) + Send + 'a>>,
278}
279impl Command for UploadProgram<'_> {
280    type Output = ();
281
282    async fn execute<C: Connection + ?Sized>(
283        mut self,
284        connection: &mut C,
285    ) -> Result<Self::Output, C::Error> {
286        let base_file_name = format!("slot_{}", self.slot);
287
288        debug!("Uploading program ini file");
289
290        let ini = format!(
291            "[project]
292ide={}
293[program]
294name={}
295slot={}
296icon={}
297iconalt=
298description={}",
299            self.program_type,
300            self.name,
301            self.slot - 1,
302            self.icon,
303            self.program_type
304        );
305
306        connection
307            .execute_command(UploadFile {
308                file_name: FixedString::new(format!("{}.ini", base_file_name))?,
309                metadata: FileMetadata {
310                    extension: unsafe { FixedString::new_unchecked("ini") },
311                    extension_type: ExtensionType::default(),
312                    timestamp: j2000_timestamp(),
313                    version: Version {
314                        major: 1,
315                        minor: 0,
316                        build: 0,
317                        beta: 0,
318                    },
319                },
320                vendor: FileVendor::User,
321                data: ini.as_bytes(),
322                target: FileTransferTarget::Qspi,
323                load_address: USER_PROGRAM_LOAD_ADDR,
324                linked_file: None,
325                after_upload: FileExitAction::DoNothing,
326                progress_callback: self.ini_callback.take(),
327            })
328            .await?;
329
330        let program_bin_name = format!("{base_file_name}.bin");
331        let program_lib_name = format!("{base_file_name}_lib.bin");
332
333        let is_monolith = matches!(self.data, ProgramData::Monolith(_));
334        let (program_data, library_data) = match self.data {
335            ProgramData::HotCold { hot, cold } => (hot, cold),
336            ProgramData::Monolith(data) => (Some(data), None),
337        };
338
339        if let Some(mut library_data) = library_data {
340            debug!("Uploading cold library binary");
341
342            // Compress the file to improve upload times
343            // We don't need to change any other flags, the brain is smart enough to decompress it
344            if self.compress {
345                debug!("Compressing cold library binary");
346                compress(&mut library_data);
347                debug!("Compression complete");
348            }
349
350            connection
351                .execute_command(UploadFile {
352                    file_name: FixedString::new(program_lib_name.clone())?,
353                    metadata: FileMetadata {
354                        extension: unsafe { FixedString::new_unchecked("bin") },
355                        extension_type: ExtensionType::default(),
356                        timestamp: j2000_timestamp(),
357                        version: Version {
358                            major: 1,
359                            minor: 0,
360                            build: 0,
361                            beta: 0,
362                        },
363                    },
364                    vendor: FileVendor::User,
365                    data: &library_data,
366                    target: FileTransferTarget::Qspi,
367                    load_address: PROS_HOT_BIN_LOAD_ADDR,
368                    linked_file: None,
369                    after_upload: if is_monolith {
370                        self.after_upload
371                    } else {
372                        // we are still uploading, so the post-upload action should not yet be performed
373                        FileExitAction::DoNothing
374                    },
375                    progress_callback: self.lib_callback.take(),
376                })
377                .await?;
378        }
379
380        if let Some(mut program_data) = program_data {
381            debug!("Uploading program binary");
382
383            if self.compress {
384                debug!("Compressing program binary");
385                compress(&mut program_data);
386                debug!("Compression complete");
387            }
388
389            // Only ask the brain to link to a library if the program expects it.
390            // Monolith programs don't have libraries.
391            let linked_file = if is_monolith {
392                None
393            } else {
394                debug!("Program will be linked to cold library: {program_lib_name:?}");
395                Some(LinkedFile {
396                    file_name: FixedString::new(program_lib_name)?,
397                    vendor: FileVendor::User,
398                })
399            };
400
401            connection
402                .execute_command(UploadFile {
403                    file_name: FixedString::new(program_bin_name)?,
404                    metadata: FileMetadata {
405                        extension: unsafe { FixedString::new_unchecked("bin") },
406                        extension_type: ExtensionType::default(),
407                        timestamp: j2000_timestamp(),
408                        version: Version {
409                            major: 1,
410                            minor: 0,
411                            build: 0,
412                            beta: 0,
413                        },
414                    },
415                    vendor: FileVendor::User,
416                    data: &program_data,
417                    target: FileTransferTarget::Qspi,
418                    load_address: USER_PROGRAM_LOAD_ADDR,
419                    linked_file,
420                    after_upload: self.after_upload,
421                    progress_callback: self.bin_callback.take(),
422                })
423                .await?;
424        }
425
426        Ok(())
427    }
428}
429
430/// Apply gzip compression to the given data
431fn compress(data: &mut Vec<u8>) {
432    let mut encoder = GzBuilder::new().write(Vec::new(), Compression::default());
433    encoder.write_all(data).unwrap();
434    *data = encoder.finish().unwrap();
435}