fix(firmware): keep the editor alive when a panel paint fails
A screen refresh that ran while :sync had Wi-Fi + TLS up could fail to allocate an internal DMA bounce buffer (ESP_ERR_NO_MEM); the error propagated through ? out of main(), main_task returned from app_main(), and the editor loop died while the USB and git threads kept running — keys logged, panel frozen forever. A paint is idempotent and retryable (the buffer is the source of truth), so every editor-loop paint now logs and drops the frame instead of propagating, leaves shown untouched so the next paint repaints the diff, and forces a full refresh next to re-sync both RAM banks. Same contract as save_note. The boot-time first render stays fatal on purpose.
This commit is contained in:
@@ -113,6 +113,10 @@ fn main() -> anyhow::Result<()> {
|
||||
let mut updates: u32 = 0;
|
||||
let mut cursor_shown = true; // the initial render includes the caret
|
||||
let mut last_activity = Instant::now();
|
||||
// Set when a paint fails (see the refresh block below): the next paint then
|
||||
// does a full refresh to re-establish both RAM banks, since a partial that
|
||||
// died mid-transfer may have left them inconsistent.
|
||||
let mut force_full = false;
|
||||
|
||||
// Keyboard attach/detach state drives the panel's disconnect flag; seed it
|
||||
// (and the word-count snapshot) before the first render.
|
||||
@@ -202,7 +206,11 @@ fn main() -> anyhow::Result<()> {
|
||||
Failed(reason) => reason,
|
||||
});
|
||||
let f = ed.draw(true);
|
||||
epd.display_frame_partial_window(f.bytes(), 0, epd::HEIGHT)?;
|
||||
if let Err(e) = epd.display_frame_partial_window(f.bytes(), 0, epd::HEIGHT) {
|
||||
log::warn!("sync-notice repaint FAILED ({e}); full refresh next");
|
||||
force_full = true;
|
||||
continue;
|
||||
}
|
||||
shown = f;
|
||||
cursor_shown = true;
|
||||
continue;
|
||||
@@ -211,7 +219,11 @@ fn main() -> anyhow::Result<()> {
|
||||
// no keystroke will arrive to trigger it otherwise.
|
||||
if kbd_changed {
|
||||
let f = ed.draw(true);
|
||||
epd.display_frame_partial_window(f.bytes(), 0, epd::HEIGHT)?;
|
||||
if let Err(e) = epd.display_frame_partial_window(f.bytes(), 0, epd::HEIGHT) {
|
||||
log::warn!("kbd-flag repaint FAILED ({e}); full refresh next");
|
||||
force_full = true;
|
||||
continue;
|
||||
}
|
||||
shown = f;
|
||||
cursor_shown = true;
|
||||
log::info!("keyboard {}", if kbd { "connected" } else { "disconnected" });
|
||||
@@ -226,10 +238,14 @@ fn main() -> anyhow::Result<()> {
|
||||
{
|
||||
ed.refresh_stats();
|
||||
let f = ed.draw(true);
|
||||
epd.display_frame_partial_window(f.bytes(), 0, epd::HEIGHT)?;
|
||||
shown = f;
|
||||
cursor_shown = true;
|
||||
log::info!("caret shown");
|
||||
if let Err(e) = epd.display_frame_partial_window(f.bytes(), 0, epd::HEIGHT) {
|
||||
log::warn!("caret repaint FAILED ({e}); full refresh next");
|
||||
force_full = true;
|
||||
} else {
|
||||
shown = f;
|
||||
cursor_shown = true;
|
||||
log::info!("caret shown");
|
||||
}
|
||||
} else {
|
||||
FreeRtos::delay_ms(8);
|
||||
}
|
||||
@@ -273,17 +289,29 @@ fn main() -> anyhow::Result<()> {
|
||||
&& only_adds_ink(shown.bytes(), frame.bytes(), y0, y1);
|
||||
|
||||
let t0 = Instant::now();
|
||||
let refresh = if periodic {
|
||||
epd.display_frame(frame.bytes())?;
|
||||
"FULL"
|
||||
// `force_full` promotes to a full refresh after a failed paint: it
|
||||
// rewrites both RAM banks, recovering from a partial that may have died
|
||||
// mid-transfer and desynced them.
|
||||
let (result, refresh) = if periodic || force_full {
|
||||
(epd.display_frame(frame.bytes()), "FULL")
|
||||
} else if additive {
|
||||
epd.display_frame_partial_window(frame.bytes(), y0, y1 - y0 + 1)?;
|
||||
"windowed"
|
||||
(epd.display_frame_partial_window(frame.bytes(), y0, y1 - y0 + 1), "windowed")
|
||||
} else {
|
||||
epd.display_frame_partial_window(frame.bytes(), 0, epd::HEIGHT)?;
|
||||
"full-area"
|
||||
(epd.display_frame_partial_window(frame.bytes(), 0, epd::HEIGHT), "full-area")
|
||||
};
|
||||
let ms = t0.elapsed().as_millis();
|
||||
if let Err(e) = result {
|
||||
// Never fatal — the buffer is the source of truth and safe in RAM,
|
||||
// exactly like a failed `save_note`. Drop this frame, leave `shown`
|
||||
// untouched so the next paint repaints the same diff, and force a
|
||||
// clean full refresh then. Typical cause: internal DMA-capable RAM
|
||||
// briefly starved by Wi-Fi/TLS during a background `:sync`; it frees
|
||||
// the moment the push finishes.
|
||||
log::warn!("{refresh} refresh #{updates} FAILED ({e}); frame dropped, full refresh next");
|
||||
force_full = true;
|
||||
continue;
|
||||
}
|
||||
force_full = false;
|
||||
log::info!(
|
||||
"{refresh} refresh #{updates} [{:?}]: {ms} ms (rows {y0}..={y1}, {keys} key(s))",
|
||||
ed.mode()
|
||||
|
||||
Reference in New Issue
Block a user