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
26pub 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 let eof = chunk_data.len() - (offset - transfer_response.file_size) as usize;
117 data.extend(&chunk_data[0..eof]);
118 break; } 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 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 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 pub slot: u8,
262 pub compress: bool,
263 pub data: ProgramData,
264 pub after_upload: FileExitAction,
265
266 pub ini_callback: Option<Box<dyn FnMut(f32) + Send + 'a>>,
270 pub bin_callback: Option<Box<dyn FnMut(f32) + Send + 'a>>,
274 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 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 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 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
430fn 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}