Add boot splash screen with Pyxis constellation logo#12
Conversation
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 SummaryThis 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:
The implementation is functionally correct and ready to merge. Confidence Score: 4/5
Sequence DiagramsequenceDiagram
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
Last reviewed commit: ff00c1d |
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>
Saves one full 320x240 SPI screen write before the splash renders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| png_data = cairosvg.svg2png( | ||
| url=svg_path, | ||
| output_width=SPLASH_HEIGHT, # Square SVG scaled to screen height | ||
| output_height=SPLASH_HEIGHT, |
There was a problem hiding this 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:
| 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>
| 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") |
There was a problem hiding this 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:
- Keep the
static(fine as long as the include remains in exactly one file), or - Split into a
.h(extern const uint8_t splash_image[];) + a generated.cppthat 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.| // 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(); |
There was a problem hiding this 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).
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>
Summary
Test plan
🤖 Generated with Claude Code