1use crate::model::{bank_bytes, flash_map};
4use crate::util::{await_reply, contains_lattice_header, has_lattice_header, hex, open, warn};
5use crate::{check, protocol, rcvbp, Ctx, Progress};
6use anyhow::{Context, Result};
7use rawlink::Link;
8use receivers::CardModel;
9use std::time::Duration;
10
11const CHIP_REGS: u16 = 0x0a84;
13
14fn has_chip_regs(f: &rcvbp::Rcvbp) -> bool {
16 f.find(CHIP_REGS).is_some_and(|r| !r.is_empty_table())
17}
18
19const fn page_offset(block: u8, page: u16) -> usize {
21 (block as usize * 256 + page as usize) * protocol::FLASH_PAGE_BYTES
22}
23
24fn page(image: &[u8], i: usize) -> &[u8] {
26 &image[i * protocol::FLASH_PAGE_BYTES..(i + 1) * protocol::FLASH_PAGE_BYTES]
27}
28
29fn mismatched_pages(after: &[u8], pages: &[&[u8]]) -> Vec<usize> {
31 after
32 .chunks(protocol::FLASH_PAGE_BYTES)
33 .zip(pages)
34 .enumerate()
35 .filter(|(_, (a, b))| a != *b)
36 .map(|(i, _)| i)
37 .collect()
38}
39
40pub fn read_config(
45 ctx: &Ctx,
46 index: u16,
47 page: Option<u16>,
48 max_chunks: u16,
49 wait: u64,
50) -> Result<Vec<u8>> {
51 let page = match page {
52 Some(p) => p,
53 None => ctx.model()?.memory.config_page(),
54 };
55 let mut dev = open(ctx)?;
56 let mut flash: Vec<u8> = Vec::new();
57 let mut expected: Option<usize> = None;
58
59 for chunk in 0..max_chunks {
60 let page = page + chunk * protocol::FLASH_PAGES_PER_CHUNK;
61 flash.extend_from_slice(&read_chunk(&mut dev, index, page, wait)?);
62
63 if expected.is_none() && flash.len() >= 4 {
65 expected = Some(u32::from_le_bytes(flash[..4].try_into()?) as usize);
66 }
67 if expected.is_some_and(|n| flash.len() >= n) {
68 break;
69 }
70 }
71
72 let total = expected.context("card returned no length prefix")?;
73 if flash.len() < total + 4 {
74 anyhow::bail!(
75 "only read {} of {total} bytes; raise --max-chunks",
76 flash.len()
77 );
78 }
79 let file = flash
81 .get(4..4 + total)
82 .context("card reported more configuration than it returned")?;
83 Ok(file.to_vec())
84}
85
86pub fn save_config(file: &[u8], out: &str, p: &mut dyn Progress) -> Result<()> {
89 std::fs::write(out, file).with_context(|| format!("write {out}"))?;
90 p.out(out);
91
92 match rcvbp::Rcvbp::load(out) {
93 Ok(f) if !has_chip_regs(&f) => warn(
94 p,
95 format!(
96 "{out} has no driver-chip register table; panels with PWM driver ICs will stay dark"
97 ),
98 ),
99 Ok(_) => {}
100 Err(e) => warn(p, format!("{out} does not parse as .rcvbp: {e:#}")),
101 }
102 Ok(())
103}
104
105pub(crate) fn read_chunk(dev: &mut Link, index: u16, page: u16, wait: u64) -> Result<Vec<u8>> {
107 dev.send(&protocol::read_flash(index, page))?;
108 await_reply(dev, Duration::from_secs(wait), |f| {
109 protocol::flash_reply_data(f).map(<[u8]>::to_vec)
110 })?
111 .with_context(|| format!("no reply for page 0x{page:04x} within {wait}s"))
112}
113
114fn verify_firmware(m: &CardModel, dev: &mut Link, index: u16, img: &[u8], wait: u64) -> Result<usize> {
116 let mut bad = 0usize;
117 for block in m.memory.primary_blocks() {
118 for lo in (0u16..0x100).step_by(protocol::FLASH_PAGES_PER_CHUNK as usize) {
119 let page = (u16::from(block) << 8) | lo;
120 let got = read_chunk(dev, index, page, wait)?;
121 let off = page_offset(block, lo);
122 let want = &img[off..off + got.len()];
123 bad += got.iter().zip(want).filter(|(g, w)| g != w).count();
124 }
125 }
126 Ok(bad)
127}
128
129fn describe_image(img: &[u8], p: &mut dyn Progress) {
131 let header: String = img[..200.min(img.len())]
132 .iter()
133 .map(|&b| {
134 if b.is_ascii_graphic() || b == b' ' {
135 b as char
136 } else {
137 ' '
138 }
139 })
140 .collect();
141 for field in ["Design name", "Part", "Date"] {
142 if let Some(i) = header.find(field) {
143 p.err(&format!(
144 "firmware: {}",
145 header[i..].split(" ").next().unwrap_or("").trim()
146 ));
147 }
148 }
149}
150
151#[allow(clippy::too_many_arguments)]
161pub fn flash_firmware(
162 ctx: &Ctx,
163 image: &str,
164 backup: &str,
165 commit: bool,
166 blocks: std::ops::Range<u8>,
167 index: u16,
168 wait: u64,
169 p: &mut dyn Progress,
170) -> Result<()> {
171 let m = ctx.model()?;
172 let map = flash_map(m);
173 anyhow::ensure!(
174 blocks.start < blocks.end
175 && map.firmware_blocks.contains(&blocks.start)
176 && blocks.end <= map.firmware_blocks.end,
177 "blocks 0x{:02x}..0x{:02x} fall outside the primary bank",
178 blocks.start,
179 blocks.end
180 );
181
182 let loaded = crate::firmware::load(image, p)?;
183 let checked = crate::firmware::checked(&loaded);
184 let (image, img) = (loaded.path.as_str(), loaded.bytes.as_slice());
185 anyhow::ensure!(
186 has_lattice_header(img),
187 "{image} does not look like a Lattice bitstream"
188 );
189 let span = bank_bytes(m);
190 anyhow::ensure!(
191 img.len() >= span,
192 "{image} is only {} bytes; the primary bank is {span}",
193 img.len()
194 );
195 let img = &img[..span];
198
199 let old = std::fs::read(backup).with_context(|| format!("read backup {backup}"))?;
200 anyhow::ensure!(
201 old.len() >= span && has_lattice_header(&old),
202 "{backup} is not a usable dump of the current primary bank"
203 );
204
205 p.err(&format!(
206 "firmware: {image} ({checked}) -> blocks 0x{:02x}..0x{:02x}, recovery {backup}",
207 blocks.start,
208 blocks.end - 1
209 ));
210 describe_image(img, p);
211
212 if !commit {
213 p.out("dry run: nothing written (add --commit)");
214 return Ok(());
215 }
216
217 let mut dev = open(ctx)?;
218
219 dev.send(&protocol::set_program_writable(index, true))?;
222 std::thread::sleep(Duration::from_millis(200));
223
224 for block in blocks.clone() {
225 p.err(&format!("firmware: erase 0x{block:02x}"));
226 dev.send(&map.erase_firmware_block(index, block)?)?;
227 std::thread::sleep(Duration::from_secs(3));
228 }
229
230 for block in blocks {
231 p.err(&format!("firmware: write 0x{block:02x}"));
232 for page in 0..=0xffu8 {
233 let off = page_offset(block, u16::from(page));
234 let data = &img[off..off + protocol::FLASH_PAGE_BYTES];
235 dev.send(&map.write_firmware_page(index, block, page, data)?)?;
236 std::thread::sleep(Duration::from_millis(6));
237 }
238 }
239
240 dev.send(&protocol::set_program_writable(index, false))?;
242
243 p.err("firmware: verify");
244 let bad = verify_firmware(m, &mut dev, index, img, wait)?;
245 if bad == 0 {
246 p.err("firmware: bank verified");
247 } else {
248 warn(
251 p,
252 format!(
253 "{bad} bytes differ after writing; golden bank at 0x{:02x} untouched; \
254 recover with: rxp firmware write {backup} --backup {backup} --commit",
255 map.golden_block
256 ),
257 );
258 }
259 Ok(())
260}
261
262pub fn scan_flash(
264 ctx: &Ctx,
265 first: u8,
266 last: u8,
267 index: u16,
268 wait: u64,
269 p: &mut dyn Progress,
270) -> Result<()> {
271 let mut dev = open(ctx)?;
272 for blk in first..=last {
273 let page = u16::from(blk) << 8;
274 let Ok(d) = read_chunk(&mut dev, index, page, wait) else {
275 continue;
276 };
277 if d.iter().all(|&b| b == 0xff) || d.iter().all(|&b| b == 0) {
278 continue;
279 }
280 let kind = if contains_lattice_header(&d) {
281 "lattice bitstream header"
282 } else if d.starts_with(&[0x20, 0x20, 0x19, 0xbe]) {
283 "rcvbp config"
284 } else {
285 "data"
286 };
287 p.out(&format!(
288 "0x{blk:02x} 0x{:06x} {kind:<24} {}",
289 u32::from(blk) << 16,
290 hex(&d[..d.len().min(12)], " ")
291 ));
292 }
293 Ok(())
294}
295
296pub fn dump_range(
298 ctx: &Ctx,
299 start: &str,
300 len: &str,
301 index: u16,
302 wait: u64,
303 out: &str,
304 p: &mut dyn Progress,
305) -> Result<()> {
306 let start = u32::from_str_radix(start.trim_start_matches("0x"), 16).context("bad --start")?;
307 let len = u32::from_str_radix(len.trim_start_matches("0x"), 16).context("bad --len")?;
308 let mut dev = open(ctx)?;
309 let mut image = Vec::with_capacity(len as usize);
310
311 let step = protocol::FLASH_PAGE_BYTES as u32;
314 let mut addr = start;
315 let mut misses = 0u32;
316 while addr < start + len {
317 dev.send(&protocol::read_flash_linear(index, addr, step))?;
318 let reply = await_reply(&mut dev, Duration::from_secs(wait), |f| {
321 (f.len() >= 15 + step as usize).then(|| f[15..15 + step as usize].to_vec())
322 })?;
323 if let Some(data) = reply {
324 image.extend_from_slice(&data);
325 } else {
326 misses += 1;
327 image.extend(std::iter::repeat_n(0xffu8, step as usize));
328 if misses > 8 {
329 warn(
330 p,
331 format!("giving up after {misses} unanswered reads at 0x{addr:08x}"),
332 );
333 break;
334 }
335 }
336 if (addr - start).is_multiple_of(0x10000) {
337 p.err(&format!("read 0x{addr:08x}"));
338 }
339 addr += step;
340 }
341 std::fs::write(out, &image).with_context(|| format!("write {out}"))?;
342 if misses > 0 {
343 warn(p, format!("{misses} unanswered reads filled with 0xff"));
344 }
345 p.out(out);
346 Ok(())
347}
348
349pub fn dump_flash(
351 ctx: &Ctx,
352 block: u8,
353 blocks: u16,
354 index: u16,
355 wait: u64,
356 out: &str,
357 p: &mut dyn Progress,
358) -> Result<()> {
359 let mut dev = open(ctx)?;
360 let image = read_blocks(&mut dev, index, block, blocks, wait, p)?;
361 std::fs::write(out, &image).with_context(|| format!("write {out}"))?;
362 p.out(out);
363 Ok(())
364}
365
366pub fn read_primary_bank(
368 m: &CardModel,
369 dev: &mut Link,
370 index: u16,
371 wait: u64,
372 p: &mut dyn Progress,
373) -> Result<Vec<u8>> {
374 let blocks = m.memory.primary_blocks();
375 read_blocks(dev, index, blocks.start, u16::from(m.memory.bank_blocks()), wait, p)
376}
377
378fn read_block(m: &CardModel, dev: &mut Link, index: u16, wait: u64, p: &mut dyn Progress) -> Result<Vec<u8>> {
380 read_blocks(dev, index, m.memory.parameter_block, 1, wait, p)
381}
382
383pub fn read_blocks(
389 dev: &mut Link,
390 index: u16,
391 first: u8,
392 count: u16,
393 wait: u64,
394 p: &mut dyn Progress,
395) -> Result<Vec<u8>> {
396 let mut image = Vec::with_capacity(64 * 1024 * count as usize);
397 for b in 0..count {
398 check(p)?;
399 let block = first.wrapping_add(b as u8);
400 for lo in (0u16..0x100).step_by(protocol::FLASH_PAGES_PER_CHUNK as usize) {
401 let page = (u16::from(block) << 8) | lo;
402 image.extend_from_slice(&read_chunk(dev, index, page, wait)?);
403 }
404 p.err(&format!("read 0x{block:02x}"));
405 }
406 Ok(image)
407}
408
409pub fn rewrite_block(
414 m: &CardModel,
415 dev: &mut Link,
416 index: u16,
417 image: &[u8],
418 wait: u64,
419 must_verify: std::ops::Range<usize>,
420 p: &mut dyn Progress,
421) -> Result<()> {
422 anyhow::ensure!(image.len() == 64 * 1024, "image must be exactly 64KB");
423 let map = flash_map(m);
424 let pages: Vec<&[u8]> = image.chunks(protocol::FLASH_PAGE_BYTES).collect();
425
426 for attempt in 1..=4 {
427 let repair: Vec<usize> = if attempt == 1 {
428 erase_and_settle(&map, dev, index, p)?;
429 (0..pages.len()).collect()
430 } else {
431 let after = read_block(m, dev, index, wait, p)?;
432 let bad = mismatched_pages(&after, &pages);
433 if bad.is_empty() {
434 p.err("flash: block verified");
435 return Ok(());
436 }
437 let dirty = bad
438 .iter()
439 .any(|&i| page(&after, i).iter().any(|&b| b != 0xff));
440 p.err(&format!(
441 "flash: attempt {attempt}: {} pages to rewrite{}",
442 bad.len(),
443 if dirty { " (re-erasing first)" } else { "" }
444 ));
445 if dirty {
446 erase_and_settle(&map, dev, index, p)?;
447 (0..pages.len()).collect()
448 } else {
449 bad
450 }
451 };
452
453 for (n, &i) in repair.iter().enumerate() {
454 dev.send(&map.write_page(index, map.param_block, i as u8, pages[i])?)?;
455 std::thread::sleep(Duration::from_millis(8));
456 if repair.len() > 32 && n.is_multiple_of(64) {
457 p.err(&format!("flash: page {n}/{}", repair.len()));
458 }
459 }
460 }
461 let after = read_block(m, dev, index, wait, p)?;
464 let bad = mismatched_pages(&after, &pages);
465 let in_config = bad.iter().any(|i| must_verify.contains(i));
466 anyhow::ensure!(
467 !in_config,
468 "verify failed: {} pages differ, including configuration pages",
469 bad.len()
470 );
471 p.err(&format!(
472 "flash: configuration pages verified; {} page(s) outside them would not take writes: {}",
473 bad.len(),
474 bad.iter()
475 .map(|i| format!("0x{i:02x}"))
476 .collect::<Vec<_>>()
477 .join(", ")
478 ));
479 Ok(())
480}
481
482fn erase_and_settle(map: &protocol::FlashMap, dev: &mut Link, index: u16, p: &mut dyn Progress) -> Result<()> {
484 p.err(&format!("flash: erase 0x{:02x}", map.param_block));
485 dev.send(&map.erase_block(index, map.param_block)?)?;
486 std::thread::sleep(Duration::from_secs(3));
488 Ok(())
489}
490
491#[allow(clippy::too_many_arguments)]
497pub fn write_config(
498 ctx: &Ctx,
499 config: &str,
500 commit: bool,
501 backup: &str,
502 base_image: Option<&str>,
503 index: u16,
504 wait: u64,
505 p: &mut dyn Progress,
506) -> Result<()> {
507 let parsed = rcvbp::Rcvbp::load(config)?;
508 let file = std::fs::read(config).with_context(|| format!("read {config}"))?;
509 let m = ctx.model()?;
510 let at = m.memory.boot_image.rcvbp;
511 let max = m.memory.boot_image.rcvbp_max;
512 anyhow::ensure!(
513 file.len() <= max,
514 "{config} is {} bytes, over the {max}-byte limit the card accepts",
515 file.len()
516 );
517 if !has_chip_regs(&parsed) {
518 warn(p, format!("{config} has no driver-chip register table"));
519 }
520
521 let mut dev = open(ctx)?;
522 let original = match base_image {
523 Some(path) => {
524 let img = std::fs::read(path).with_context(|| format!("read {path}"))?;
525 anyhow::ensure!(img.len() == 64 * 1024, "{path} must be exactly 65536 bytes");
526 img
527 }
528 None => {
529 let img = read_block(m, &mut dev, index, wait, p)?;
530 std::fs::write(backup, &img).with_context(|| format!("write {backup}"))?;
531 p.err(&format!("flash: backup {backup}"));
532 img
533 }
534 };
535
536 let mut image = original.clone();
537 let old_len = u32::from_le_bytes(image[at..at + 4].try_into()?) as usize;
538 let old_len = old_len.min(max);
541 let region = at + 4 + old_len.max(file.len());
542 anyhow::ensure!(region <= image.len(), "parameter region overruns the block");
543 image[at..region].fill(0);
545 image[at..at + 4].copy_from_slice(&(file.len() as u32).to_le_bytes());
546 image[at + 4..at + 4 + file.len()].copy_from_slice(&file);
547
548 let changed = original.iter().zip(&image).filter(|(a, b)| a != b).count();
549 p.err(&format!(
550 "flash: parameter blob {old_len} -> {} bytes, {changed} bytes of block 0x{:02x} change",
551 file.len(),
552 m.memory.parameter_block
553 ));
554
555 if !commit {
556 p.out("dry run: nothing written (add --commit)");
557 return Ok(());
558 }
559
560 let first = at / protocol::FLASH_PAGE_BYTES;
562 let last = (at + 4 + file.len()).div_ceil(protocol::FLASH_PAGE_BYTES);
563 rewrite_block(m, &mut dev, index, &image, wait, first..last, p).with_context(|| {
564 format!(
565 "original block saved at {backup}; restore with: rxp flash restore-block {backup} --commit"
566 )
567 })?;
568
569 let map = flash_map(m);
573 let off = map.screen_record_addr as usize % image.len();
574 let record = &original[off..off + protocol::SCREEN_RECORD_LEN];
575 if base_image.is_none() && record.iter().any(|&b| b != 0xff) {
576 dev.send(&map.write_screen_record(index, map.screen_record_addr, record)?)?;
577 std::thread::sleep(Duration::from_millis(100));
578 p.err(&format!(
579 "flash: screen-size record restored ({}x{})",
580 u16::from_be_bytes([record[6], record[7]]),
581 u16::from_be_bytes([record[8], record[9]])
582 ));
583 }
584
585 p.err("power-cycle the card to apply");
586 Ok(())
587}
588
589pub fn restore_flash(
591 ctx: &Ctx,
592 image_path: &str,
593 commit: bool,
594 index: u16,
595 p: &mut dyn Progress,
596) -> Result<()> {
597 let image = std::fs::read(image_path).with_context(|| format!("read {image_path}"))?;
598 anyhow::ensure!(
599 image.len() == 64 * 1024,
600 "{image_path} is {} bytes; a block image must be exactly 65536",
601 image.len()
602 );
603 let m = ctx.model()?;
604 if !commit {
605 p.out(&format!(
606 "dry run: {image_path} -> block 0x{:02x} (add --commit)",
607 m.memory.parameter_block
608 ));
609 return Ok(());
610 }
611 let mut dev = open(ctx)?;
612 rewrite_block(m, &mut dev, index, &image, 2, 0..256, p)?;
613 Ok(())
614}