Skip to content

Changed epaper logic to better enable partial refresh during operation#876

Closed
babo989 wants to merge 1 commit intomainfrom
feature-Partial_Refresh
Closed

Changed epaper logic to better enable partial refresh during operation#876
babo989 wants to merge 1 commit intomainfrom
feature-Partial_Refresh

Conversation

@babo989
Copy link
Collaborator

@babo989 babo989 commented Mar 4, 2026

E-ink Display: Partial Refresh and Visual State Changes

Summary

Reworks the e-ink display controller to use proper partial refresh for runtime updates and adds visual state indication (on/off) through inverted color schemes. Also fixes a bug in the outdated Waveshare driver that increased ghosting during partial refresh.

Problem

Our implementation called epd.Clear() + display_Partial() on every update, which caused:

  1. A full-screen flash on every text change
  2. No visual distinction between "device is running" and "device is off"

Changes

1. Waveshare Driver Fix (e-paper/lib/waveshare_epd/epd2in9_V2.py)

Root cause of ghosting: The SSD1680 display controller uses two RAM buffers to compute partial refresh diffs:

  • 0x24 — the "new" image (what we want to display)
  • 0x26 — the "old" image (what's currently on screen)

The stock Waveshare display_Partial() only wrote to 0x24, leaving 0x26 stuck at whatever display_Base() last set. Every partial refresh diffed against the original baseline, not the current screen state, causing accumulated ghosting and illegible text.

Fix:

  • display_Base() now saves a copy of the image buffer (_prev_image)
  • display_Partial() explicitly writes _prev_image to 0x26 and the new image to 0x24 before triggering the refresh, then updates _prev_image afterward
  • This matches the approach used by GxEPD2 (the nice C++ e-ink library often referenced on GitHub)
  • However, with our current design, the partiatl waveform is too weak to transition a large area (large black bars) in one partial pass, as its designed for small tex changes. I propose a solution for 2 states between 'Off' and 'On', where the 'On' configuration allows for rapid partial refresh for state changes.

2. Display Layout Redesign (main.py)

"On" state (startup + runtime):

  • White top bar with black text (hostname)
  • Black center area with white (inverted) logo
  • White bottom bar with black text (URL/status)
  • White bars with black text are nice for partial refresh — the weak partial waveform handles small text pixel changes on white backgrounds cleanly, even nicely.

"Off" state (shutdown):

  • Black top bar with white text (hostname)
  • White center area with black (original) logo
  • Black bottom bar with white text ("OFF")
  • Uses full refresh (display_Base) so the large solid-black regions render crisply, and well.

The visual inversion makes it immediately obvious whether the device is on or off.

3. Refresh Strategy (main.py)

Event Function Refresh Type Flashes?
Startup init_display() Full (Clear + display_Base) Yes — establishes clean baseline
MQTT configure render() Partial (display_Partial) No — only changed text pixels update
MQTT clear clear() Full (Clear + display_Base) Yes — resets to blank
Shutdown (SIGTERM) render_off() Full (display_Base) Yes — inverts color scheme

4. Bar Height

Increased BAR_HEIGHT from 26px to 30px to prevent text descenders from clipping outside the bar region, as there were some letters overlapping.

5. New Helper Functions

  • update_url(url) — partial refresh of bottom bar only
  • update_hostname(hostname) — partial refresh of top bar only
  • render(url, hostname) — partial refresh of both bars
  • render_off(hostname) — full refresh with inverted color scheme

Node-Red Integration (TODO)

To show "Acquiring" on the e-ink display when acquisition starts, or "Segmenting" ect.. we just add the following to the Node-Red dashboard flow:

When acquisition starts:

Wire a function node from the "start acquisition" path that outputs:

msg.topic = "display";
msg.payload = {
    action: "configure",
    config: {
        url: "Acquiring",
        "machine-name": global.get("machine_name") || ""
    }
};
return msg;

Connect this to an MQTT-out node (broker: localhost:1883, no topic — uses msg.topic).

When acquisition stops:

Wire a function node from the "stop acquisition" path that outputs:

msg.topic = "display";
msg.payload = {
    action: "configure",
    config: {
        url: "http://192.168.4.1",
        "machine-name": global.get("machine_name") || ""
    }
};
return msg;

Connect this to the same MQTT-out node.

Flow wiring

The acquisition page already has a switch node that routes acq_status values of "on" and "off". Add the display function nodes as additional outputs from this switch, or wire them in parallel with the existing "start acquisition" and "stop acquisition" function nodes.

This uses the existing configure MQTT action — no Python changes needed. The display controller will do a partial refresh, silently updating the bottom bar text without flashing.

Testing

# Start display in dev mode
cd ~/PlanktoScope/controller/display
just dev

# From a second terminal:

# Test configure (partial refresh — no flash)
mosquitto_pub -t display -m '{"action":"configure","config":{"url":"http://10.0.0.1","machine-name":"test-scope"}}'

# Test acquiring status (partial refresh — no flash)
mosquitto_pub -t display -m '{"action":"configure","config":{"url":"Acquiring","machine-name":"test-scope"}}'

# Test shutdown (Ctrl+C in the just dev terminal)
# Should do a full refresh, inverting to black bars + white center

# Test clear (full refresh)
mosquitto_pub -t display -m '{"action":"clear"}'

Files Changed

  • controller/display/main.py — display layout, refresh strategy, on/off states
  • controller/display/e-paper/lib/waveshare_epd/epd2in9_V2.py — dual-buffer partial refresh fix
  • node-red/projects/dashboard/flows.json — (pending) acquisition status display updates

@sonnyp
Copy link
Collaborator

sonnyp commented Mar 9, 2026

I already have partial refresh working at #874

I left a comment/question for you #874 (comment)

@sonnyp sonnyp closed this Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants