diff options
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/gop.rs | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 012a9d0..ecf8fa9 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -516,6 +516,89 @@ pub fn dump_lines(lines: &[&str]) { | |||
| 516 | } | 516 | } |
| 517 | } | 517 | } |
| 518 | } | 518 | } |
| 519 | // Scannable QR of the same text below the band (heap-free encoder; | ||
| 520 | // real hardware has no serial console — photograph the screen). | ||
| 521 | dump_qr(lines); | ||
| 522 | } | ||
| 523 | |||
| 524 | /// Render the crash/fault lines as a QR code below the dump band. | ||
| 525 | /// | ||
| 526 | /// The joined text is built in a `.bss` buffer (the fault path must not | ||
| 527 | /// allocate) and encoded byte-mode EC-L by `crate::qr` (also heap-free, | ||
| 528 | /// static matrix). Over-long text is truncated with a "…" marker. If | ||
| 529 | /// the text does not fit a QR or there is no framebuffer, this is a no-op | ||
| 530 | /// and the band text remains the only channel. | ||
| 531 | static mut QR_TEXT: [u8; crate::qr::MAX_BYTES + 4] = [0; crate::qr::MAX_BYTES + 4]; | ||
| 532 | |||
| 533 | pub fn dump_qr(lines: &[&str]) { | ||
| 534 | let Some(fb) = fb().as_ref() else { | ||
| 535 | return; | ||
| 536 | }; | ||
| 537 | unsafe { | ||
| 538 | let text = &mut *core::ptr::addr_of_mut!(QR_TEXT); | ||
| 539 | let mut n = 0usize; | ||
| 540 | for line in lines { | ||
| 541 | let b = line.as_bytes(); | ||
| 542 | let take = b.len().min(text.len() - n); | ||
| 543 | text[n..n + take].copy_from_slice(&b[..take]); | ||
| 544 | n += take; | ||
| 545 | if n < text.len() { | ||
| 546 | text[n] = b'\n'; | ||
| 547 | n += 1; | ||
| 548 | } | ||
| 549 | } | ||
| 550 | while n > 0 && text[n - 1] == b'\n' { | ||
| 551 | n -= 1; | ||
| 552 | } | ||
| 553 | if n == 0 { | ||
| 554 | return; | ||
| 555 | } | ||
| 556 | let cap = crate::qr::MAX_BYTES.saturating_sub(3); | ||
| 557 | if n > cap { | ||
| 558 | n = cap; | ||
| 559 | text[n..n + 3].copy_from_slice(b"..."); | ||
| 560 | n += 3; | ||
| 561 | } | ||
| 562 | if crate::qr::encode(&text[..n]) == 0 { | ||
| 563 | return; | ||
| 564 | } | ||
| 565 | } | ||
| 566 | render_qr(fb, lines.len() * 16); | ||
| 567 | } | ||
| 568 | |||
| 569 | /// Draw the current QR matrix (black on white, quiet zone) centred below | ||
| 570 | /// the band, scaled to the largest square that fits. | ||
| 571 | fn render_qr(fb: &Framebuffer, band_px: usize) { | ||
| 572 | let size = crate::qr::size(); | ||
| 573 | if size == 0 { | ||
| 574 | return; | ||
| 575 | } | ||
| 576 | let total = size + 8; // 4-module quiet zone each side | ||
| 577 | let avail_w = fb.width.saturating_sub(24); | ||
| 578 | let avail_h = fb.height.saturating_sub(band_px + 16); | ||
| 579 | let scale = (avail_w / total).min(avail_h / total); | ||
| 580 | if scale < 1 { | ||
| 581 | return; | ||
| 582 | } | ||
| 583 | let px = total * scale; | ||
| 584 | let x0 = (fb.width - px) / 2; | ||
| 585 | let y0 = band_px + 8 + (avail_h.saturating_sub(px) / 2); | ||
| 586 | let white = rgb(0xFF, 0xFF, 0xFF); | ||
| 587 | let black = rgb(0, 0, 0); | ||
| 588 | fill_rect(x0, y0, px, px, white); | ||
| 589 | for y in 0..size { | ||
| 590 | for x in 0..size { | ||
| 591 | if crate::qr::module(x, y) { | ||
| 592 | fill_rect( | ||
| 593 | x0 + (x + 4) * scale, | ||
| 594 | y0 + (y + 4) * scale, | ||
| 595 | scale, | ||
| 596 | scale, | ||
| 597 | black, | ||
| 598 | ); | ||
| 599 | } | ||
| 600 | } | ||
| 601 | } | ||
| 519 | } | 602 | } |
| 520 | 603 | ||
| 521 | /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. | 604 | /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. |
