Browse documentation

🧭 Start Here

Blackcap Overview ✨ Blackcap Feature Catalog Installation First Run

🚀 Deploy Blackcap

Platform Stacks and Raspberry Pi Hardware Raspberry Pi Deployment Raspberry Pi Client Services GCP Deployment Application Updates Environment Variables and Secrets Reverse Proxy and TLS Background Jobs and Schedules

🛠️ Administer Blackcap

Organizations Users, Permissions, and Authentication Configuration Workspace Backups and Restore Database Administration Regression Testing Performance and Job Status Audit, Access Activity, and Logging GeoIP and Access Location Data Retention and Purge Support Requests API Tester and Postman Instance Reporting

🍽️ Use Recipes

Recipes and the Recipe Library Recipe Import and Discovery Recipe Editing and Cache Artifacts Recipe Sharing Social Recipe Import AI Recipe Image Generation

📅 Plan Meals

Meal Planner

🛒 Use Shopping Lists

Shopping Lists and Shop a List External and Household Shopping 🧩 Chrome Extension Shop With

🧺 Manage Kitchen Inventory

🧺 Kitchen Inventory

🖥️ Use Displays

Displays and Connections Assigning and Scheduling Display Content Remote Pi Client E-Ink Rendering Menu Refresh and Rendering Noun Project Footer Images

🧑‍🍳 Cook with Let’s Cook

🧑‍🍳 Let’s Cook 🧑‍🍳 Let’s Cook Controls and Timers

🤖 Use and Administer AI

🤖 AI in Blackcap 🤖 AI Providers and Connections 🤖 AI Seeds and Usage

🧩 Use the Chrome Extension

🧩 Blackcap Chrome Extension 🧩 Chrome Extension Recipe Capture 🧩 Chrome Extension Shop With 🧩 Chrome Extension Release and Privacy

🎮 Play Games

🎮 Games and Trivia

🔌 Integrations

Email Integration Cloud Storage Integrations Voice Assistants Shop With Integrations Authentication Providers

⚙️ Develop Blackcap

Application Architecture Database Service and Data Access SQLite and PostgreSQL Database Migrations Background Job Architecture Testing API Architecture Security and Organization Scoping UI, Icons, and Documentation Assets Blackcap-Safe Emoji Documentation Standards Terminology

⚠️ Troubleshoot Blackcap

⚠️ Troubleshooting Deployment Troubleshooting Display Troubleshooting Recipe Import Troubleshooting 🤖 AI Troubleshooting Backup Troubleshooting Database Troubleshooting Diagnostic Organization Clones Support Requests

Remote Pi Client

Audience: Org Admin, Deployment Administrator, Developer, Support Related: Overview · Eink Rendering · Raspberry Pi · Overview · Displays

Remote Pi Client mode links a Raspberry Pi display client to a Blackcap server. The client polls for state and preview/transport artifacts, validates the server-prepared content, atomically stores the last known-good frame, pushes it to the attached panel, and acknowledges physical display completion.

Client mode suspends normal server pushes to the local panel. Let’s Cook uses faster polling and a separate server-side watcher when enabled; deployments that disable the watcher can still start a session but should warn that remote updates will be delayed.

Client-mode behavior

A Raspberry Pi can remain a full Blackcap server while temporarily allowing a different Blackcap server—normally GCP—to control its attached Waveshare 13.3-inch Type K e-ink panel.

This is a display content mode, not a device runtime mode. The Pi application, SQLite database, Admin UI, scheduled jobs, and server services remain running.

Pi server remains online
  ├─ Local content (Menu, Recipe, Meal Plan, Let’s Cook)
  │    Local Blackcap owns the attached panel
  └─ Client content
       Paired remote Blackcap owns the attached panel
       Local hardware pushes are blocked

Supported panel

The first supported client panel preset is:

  • Waveshare 13.3-inch e-Paper HAT (K)
  • 960 × 680
  • four-level grayscale: white, black, and two gray levels
  • full-refresh remote preview updates

Both attached local_eink displays and newly created remote_pi_client targets default to this grayscale profile.

Attached display restrictions

Blackcap permits only one active attached local e-ink display. It can be created only when:

  • Blackcap identifies the host as a Raspberry Pi;
  • the active organization is the Default organization; and
  • no other active local_eink display exists.

The UI hides the type when it is unavailable, the display service independently enforces the rule, and migration 200 adds database uniqueness protection.

Assigning Client content

Use Tools → Displays → Assign Content → Client for the attached local e-ink display.

Client can only be the immediate current content assignment. It cannot be scheduled or configured as default content. Entering Client:

  • ends any active Let’s Cook session on the display;
  • ends the current local content state rather than pausing it;
  • blocks all local server hardware writes;
  • keeps local schedules stored but marks them Suspended — display is in Client mode;
  • removes suspended schedules from the compact Upcoming summary;
  • leaves the last physical e-ink frame unchanged until a valid remote preview is displayed.

Scheduled windows that expire while Client is active are completed and are not replayed later. Future or still-applicable schedules remain stored. Assigning normal content again immediately returns hardware ownership to the local server and pushes the newly selected content.

Linking the attached display

After assigning Client, open Display Details and enter:

  • the remote Blackcap URL, defaulting to https://blackcap.microwavepie.com; and
  • the eight-character pairing code generated for a Remote Raspberry Pi Client display on that server.

HTTPS is required outside development. Pairing returns a durable bc_client_... token, which Blackcap encrypts inside the local display settings. The UI never displays that token.

The link remains saved when the display returns to local content. Assigning Client later reconnects immediately without another code. Relink keeps the existing working relationship until the new pairing succeeds. Unlink removes the local relationship and best-effort revokes the old token on the remote server; the display remains in Client until other content is assigned.

Only one remote relationship is active locally at a time.

Always-running client service

On a Pi with attached e-ink, tools/deploy_pi.sh installs and enables:

blackcap-client.service

The process remains running at boot. Every five seconds while inactive it checks the local attached display through Blackcap’s service/database layer:

  • local content active → sleep;
  • Client active but not linked → sleep;
  • Client active and linked → poll the remote Blackcap server.
  • Normal content uses the configured pull interval, 30 seconds by default.
  • An active Let’s Cook assignment temporarily uses a 5-second pull interval; the normal interval returns automatically when the session ends and default content is restored.

No server service or cron job is disabled when Client is selected.

When a changed remote preview is available, the service:

  1. prefers the server-generated blackcap-gray4-planes-v1 transport artifact;
  2. validates its dimensions, compressed payload, and SHA-256 checksum;
  3. falls back to the panel-ready PNG only when the transport is unavailable;
  4. calls push_eink_image.py --client-owner --four-gray --gray4-transport ... while holding the normal display lock;
  5. bulk-writes the two controller RAM planes and reports the physically displayed version through heartbeat metadata.

The server retains the normal PNG for Admin and receiver surfaces. A standalone client-only Pi does not download that PNG when the controller transport is available. A Pi that is also running the Blackcap server downloads both artifacts so its local Admin UI retains the preview image.

For a Waveshare 13.3-inch Type K client, display-targeted renderers produce exact 960 × 680 grayscale PNGs on the server. Let’s Cook creates its canvas directly in Pillow L mode and maps antialiased pixels to the four panel values (0, 128, 192, 255). The server then converts those pixels into the exact 0x24 and 0x26 controller bitplanes and publishes them atomically beside the PNG.

The client no longer decodes PNG pixels or packs the Waveshare two-bit buffer on the normal transport path. The hardware helper validates/decompresses the prepacked planes, initializes the controller, performs two bulk SPI writes, runs the full e-ink waveform, and puts the panel to sleep. The PNG conversion and local plane-packing paths remain as compatibility fallbacks.

The client owner flag is accepted only while the attached display is in Client. Local server pushes are accepted only while it is not in Client. This provides exclusive panel ownership without stopping the Pi server.

For active Let’s Cook sessions assigned to a remote_pi_client, each durable step, ingredient, render-mode, timer-state, or completion action regenerates the server preview immediately with no server-side debounce. blackcap_lets_cook_timer_refresh.service remains a two-second retry/race-recovery watcher rather than the normal delivery path. Attached local e-ink displays retain their separate roughly ten-second server-side coalescing behavior.

The client continues to coalesce physical panel work naturally: while the 23–24 second full waveform is running, later server actions replace the remote preview artifact. Immediately after a Let’s Cook physical refresh completes, the client checks /api/display-client/state again without waiting another five seconds. If a newer artifact arrived during the waveform, the client downloads and displays the latest image; otherwise it resumes the normal five-second active-session interval. This immediate post-refresh check is Let’s Cook-only.

The state API uses one artifact-backed generation timestamp and advertises separate PNG and panel-transport versions. The controller transport version is authoritative for capable clients, while the preview URL uses the matching PNG version for cache busting. This removes the earlier stale timestamp mismatch and still detects changes that occur within one database timestamp second.

See remote-pi-client.md for the measured timing breakdown and client/server processing boundary.

When Let’s Cook ends through Admin, Mobile, Kiosk, voice, or automatic timed completion, GCP assigns and renders the remote display’s configured default content. The next state response returns the normal pull interval.

If the remote server is unreachable, the link is missing, the preview is invalid, or the hardware update fails, Blackcap does not push an error or blank screen. E-ink retains the last successful frame and Display Details shows the latest diagnostic error.

Display Details diagnostics

The local attached display shows:

  • local content/Client ownership state;
  • linked or unlinked status;
  • remote URL and remote display ID;
  • client service status;
  • last successful poll;
  • last physical panel update;
  • last error;
  • the last remote preview downloaded locally;
  • Link, Relink, and Unlink actions.

The remote remote_pi_client Display Details page also shows heartbeat metadata confirming the last preview physically displayed by the client.

The normal client status progression is:

Waiting for Link → Linked → Remote Displayed
                         ↘ Client Error

A successful later poll or panel update clears an earlier transient Client Error. Last Poll and Physical Update intentionally include seconds because those values distinguish individual polling and hardware-update cycles. Normal polling is 30 seconds by default; active Let’s Cook polling is 5 seconds.

When the locally attached panel is known to be showing a downloaded remote preview, Home, Displays, and Display Details use that preview as the current image and overlay a 🌐 Remote badge on the image itself.

Display Busy means a local-server or remote-client process is actively writing to the physical panel. Client mode, waiting for a link, ordinary polling, and the existence of a stale lock file do not make the display busy.

Backup, restore, and diagnostic copies

Organization backups can contain encrypted or hashed display-client state. Restore behavior is explicit:

  • Leave Preserve this installation’s existing display-client links cleared for another Pi/server, a copied environment, or a cross-environment restore. Restored client credentials are removed, transient client jobs are discarded, and a restored Client display returns to Waiting for Link.
  • Select the option only for an in-place restore on the same Blackcap installation. Blackcap sanitizes the restored client state first, then reapplies that installation’s current live encrypted/hashed link state to matching restored display/client records. Stale credentials embedded in the backup are never activated directly.
  • The same rule applies to SQLite platform restores because those backups replace the application database. PostgreSQL platform backups do not contain the database, so restoring their application files does not replace current display-client rows.
  • Diagnostic clones and configuration copies strip token/credential fields and never receive a live attached-client credential.

Do not use a platform backup as a shortcut for cloning a linked Pi. Leave preservation cleared and relink the destination deliberately.

Remote client API

The paired client uses:

  • POST /api/display-client/pair
  • GET /api/display-client/state
  • GET /api/display-client/preview
  • POST /api/display-client/heartbeat
  • POST /api/display-client/unlink

State responses no longer put the durable token in the preview URL; the client uses the bearer-token header for all protected requests.

Standalone client compatibility

tools/blackcap_client_service.py still supports a dedicated client installation using /etc/blackcap/client.json. Run the script without --local-attached for that topology; the repository systemd unit is intentionally configured for a Pi that also runs the Blackcap server and therefore includes --local-attached. In standalone mode the client also normalizes and physically displays changed previews by default. Set hardware_renderer to false only for diagnostics on a machine without the Waveshare panel.

Repository-local client credential filenames are ignored by Git, but /etc/blackcap/client.json with mode 0600 is the preferred standalone location.

Operations

Check the service with:

sudo systemctl status blackcap-client.service --no-pager -l
sudo journalctl -u blackcap-client.service -n 120 --no-pager
sudo journalctl -u blackcap-client.service -f

The monthly deep-clean task detects Client ownership and exits successfully without touching the panel. The Let’s Cook timer watcher and all normal render paths also honor Client ownership.

Dedicated client-only schedule profile

A separate client-only Pi can still install only its local e-ink maintenance schedule:

cd /home/pi/Blackcap-Pi
sudo env \
  BLACKCAP_SCHEDULE_MODE=pi-client \
  BLACKCAP_SCHEDULE_APP_DIR=/home/pi/Blackcap-Pi \
  BLACKCAP_SCHEDULE_VENV_DIR=/home/pi/inky_env \
  BLACKCAP_SCHEDULE_USER=pi \
  BLACKCAP_ENABLE_EINK_DEEP_CLEAN=1 \
  BLACKCAP_ENABLE_LETS_COOK_EINK_WATCHER=0 \
  bash ./tools/install_blackcap_scheduled_operations.sh

The full Pi-server arrangement described above continues using the normal pi schedule profile; it does not switch schedules when the attached display enters Client content.

Troubleshooting

Service does not stay running

sudo systemctl status blackcap-client.service --no-pager -l
sudo journalctl -u blackcap-client.service -n 200 --no-pager

The attached-server unit must start tools/blackcap_client_service.py --local-attached. It does not require a command-line server URL because the URL and encrypted token come from the local display link.

Pairing succeeds but no preview downloads

  • Confirm the attached display is currently assigned Client.
  • Confirm the remote display has a generated preview.
  • Check that the remote URL is reachable from the Pi and its TLS certificate matches the hostname.
  • Confirm the remote client token has not been revoked.
  • Check for Downloaded remote display preview and Displayed remote preview on attached e-ink success messages in blackcap-client.service; their absence means the state response did not advertise a new preview version or the client could not complete the download.

Preview downloads but the panel does not change

Check the client log for transport validation, PNG compatibility conversion, display-lock, SPI/GPIO, or Waveshare-driver errors. Successful messages include artifact=gray4_transport, download_prepare_seconds=..., physical_refresh_seconds=..., and a detailed E-ink stages line covering input decode, local plane packing, lock wait, driver import, module initialization, controller initialization, each SPI plane, waveform wait, panel sleep, helper total, and subprocess overhead. The last valid artifact remains on disk and the panel retains the last successful frame after a failed update.

Display Busy remains visible

The banner should exist only during an active physical write. Blackcap removes malformed, dead-process, and reused-PID lock files. Check both services if a legitimate writer is still active:

sudo journalctl -u inky_admin.service -n 120 --no-pager
sudo journalctl -u blackcap-client.service -n 120 --no-pager

Client token was revoked

The client reports a relink-required error. Generate a new pairing code on the remote display and use Relink on the Pi display. A failed relink leaves the old working relationship untouched; a revoked old token cannot be reused.

TLS or hostname errors

Production linking requires HTTPS. Use the hostname covered by the server certificate. Insecure HTTP is accepted only in Blackcap development mode.

Physical-panel deployment checks

After client-mode changes, verify these behaviors on the actual Pi:

  • reboot while Client is active and confirm automatic reconnection;
  • restart the remote server and confirm the panel retains its image, then reconnects;
  • switch Client → local content → Client and confirm the saved link is reused;
  • attempt a failed Relink and confirm the existing working link is retained;
  • Unlink while Client remains active and confirm the physical image is unchanged;
  • allow a local scheduled window to expire during Client and confirm it is not replayed;
  • confirm an unchanged preview does not trigger another full panel refresh;
  • confirm one changed remote preview produces exactly one four-level grayscale refresh.

Transport contract

Blackcap preserves a PNG preview for every display because Admin and receiver surfaces need a standard image. For a Waveshare 13.3-inch Type K target, Blackcap additionally creates a device-specific transport artifact after the PNG is finalized and before the new display generation is advertised. Remote clients download it; attached local e-ink paths consume the same artifact directly.

See eink-rendering.md for all local/remote content paths and atomic publication details.

Format

Encoding: blackcap-gray4-planes-v1

The artifact contains:

  • an 8-byte magic/version value;
  • 960 × 680 dimensions;
  • the per-plane byte count;
  • compressed payload length;
  • SHA-256 of the uncompressed planes;
  • zlib-compressed command 0x24 and command 0x26 RAM planes.

Each uncompressed plane is 81,600 bytes. The client validates the header, dimensions, lengths, decompression, and checksum before replacing its last known-good artifact.

Pixel mapping

The four server-rendered grayscale values map to the controller planes as follows:

PNG gray Meaning 0x24 0x26
255 White 0 0
192 Light gray 1 0
128 Dark gray 0 1
0 Black 1 1

Pixels are packed MSB first, matching Waveshare's official Type K Python driver.

Client and local behavior

For structured Type K content, a standalone client normally downloads the controller-plane transport and skips PNG repacking. A Pi acting as both a Blackcap server and remote client may download both files and verify that the transport reconstructs to the exact same four-level image as the PNG before using the transport for hardware.

Photographic Menu content defaults to an exact one-bit PNG. No gray4 transport is published for that generation, and the client uses the normal black/white panel path. If the organization explicitly selects experimental native_four_gray, the client downloads the PNG and uses the installed Waveshare getbuffer_4Gray() / display_4Gray() path in automatic mode.

If a required artifact is absent or invalid, Blackcap retains the last valid panel artifact and frame after the failure.

Hardware push

Structured Meal Plan, Recipe, and Let’s Cook content uses Blackcap's optimized controller-plane transport in automatic mode. It sends the prepared 0x24 and 0x26 planes with the driver's bulk operation and does not perform a mandatory white pre-clear.

The environment override applies only to native four-gray content:

BLACKCAP_EINK_GRAY4_PUSH_MODE=auto      # default; structured content bulk, experimental native Menu official
BLACKCAP_EINK_GRAY4_PUSH_MODE=bulk      # diagnostic: force optimized gray4 transport
BLACKCAP_EINK_GRAY4_PUSH_MODE=official  # diagnostic: force installed driver for all gray4 content

The official path is slower because the Waveshare Python method translates and writes the frame byte by byte. One-bit Menu modes ignore this override. Detailed stage timings, the imported driver module path, the four-gray LUT fingerprint, selected panel mode, and artifact comparison hashes are written to blackcap-client.service.

Type K Menu strategy

Photographic Menu content now defaults to the organization-scoped crisp_bw strategy. The server publishes an exact one-bit PNG, does not publish a gray4 transport for that generation, and the client uses the normal fast black/white panel path. perceptual_four_tone provides a patterned black/white alternative, while native_four_gray remains experimental. See eink-rendering.md. Structured Meal Plan, Recipe, and Let’s Cook gray4 updates retain the fast bulk transport without a mandatory white pre-clear.

Let’s Cook delivery timing

Observed behavior

The July 15 client log separates three different sources of delay:

  1. Initial Let’s Cook discovery — the client was still using its normal 30-second interval while the display showed meal-plan content. It polled at 11:32:36, the server artifact was generated at approximately 11:32:40, and the client did not pull Let’s Cook state until 11:33:12. A client cannot switch to the active-session five-second interval until it first discovers the session.
  2. Physical panel refresh — successful downloads generally completed in about one second, while full Waveshare Type K refreshes consistently consumed roughly 23–24 seconds.
  3. Post-refresh sleep — after a full Let’s Cook refresh completed, the previous loop waited the normal active-session interval before checking whether another preview had arrived. The log repeatedly shows the next state pull about five to six seconds after the physical refresh message.

The full e-ink waveform is therefore the dominant unavoidable delay. Server preview debounce and the extra post-refresh sleep were avoidable.

Updated delivery model

Remote Raspberry Pi Client

A durable Let’s Cook action now follows this path:

controller action
→ durable Let’s Cook state update
→ immediate server preview render (no server debounce)
→ native Type K `L`/gray4 artifact generation on the server
→ artifact-backed preview version
→ client poll/download
→ attached e-ink full refresh
→ immediate Let’s Cook-only state recheck

The two-second remote preview watcher remains active only for retry and concurrent-state recovery. It is no longer the normal preview-generation path.

Attached local e-ink owned by the server

The existing local debounce remains unchanged:

controller action
→ durable pending marker
→ roughly ten-second coalescing watcher
→ full e-ink refresh

This prevents rapid local actions from launching back-to-back hardware refreshes.

Display-targeted Let’s Cook rendering

Let’s Cook resolves the destination display profile before creating its canvas. For the supported Waveshare 13.3-inch Type K panel—both attached local_eink and remote_pi_client targets—the renderer starts directly with a 960 × 680 Pillow L canvas instead of creating an RGB image and converting it afterward. Hosted receivers and kiosks retain RGB rendering.

The native Type K artifact has:

  • exact 960 × 680 resolution;
  • Pillow mode L grayscale;
  • only the four driver-supported values: 0, 128, 192, and 255;
  • a normal PNG encoding ready for the client hardware helper.

Text antialiasing can create intermediate grayscale samples even on an L canvas, so the renderer performs one final L-to-four-native-level point mapping before atomically publishing the PNG. It does not perform RGB conversion or resizing. The compatibility finalizer remains only as a no-op validator for native Let’s Cook artifacts and as a safety fallback for older/other content paths.

A conforming client download no longer performs:

  • EXIF transpose;
  • RGB-to-grayscale conversion;
  • Lanczos resize/crop;
  • per-pixel four-level quantization;
  • optimized PNG re-encoding;
  • a second full PNG verification decode before the helper starts.

The client performs a lightweight PNG header/profile check, atomically installs the file, and invokes the hardware helper. A compatibility conversion remains available for older servers that do not advertise a panel-ready profile.

Server-prepared controller transport

The server now keeps two related artifacts for a remote Type K display:

  • the normal PNG used by Admin and receiver surfaces;
  • a compressed blackcap-gray4-planes-v1 file containing the already packed 0x24 and 0x26 controller RAM planes.

The plane mapping is equivalent to Waveshare's official getbuffer_4Gray() plus display_4Gray() implementation. The custom artifact includes dimensions, plane length, compressed length, and a SHA-256 checksum. It is validated before atomic publication on the server and again before atomic installation on the Pi.

A standalone client-only Pi downloads only the transport file. A Pi that also runs Blackcap downloads the PNG as well for its local Admin UI.

Bulk Waveshare transfer

Waveshare's Python display_4Gray() implementation translates the packed image into two 81,600-byte planes and calls send_data() once for every output byte. That results in 163,200 Python SPI calls and chip-select toggles. Blackcap now sends the exact same two planes using two send_data2() bulk writes.

The panel still requires:

  • driver import and GPIO/SPI module initialization;
  • Type K four-gray controller initialization and LUT loading;
  • one bulk transfer for command 0x24;
  • one bulk transfer for command 0x26;
  • the physical four-gray waveform and busy wait;
  • panel deep sleep and hardware cleanup.

The server has removed PNG decode and plane packing from the normal client path. The client helper retains local PNG-to-plane generation only as an older-server fallback.

The panel is still put to sleep after every full refresh. Waveshare recommends sleep or power-off after refresh to avoid leaving the panel in a high-voltage state. Keeping the controller awake is therefore not enabled until stage timing proves that the remaining initialization cost justifies a separate, carefully bounded design.

Detailed timing

Every hardware refresh emits an E-ink stages line with:

  • input_decode_seconds — transport decompression/checksum or PNG decode;
  • plane_pack_seconds — zero for server transport, nonzero only for PNG fallback;
  • lock_wait_seconds;
  • driver_import_seconds;
  • module_init_seconds — Waveshare EPD construction and GPIO/SPI setup;
  • controller_init_seconds — reset, four-gray initialization, and LUT setup;
  • spi_plane_24_seconds;
  • spi_plane_26_seconds;
  • waveform_seconds — the visible panel transition and busy wait;
  • panel_sleep_seconds;
  • helper_total_seconds;
  • subprocess_overhead_seconds in the parent client log.

This separates the approximately five-second panel specification from Python process, initialization, transfer, and sleep time.

True five-second active polling cadence

The client now schedules the next Let’s Cook poll relative to the start of the previous poll. Request and state-processing time is subtracted from the sleep. A five-second setting therefore produces approximately five seconds between poll starts instead of five seconds plus request duration. The immediate post-refresh recheck remains unchanged.

Post-refresh recheck

After a physical refresh whose downloaded state was lets_cook, the client performs the next state request immediately rather than sleeping five seconds.

  • If actions arrived during the 23–24 second waveform, the latest artifact is downloaded and displayed next.
  • Multiple actions during the waveform are naturally coalesced into that latest artifact.
  • If no newer artifact exists, the client resumes the five-second Let’s Cook interval.
  • Non-Let’s-Cook content retains its normal configured polling behavior.

Remaining initial-session consideration

The first transition from normal content into Let’s Cook can still wait for the display’s normal client interval, which defaults to 30 seconds. Reducing that initial discovery window requires either configuring a shorter normal interval, polling every five seconds while Client content owns the panel, or adding a push/long-poll transport. This implementation does not broaden the normal-content polling policy; the immediate post-refresh check is Let’s Cook-only.

Diagnostic log fields

Successful client messages now include:

artifact=gray4_transport
server_advertised_panel_ready=true
download_prepare_seconds=<seconds>
physical_refresh_seconds=<seconds>
subprocess_overhead_seconds=<seconds>

The next line provides the detailed helper stage timing described above.

After a Let’s Cook physical refresh, the client also logs:

Let’s Cook e-ink refresh complete; checking the server again immediately.

These fields make server/download overhead distinguishable from the physical panel waveform.

First-frame commit behavior

Failure observed

The remote client correctly changed from current_meal_plan to lets_cook and switched to five-second polling, but every state response kept the prior meal-plan preview generation. Because the version never changed, the Pi correctly did not download or display another artifact.

Root cause

Two behaviors combined to hide the failure:

  1. The immutable preview-publication layer introduced after the bulk-transfer optimization could fail before displays.last_preview_updated_at was advanced.
  2. Inline display jobs returned an {ok: false} result when rendering or publication failed, but queue_display_content_push() discarded that result. The Let’s Cook assignment therefore became current even though the first frame was never committed.

The client was not at fault. It was receiving the old committed meal-plan generation while the current assignment already said lets_cook.

Corrected flow

The first-frame path is intentionally simple again:

  1. Let’s Cook atomically saves the display-scoped panel-ready PNG.
  2. For a supported Type K target, Blackcap generates and validates the matching .gray4 controller transport.
  3. Only after preparation completes, Blackcap updates the display preview path and a new microsecond UTC generation token in the database.
  4. The remote client versions both PNG and controller transport from that database token.
  5. An inline first-frame job that reports failure now raises immediately. It can no longer silently leave the display in Let’s Cook mode with the previous preview.
  6. _push_lets_cook() requires a committed generation result and logs the committed generation and transport status.

The controller transport and bulk SPI performance optimizations remain unchanged.

Expected server log

A successful first frame includes a line similar to:

Let’s Cook preview committed: org=... display=... session=... generation=... transport=True path=...

If rendering or publication fails, the Let’s Cook start request now reports the actual failure instead of appearing successful.

Expected client log

On the next poll after starting Let’s Cook, the preview timestamp/version changes and the client logs a download followed by the optimized physical refresh:

Preview URL: /api/display-client/preview?t=<new generation>
Downloaded panel transport ...
Downloaded remote display artifact mode=lets_cook ... artifact=gray4_transport...
Displayed remote preview on attached e-ink ...
On this page