update/
messages.rs

1// SPDX-FileCopyrightText: 2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#[derive(Debug, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
5#[event(ProgressUpdate)]
6pub struct SubscribeUpdateProgress;
7
8#[derive(Debug, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
9pub struct StartUpdate {
10    pub release_paths: Vec<String>,
11}
12
13#[derive(Debug, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
14pub struct ContinueUpdate;
15
16#[derive(Debug, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
17#[response(Result<String, crate::Error>)]
18pub struct FirmwareVersion;
19
20#[derive(Debug, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
21pub struct ApplyDownloadedUpdate;
22
23#[derive(Debug, server::Message)]
24#[response(bool)]
25pub struct GetUpdateApplied;
26
27#[derive(Debug, server::Message)]
28pub struct ClearUpdateApplied;
29
30#[derive(Debug, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
31#[response(UpdateStatus)]
32pub struct GetUpdateStatus;
33
34/// Status of the update system, used to determine if an update can be applied.
35#[derive(Clone, Debug, Default, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
36pub struct UpdateStatus {
37    /// Whether there is a downloaded update ready to apply.
38    pub downloaded_update: bool,
39    /// Whether there is an update that was interrupted by reboot and needs to continue.
40    pub needs_continue: bool,
41    /// Whether a firmware update is currently being installed.
42    pub installing: bool,
43    /// Whether the battery level is sufficient for an update.
44    pub sufficient_battery: bool,
45}
46
47#[derive(Clone, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
48pub enum ProgressUpdate {
49    DownloadProgress(DownloadProgress),
50    // firmware files have been downloaded and are ready to apply
51    DownloadComplete,
52    // install progress
53    InstallProgress(InstallProgress),
54    // need reboot mid-update
55    Rebooting,
56    // completed update, and is about to reboot
57    Done,
58    InstallError(crate::Error),
59    DownloadError(crate::DownloadError),
60}
61
62#[derive(Debug, Clone, Copy, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
63pub struct DownloadProgress {
64    pub patches_total: u32,
65    pub patches_complete: u32,
66    pub chunks_received: u32,
67    pub total_chunks: u32,
68}
69
70impl DownloadProgress {
71    pub fn is_start(&self) -> bool { self.patches_complete == 0 && self.chunks_received == 0 }
72
73    pub fn completion_percentage(&self) -> u32 {
74        if self.total_chunks == 0 {
75            return 0;
76        }
77
78        self.chunks_received.saturating_mul(100).saturating_div(self.total_chunks).min(100)
79    }
80}
81
82#[derive(Clone, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
83pub struct InstallProgress {
84    pub patches: Vec<PatchProgress>,
85    pub firmware_copy: FirmwareCopyProgress,
86}
87
88#[derive(Clone, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
89pub struct FirmwareCopyProgress {
90    pub copied_bytes: u64,
91    pub total_bytes: u64,
92}
93
94/// Progress information for a single patch/release file
95#[derive(Clone, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
96pub struct PatchProgress {
97    /// Size of the patch file in bytes
98    pub file_size: u64,
99    pub total_actions: u32,
100    pub completed_actions: u32,
101    /// Whether this patch requires a reboot after application
102    pub requires_reboot: bool,
103}
104
105impl InstallProgress {
106    const COPY_BYTES_PER_SECOND: f64 = 7.0 * 1024.0 * 1024.0;
107    const PATCH_OVERHEAD_SECONDS: f64 = 2.0;
108    const SECONDS_PER_ACTION: f64 = 1.5;
109    const SECONDS_PER_MB: f64 = 5.0;
110
111    pub fn action_completed(&mut self) {
112        if let Some(patch) = self.patches.iter_mut().find(|p| p.completed_actions < p.total_actions) {
113            patch.completed_actions += 1;
114        }
115    }
116
117    pub fn set_firmware_copy(&mut self, progress: FirmwareCopyProgress) { self.firmware_copy = progress; }
118
119    pub fn estimate_time_remaining_secs(&self) -> u64 {
120        let total = self.time_total_secs();
121        let completed = self.time_completed_secs();
122        total.saturating_sub(completed)
123    }
124
125    pub fn completion_percentage(&self) -> u32 {
126        let total = self.time_total_secs();
127        if total == 0 {
128            return 100;
129        }
130
131        let completed = self.time_completed_secs();
132        ((completed as f64 / total as f64) * 100.0).min(99.0) as u32
133    }
134
135    pub fn time_total_secs(&self) -> u64 {
136        let mut total = 0.0;
137
138        let copy_time = self.firmware_copy.total_bytes as f64 / Self::COPY_BYTES_PER_SECOND;
139        total += copy_time;
140
141        for (idx, patch) in self.patches.iter().enumerate() {
142            total += Self::PATCH_OVERHEAD_SECONDS;
143            let mb = patch.file_size as f64 / (1024.0 * 1024.0);
144            total += mb * Self::SECONDS_PER_MB;
145            total += patch.total_actions as f64 * Self::SECONDS_PER_ACTION;
146
147            if patch.requires_reboot && idx < self.patches.len() - 1 {
148                total += copy_time;
149            }
150        }
151
152        total as u64
153    }
154
155    pub fn time_completed_secs(&self) -> u64 {
156        let mut completed = 0.0;
157
158        let copy_time = self.firmware_copy.copied_bytes as f64 / Self::COPY_BYTES_PER_SECOND;
159        completed += copy_time;
160
161        for (idx, patch) in self.patches.iter().enumerate() {
162            let mb = patch.file_size as f64 / (1024.0 * 1024.0);
163            let file_work = mb * Self::SECONDS_PER_MB;
164            let action_work = patch.total_actions as f64 * Self::SECONDS_PER_ACTION;
165
166            if patch.completed_actions >= patch.total_actions {
167                completed += Self::PATCH_OVERHEAD_SECONDS + file_work + action_work;
168            } else if patch.completed_actions > 0 {
169                completed += Self::PATCH_OVERHEAD_SECONDS + file_work;
170                completed += patch.completed_actions as f64 * Self::SECONDS_PER_ACTION;
171            }
172
173            if patch.requires_reboot && idx < self.patches.len() - 1 {
174                if patch.completed_actions >= patch.total_actions {
175                    let reboot_copy_time =
176                        self.firmware_copy.total_bytes as f64 / Self::COPY_BYTES_PER_SECOND;
177                    completed += reboot_copy_time;
178                }
179            }
180        }
181
182        completed as u64
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn action_completed() {
192        let mut progress = InstallProgress {
193            patches: vec![PatchProgress {
194                file_size: 1024,
195                total_actions: 5,
196                completed_actions: 2,
197                requires_reboot: false,
198            }],
199            firmware_copy: FirmwareCopyProgress { copied_bytes: 0, total_bytes: 0 },
200        };
201
202        progress.action_completed();
203        assert_eq!(progress.patches[0].completed_actions, 3);
204
205        progress.action_completed();
206        assert_eq!(progress.patches[0].completed_actions, 4);
207    }
208
209    #[test]
210    fn estimate_time_remaining() {
211        let progress = InstallProgress {
212            patches: vec![
213                PatchProgress {
214                    file_size: 5_242_880,
215                    total_actions: 10,
216                    completed_actions: 5,
217                    requires_reboot: false,
218                },
219                PatchProgress {
220                    file_size: 1_048_576,
221                    total_actions: 5,
222                    completed_actions: 0,
223                    requires_reboot: true,
224                },
225                PatchProgress {
226                    file_size: 10_485_760,
227                    total_actions: 8,
228                    completed_actions: 0,
229                    requires_reboot: false,
230                },
231            ],
232            firmware_copy: FirmwareCopyProgress { copied_bytes: 10_485_760, total_bytes: 10_485_760 },
233        };
234
235        let completed = progress.time_completed_secs();
236        let total = progress.time_total_secs();
237        let remaining = progress.estimate_time_remaining_secs();
238
239        assert!(total > 0);
240        assert!(completed > 0);
241        assert_eq!(remaining, total.saturating_sub(completed));
242    }
243
244    #[test]
245    fn completion_percentage() {
246        let progress = InstallProgress {
247            patches: vec![PatchProgress {
248                file_size: 1_048_576,
249                total_actions: 10,
250                completed_actions: 5,
251                requires_reboot: false,
252            }],
253            firmware_copy: FirmwareCopyProgress { copied_bytes: 10_485_760, total_bytes: 10_485_760 },
254        };
255
256        let percentage = progress.completion_percentage();
257        assert!(percentage > 0 && percentage < 100);
258    }
259
260    #[test]
261    fn download_completion_percentage() {
262        let progress = DownloadProgress {
263            patches_total: 2,
264            patches_complete: 0,
265            chunks_received: 34,
266            total_chunks: 100,
267        };
268
269        assert_eq!(progress.completion_percentage(), 34);
270    }
271
272    #[test]
273    fn download_completion_percentage_handles_zero_total() {
274        let progress =
275            DownloadProgress { patches_total: 2, patches_complete: 0, chunks_received: 0, total_chunks: 0 };
276
277        assert_eq!(progress.completion_percentage(), 0);
278    }
279}