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 latest: Option<(u32, proto::Stored)> = None;
147 for page in [page0, page0 + PAGE_SIZE] {
148 let mut address = page;
149 while address < page + PAGE_SIZE {
150 let mut bytes = [0u8; proto::SLOT_SIZE];
151 if flash.read_record(address, &mut bytes).is_ok() {
152 latest = proto::consider_record(latest, address, &bytes);
153 }
154 address += proto::SLOT_SIZE as u32;
155 }
156 }
157 drop(flash);
158 let (slot, generation, payload) = match latest {
161 Some((slot, stored)) => {
162 let payload = match stored.record {
163 proto::Record::Snapshot(payload) => Some(payload),
164 proto::Record::Cleared => None,
165 };
166 (Some(slot), stored.generation, payload)
167 }
168 None => (None, 0, None),
169 };
170 debug_log(format_args!(
171 "proto-store mount page0=0x{page0:06x} slot={:?} generation={} payload={}",
172 slot,
173 generation,
174 payload.as_ref().map_or(0, |payload| payload.len()),
175 ));
176 (
177 Self {
178 flash: shared,
179 page0,
180 generation,
181 slot,
182 walked_back_to: Some(generation),
183 },
184 payload,
185 )
186 }
187
188 pub fn flash(&self) -> &'static SharedFlash<M, F> {
194 self.flash
195 }
196
197 pub async fn older_snapshot(&mut self, out: &mut [u8]) -> Option<usize> {
207 let newer_than = self.walked_back_to?;
208 let mut flash = self.flash.lock().await;
209 let mut latest: Option<(u32, proto::Stored)> = None;
210 for page in [self.page0, self.page0 + PAGE_SIZE] {
211 let mut address = page;
212 while address < page + PAGE_SIZE {
213 let mut bytes = [0u8; proto::SLOT_SIZE];
214 if flash.read_record(address, &mut bytes).is_ok() {
215 latest = proto::consider_older_record(latest, address, &bytes, newer_than);
216 }
217 address += proto::SLOT_SIZE as u32;
218 }
219 }
220 drop(flash);
221 let (_, stored) = latest?;
222 self.walked_back_to = Some(stored.generation);
223 let proto::Record::Snapshot(payload) = stored.record else {
224 debug_log(format_args!("proto-store walk-back hit=tombstone"));
225 self.walked_back_to = None;
226 return None;
227 };
228 debug_log(format_args!(
229 "proto-store walk-back generation={} payload={}",
230 stored.generation,
231 payload.len(),
232 ));
233 let len = payload.len().min(out.len());
234 out[..len].copy_from_slice(&payload[..len]);
235 (len == payload.len()).then_some(len)
236 }
237
238 pub async fn persist(&mut self, payload: &[u8]) -> Result<(), ()> {
239 if payload.len() > proto::MAX_PAYLOAD {
240 return Err(());
241 }
242 self.write(proto::RecordRef::Snapshot(payload)).await
243 }
244
245 pub async fn clear(&mut self) -> Result<(), ()> {
251 self.write(proto::RecordRef::Cleared).await
252 }
253
254 async fn write(&mut self, record: proto::RecordRef<'_>) -> Result<(), ()> {
258 let generation = self.generation.wrapping_add(1);
259 let mut flash = self.flash.lock().await;
260 let target =
261 journal_write_target(&mut *flash, self.slot, self.page0, proto::SLOT_SIZE).await?;
262 match proto::write_record(&mut *flash, target, generation, record).await {
263 Ok(()) => {
264 debug_log(format_args!(
265 "proto-store commit generation={generation} slot=0x{target:06x} cleared={}",
266 matches!(record, proto::RecordRef::Cleared),
267 ));
268 self.generation = generation;
269 self.slot = Some(target);
270 Ok(())
271 }
272 Err(_) => {
273 debug_log(format_args!(
274 "proto-store write=FAILED target=0x{target:06x}"
275 ));
276 Err(())
277 }
278 }
279 }
280}