Skip to content

Add boot splash screen with Pyxis constellation logo#12

Merged
torlando-tech merged 10 commits intomainfrom
feature/splash-screen
Mar 4, 2026
Merged

Add boot splash screen with Pyxis constellation logo#12
torlando-tech merged 10 commits intomainfrom
feature/splash-screen

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

  • Show splash screen within ~300ms of power-on (moved display init before GPS/WiFi/SD which take 20-45s)
  • Add SVG constellation logo with "PYXIS" text, auto-converted to RGB565 PROGMEM at build time
  • Render full-screen 320x240 splash instead of small centered icon
  • Add logo to README

Test plan

  • Splash visible immediately on boot, persists until LVGL UI loads
  • "PYXIS" text and constellation fully visible, not clipped
  • Build works cleanly on fresh clone (cairosvg/Pillow gracefully skipped if missing)

🤖 Generated with Claude Code

torlando-tech and others added 4 commits March 4, 2026 14:12
Move Display::init_hardware_only() and POWER_EN to right after serial
banner, before GPS/WiFi/SD/Reticulum init. Add 150ms delay after
POWER_EN HIGH so ST7789V power rail stabilizes before SPI commands
(without this, SWRESET is sent to an unpowered chip and silently lost).

Splash now visible for entire boot period (~18s) until LVGL takes over.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pyxis-icon.svg: Pyxis constellation icon (3 stars with connecting lines)
- generate_splash.py: PlatformIO pre-build script that renders the SVG to
  a 160x160 RGB565 PROGMEM header (SplashImage.h) using cairosvg + Pillow
- .gitignore: Exclude generated SplashImage.h
- platformio.ini: Add generate_splash.py to both build environments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scale constellation to 80% and shift down to make room for title text.
Generate splash at full display resolution instead of 160x160 centered.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR successfully adds a full-screen RGB565 boot splash screen to Pyxis by moving display hardware initialization ahead of slow peripherals and keeping the backlight off until GRAM is fully populated. The implementation is sound.

Key accomplishments:

  • Splash is rendered within ~300–370ms of power-on (dominated by mandatory ST7789V hardware delays)
  • init_hardware_only() / init() separation with distinct state guards (_hw_initialized / _initialized) is correctly implemented, fixing the prior double-init memory-leak bug
  • show_splash() is private and streams 320×240 pixels from PROGMEM in 512-byte chunks
  • generate_splash.py pre-build script renders the SVG at 240×240 and composites it on a 320×240 background; generated header includes a single-include-unit warning
  • LVGL screen background is pre-set to #1D1A1E to prevent a color flash on first render
  • All state machine guards and initialization sequence are correctly ordered

The implementation is functionally correct and ready to merge.

Confidence Score: 4/5

  • Code is functionally sound with no new critical regressions or observable bugs introduced.
  • All identified concerns from the prior review are speculative (hedging language about potential edge cases) or style-based (cosmetic naming suggestions with existing inline documentation). The core boot-splash logic is sound, state guards are correctly implemented, and the GRAM/backlight sequencing is safe on the target hardware. No concrete reproducible bugs were found during verification.
  • No files require special attention. All changes are functionally correct.

Sequence Diagram

sequenceDiagram
    participant MCU as ESP32 (setup())
    participant PWR as POWER_EN Rail
    participant DSP as ST7789V Display
    participant BKL as Backlight PWM
    participant PY as generate_splash.py
    participant LVGL as LVGL Task

    Note over PY: Pre-build: SVG → RGB565 → SplashImage.h

    MCU->>PWR: digitalWrite(POWER_EN, HIGH)
    MCU->>MCU: delay(150ms) — power rail stabilise
    MCU->>DSP: init_hardware_only()
    DSP->>BKL: ledcWrite(channel, 0) — backlight OFF
    DSP->>DSP: SPI init, CS/DC pin config
    DSP->>DSP: SWRESET → delay(150ms)
    DSP->>DSP: SLPOUT → delay(10ms)
    DSP->>DSP: COLMOD, MADCTL, INVON, NORON
    DSP->>DSP: DISPON → delay(10ms)
    DSP->>DSP: show_splash()
    DSP->>DSP: set_addr_window(0,0,319,239)
    DSP->>DSP: Stream 153,600 bytes PROGMEM → GRAM
    DSP->>BKL: set_brightness(_brightness) — backlight ON
    Note over MCU: Splash visible ✓

    MCU->>MCU: setup_hardware() (I2C, GPS, WiFi, SD, Reticulum…)
    MCU->>LVGL: setup_lvgl_and_ui()
    LVGL->>LVGL: LVGLInit::init() + Display::init()
    LVGL->>LVGL: lv_obj_set_style_bg_color(0x1D1A1E)
    Note over LVGL: LVGL renders over splash seamlessly
Loading

Last reviewed commit: ff00c1d

torlando-tech and others added 3 commits March 4, 2026 14:19
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Prevents double PSRAM allocation and LVGL driver re-registration
if init() were called more than once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Saves one full 320x240 SPI screen write before the splash renders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Comment on lines +56 to +59
png_data = cairosvg.svg2png(
url=svg_path,
output_width=SPLASH_HEIGHT, # Square SVG scaled to screen height
output_height=SPLASH_HEIGHT,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Icon is top-aligned instead of vertically centered on screen

The SVG is rasterised at SPLASH_HEIGHT × SPLASH_HEIGHT (240×240) and composited onto the 320×240 background at y=0. Because the SVG viewBox is 512×512 with a full-circle (r="256") background, the bottom corners of the circle are trimmed when the image is placed flush to the top edge. Adding a small vertical offset would center the circular icon on the screen:

Suggested change
png_data = cairosvg.svg2png(
url=svg_path,
output_width=SPLASH_HEIGHT, # Square SVG scaled to screen height
output_height=SPLASH_HEIGHT,
png_data = cairosvg.svg2png(
url=svg_path,
output_width=SPLASH_HEIGHT, # Square SVG scaled to screen height
output_height=SPLASH_HEIGHT,
)

Consider passing a y_offset calculated the same way as x_offset:

y_offset = (SPLASH_HEIGHT - icon.height) // 2
bg.paste(icon, (x_offset, y_offset), icon)

Currently y_offset is hard-coded to 0 on line 67, which is correct only when the icon fills the full screen height — but if output_height ever changes this will silently mis-position the logo.

Prompt To Fix With AI
This is a comment left during a code review.
Path: generate_splash.py
Line: 56-59

Comment:
**Icon is top-aligned instead of vertically centered on screen**

The SVG is rasterised at `SPLASH_HEIGHT × SPLASH_HEIGHT` (240×240) and composited onto the 320×240 background at `y=0`. Because the SVG viewBox is 512×512 with a full-circle (`r="256"`) background, the bottom corners of the circle are trimmed when the image is placed flush to the top edge. Adding a small vertical offset would center the circular icon on the screen:

```suggestion
        png_data = cairosvg.svg2png(
            url=svg_path,
            output_width=SPLASH_HEIGHT,   # Square SVG scaled to screen height
            output_height=SPLASH_HEIGHT,
        )
```

Consider passing a `y_offset` calculated the same way as `x_offset`:
```python
y_offset = (SPLASH_HEIGHT - icon.height) // 2
bg.paste(icon, (x_offset, y_offset), icon)
```

Currently `y_offset` is hard-coded to `0` on line 67, which is correct only when the icon fills the full screen height — but if `output_height` ever changes this will silently mis-position the logo.

How can I resolve this? If you propose a fix, please make it concise.

Move BG_COLOR inline into #ifndef block to avoid unused variable
when HAS_SPLASH_IMAGE is defined. Make show_splash() private since
it's only called internally from init_hardware_only().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

f.write(f"#define SPLASH_HEIGHT {SPLASH_HEIGHT}\n")
f.write(f"#define HAS_SPLASH_IMAGE 1\n\n")
f.write(f"// {SPLASH_WIDTH}x{SPLASH_HEIGHT} RGB565 big-endian ({len(rgb565_bytes)} bytes)\n")
f.write("static const uint8_t PROGMEM splash_image[] = {\n")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static on large PROGMEM array in a header risks silent flash duplication

static const uint8_t PROGMEM splash_image[] gives the array internal linkage. Any translation unit that includes SplashImage.h gets its own 153,600-byte copy placed in flash. Because Display.cpp is currently the only consumer (via #if __has_include("SplashImage.h")), there is no immediate problem — but a future include from a second file would silently double flash usage with no linker error.

The conventional pattern for generated data headers on Arduino/ESP32 is to either:

  1. Keep the static (fine as long as the include remains in exactly one file), or
  2. Split into a .h (extern const uint8_t splash_image[];) + a generated .cpp that defines it.

At minimum, add a comment to the generated header warning against including it from multiple translation units:

            f.write("// WARNING: include this header from exactly ONE translation unit.\n")
            f.write("static const uint8_t PROGMEM splash_image[] = {\n")
Prompt To Fix With AI
This is a comment left during a code review.
Path: generate_splash.py
Line: 88

Comment:
**`static` on large PROGMEM array in a header risks silent flash duplication**

`static const uint8_t PROGMEM splash_image[]` gives the array internal linkage. Any translation unit that includes `SplashImage.h` gets its own 153,600-byte copy placed in flash. Because `Display.cpp` is currently the only consumer (via `#if __has_include("SplashImage.h")`), there is no immediate problem — but a future include from a second file would silently double flash usage with no linker error.

The conventional pattern for generated data headers on Arduino/ESP32 is to either:
1. Keep the `static` (fine as long as the include remains in exactly one file), or
2. Split into a `.h` (`extern const uint8_t splash_image[];`) + a generated `.cpp` that defines it.

At minimum, add a comment to the generated header warning against including it from multiple translation units:

```python
            f.write("// WARNING: include this header from exactly ONE translation unit.\n")
            f.write("static const uint8_t PROGMEM splash_image[] = {\n")
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1202 to +1211
// Enable peripheral power rail before display init.
// Display needs ~120ms after power-on before accepting SPI commands
// (ST7789V power-on reset time). Without this delay, SWRESET is sent
// to an unpowered chip and silently lost.
pinMode(Pin::POWER_EN, OUTPUT);
digitalWrite(Pin::POWER_EN, HIGH);
delay(150);

// Show boot splash ASAP — before any slow init (GPS, WiFi, SD, Reticulum).
Hardware::TDeck::Display::init_hardware_only();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boot time budget likely exceeds the stated ~300 ms target

The PR description promises the splash is visible within ~300 ms of power-on, but the mandatory hardware delays alone add up to at least ~370 ms before the backlight turns on:

Step Delay
delay(150) in setup() (ST7789V power-on stabilisation) 150 ms
SWRESET command + delay(150) in init_registers() 150 ms
SLPOUT + delay(10) 10 ms
INVON, NORON, DISPON + their delay(10) calls ~30 ms
Pixel streaming (~153 600 bytes at 40 MHz SPI) ~30 ms
Estimated total before backlight on ~370 ms

These delays are largely mandated by the ST7789V datasheet, so the budget may not be shrinkable without risking reliability. The PR description's "~300 ms" claim should be updated to reflect the realistic figure, or the two 150 ms delays (one in setup(), one inside SWRESET) should be examined to see whether any overlap is safe (the datsheet allows the post-SWRESET wait to count toward the boot stabilisation window on some panels).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main.cpp
Line: 1202-1211

Comment:
**Boot time budget likely exceeds the stated ~300 ms target**

The PR description promises the splash is visible within ~300 ms of power-on, but the mandatory hardware delays alone add up to at least ~370 ms before the backlight turns on:

| Step | Delay |
|---|---|
| `delay(150)` in `setup()` (ST7789V power-on stabilisation) | 150 ms |
| SWRESET command + `delay(150)` in `init_registers()` | 150 ms |
| SLPOUT + `delay(10)` | 10 ms |
| INVON, NORON, DISPON + their `delay(10)` calls | ~30 ms |
| Pixel streaming (~153 600 bytes at 40 MHz SPI) | ~30 ms |
| **Estimated total before backlight on** | **~370 ms** |

These delays are largely mandated by the ST7789V datasheet, so the budget may not be shrinkable without risking reliability. The PR description's "~300 ms" claim should be updated to reflect the realistic figure, or the two 150 ms delays (one in `setup()`, one inside `SWRESET`) should be examined to see whether any overlap is safe (the datsheet allows the post-SWRESET wait to count toward the boot stabilisation window on some panels).

How can I resolve this? If you propose a fix, please make it concise.

Consolidate #ifndef/#ifdef into single #ifdef/#else/#endif block.
Add warning comment to generated header about static linkage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

@torlando-tech torlando-tech merged commit 70b8df0 into main Mar 4, 2026
3 checks passed
@torlando-tech torlando-tech deleted the feature/splash-screen branch March 4, 2026 23:38
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.

1 participant