1use embassy_sync::blocking_mutex::raw::RawMutex;
16use embassy_sync::mutex::Mutex;
17
18use umsh_journal_store::proto;
19use umsh_journal_store::record::{
20 PAGE_SIZE, PageEraser, RecordReader, RecordWriter, erase_journal_page,
21};
22
23use crate::log::debug_log;
24
25pub trait JournalFlash:
29 RecordWriter<Error = ()> + PageEraser<Error = ()> + RecordReader<Error = ()>
30{
31}
32
33impl<F> JournalFlash for F where
34 F: RecordWriter<Error = ()> + PageEraser<Error = ()> + RecordReader<Error = ()>
35{
36}
37
38pub type SharedFlash<M, F> = Mutex<M, F>;
41
42pub type BootPayload = heapless::Vec<u8, { proto::MAX_PAYLOAD }>;
44
45fn erased_journal_slot<F: JournalFlash>(
47 flash: &mut F,
48 start: u32,
49 end: u32,
50 slot_size: usize,
51) -> Option<u32> {
52 let mut address = start;
53 while address < end {
54 let mut erased = true;
55 let mut offset = 0usize;
56 while offset < slot_size {
57 let mut chunk = [0u8; 256];
58 let take = (slot_size - offset).min(chunk.len());
59 match flash.read_record(address + offset as u32, &mut chunk[..take]) {
60 Ok(()) if chunk[..take].iter().all(|byte| *byte == 0xff) => {}
61 Ok(()) => {
62 erased = false;
63 break;
64 }
65 Err(_) => {
66 debug_log(format_args!(
67 "store erased-slot read=FAILED address=0x{address:06x}"
68 ));
69 erased = false;
70 break;
71 }
72 }
73 offset += take;
74 }
75 if erased {
76 return Some(address);
77 }
78 address += slot_size as u32;
79 }
80 None
81}
82
83pub async fn journal_write_target<F: JournalFlash>(
87 flash: &mut F,
88 current: Option<u32>,
89 page0: u32,
90 slot_size: usize,
91) -> Result<u32, ()> {
92 let page1 = page0 + PAGE_SIZE;
93 let target = if let Some(current) = current {
94 let page = if current < page1 { page0 } else { page1 };
95 erased_journal_slot(
96 flash,
97 current + slot_size as u32,
98 page + PAGE_SIZE,
99 slot_size,
100 )
101 } else {
102 erased_journal_slot(flash, page0, page0 + PAGE_SIZE, slot_size)
103 };
104 match target {
105 Some(target) => Ok(target),
106 None => {
107 let page = if current.is_some_and(|slot| slot < page1) {
108 page1
109 } else {
110 page0
111 };
112 debug_log(format_args!("store erase begin page=0x{page:06x}"));
113 if erase_journal_page(flash, page).await.is_err() {
114 debug_log(format_args!("store erase=FAILED page=0x{page:06x}"));
115 return Err(());
116 }
117 debug_log(format_args!("store erase=ok page=0x{page:06x}"));
118 Ok(page)
119 }
120 }
121}
122
123pub struct ProtoStore<M: RawMutex + 'static, F: JournalFlash + 'static> {
129 flash: &'static SharedFlash<M, F>,
130 page0: u32,
132 generation: u32,
133 slot: Option<u32>,
134 walked_back_to: Option<u32>,
138}
139
140impl<M: RawMutex + 'static, F: JournalFlash + 'static> ProtoStore<M, F> {
141 pub async fn mount(
142 shared: &'static SharedFlash<M, F>,
143 page0: u32,
144 ) -> (Self, Option<BootPayload>) {
145 let mut flash = shared.lock().await;
146 let mut newest: Option<(u32, u32)> = None;
147 let mut bytes = [0u8; proto::SLOT_SIZE];
148 for page in [page0, page0 + PAGE_SIZE] {
149 let mut address = page;
150 while address < page + PAGE_SIZE {
151 if flash.read_record(address, &mut bytes).is_ok() {
152 proto::consider_slot(&mut newest, address, &bytes);
153 }
154 address += proto::SLOT_SIZE as u32;
155 }
156 }
157 let mut slot = None;
166 let mut generation = 0;
167 let mut payload: Option<BootPayload> = None;
168 if let Some((address, _)) = newest
169 && flash.read_record(address, &mut bytes).is_ok()
170 && let Some((found, bytes)) = proto::payload_bytes(&bytes)
171 {
172 slot = Some(address);
173 generation = found;
174 payload = bytes.and_then(|bytes| BootPayload::from_slice(bytes).ok());
175 }
176 drop(flash);
177 debug_log(format_args!(
178 "proto-store mount page0=0x{page0:06x} slot={:?} generation={} payload={}",
179 slot,
180 generation,
181 payload.as_ref().map_or(0, |payload| payload.len()),
182 ));
183 (
184 Self {
185 flash: shared,
186 page0,
187 generation,
188 slot,
189 walked_back_to: Some(generation),
190 },
191 payload,
192 )
193 }
194
195 pub fn flash(&self) -> &'static SharedFlash<M, F> {
201 self.flash
202 }
203
204 pub async fn older_snapshot(&mut self, out: &mut [u8]) -> Option<usize> {
214 let newer_than = self.walked_back_to?;
215 let mut flash = self.flash.lock().await;
216 let mut newest: Option<(u32, u32)> = None;
217 let mut bytes = [0u8; proto::SLOT_SIZE];
218 for page in [self.page0, self.page0 + PAGE_SIZE] {
219 let mut address = page;
220 while address < page + PAGE_SIZE {
221 if flash.read_record(address, &mut bytes).is_ok() {
222 proto::consider_older_slot(&mut newest, address, &bytes, newer_than);
223 }
224 address += proto::SLOT_SIZE as u32;
225 }
226 }
227 let (address, _) = newest?;
228 if flash.read_record(address, &mut bytes).is_err() {
229 return None;
230 }
231 drop(flash);
232 let (generation, payload) = proto::payload_bytes(&bytes)?;
233 self.walked_back_to = Some(generation);
234 let Some(payload) = payload else {
235 debug_log(format_args!("proto-store walk-back hit=tombstone"));
236 self.walked_back_to = None;
237 return None;
238 };
239 debug_log(format_args!(
240 "proto-store walk-back generation={generation} payload={}",
241 payload.len(),
242 ));
243 let len = payload.len().min(out.len());
244 out[..len].copy_from_slice(&payload[..len]);
245 (len == payload.len()).then_some(len)
246 }
247
248 pub async fn persist(&mut self, payload: &[u8]) -> Result<(), ()> {
249 if payload.len() > proto::MAX_PAYLOAD {
250 return Err(());
251 }
252 self.write(proto::RecordRef::Snapshot(payload)).await
253 }
254
255 pub async fn clear(&mut self) -> Result<(), ()> {
261 self.write(proto::RecordRef::Cleared).await
262 }
263
264 async fn write(&mut self, record: proto::RecordRef<'_>) -> Result<(), ()> {
268 let generation = self.generation.wrapping_add(1);
269 let mut flash = self.flash.lock().await;
270 let target =
271 journal_write_target(&mut *flash, self.slot, self.page0, proto::SLOT_SIZE).await?;
272 match proto::write_record(&mut *flash, target, generation, record).await {
273 Ok(()) => {
274 debug_log(format_args!(
275 "proto-store commit generation={generation} slot=0x{target:06x} cleared={}",
276 matches!(record, proto::RecordRef::Cleared),
277 ));
278 self.generation = generation;
279 self.slot = Some(target);
280 Ok(())
281 }
282 Err(_) => {
283 debug_log(format_args!(
284 "proto-store write=FAILED target=0x{target:06x}"
285 ));
286 Err(())
287 }
288 }
289 }
290}