Migrate to Waveshare ESP32-S3-Touch-AMOLED-2.16 with auto-rotation, splash, and battery
Full hardware swap from Panlee SC01 Plus (480×320 IPS) to Waveshare 2.16" square AMOLED (480×480, CO5300 + CST9220 + AXP2101 + QMI8658). Library stack moves to Arduino_GFX, SensorLib, XPowersLib on the pioarduino platform 55.03.38-1 (Arduino Core 3.x). UI: - 4 screens (splash, usage, controller, bluetooth) with 3-button physical navigation: GPIO 0 = prev, AXP PKEY = cycle, GPIO 18 = next. - IMU-driven 90° auto-rotation. CO5300 can't rotate via MADCTL, so flush callback does CPU strip-rotation in PARTIAL render mode. Rotation transitions use AMOLED brightness flash (instant black → redraw → ramp). - Battery indicator (Lucide icons) in upper-right, RGB565A8 alpha so it blends over the splash animations. - USB plugged/unplugged auto-switches between Usage and Controller screens (suppressed while on splash). - Fonts and icons re-scaled ~1.9× for the higher-DPI panel; 20px margins to clear rounded corners. Splash: - 13 × 20×20 pixel-art creature animations sourced from claudepix.vercel.app via tools/scrape_claudepix.js (scraper handles both PRESET creature-engine and standalone FRAMES+PAL formats). tools/convert_to_c.js emits firmware/src/splash_animations.h. Attribution preserved in README and the generated header. Tooling: - tools/png_to_lvgl.js converts alpha PNGs to LVGL RGB565A8 (planar layout, with --tint to colorize Lucide black-on-transparent sources). - tools/scrape_claudepix.js, tools/convert_to_c.js for the splash pipeline. Docs: - New worktree CLAUDE.md with hardware pin map, file inventory, build commands, and the 8 critical gotchas (CO5300 rotation, OPI PSRAM, pioarduino requirement, LVGL 9 font patching, centralized touch read, even-aligned flush regions, touch swap/mirror values, RGB565A8 layout). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
10b8052bcc
commit
97a5443601
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
// Convert a PNG with alpha channel to an LVGL RGB565A8 C array.
|
||||
// Layout: w*h RGB565 pixels (little-endian) followed by w*h alpha bytes.
|
||||
// Usage: node png_to_lvgl.js <input.png> <symbol_name> [W_MACRO] [H_MACRO] [--tint=RRGGBB]
|
||||
// Default tint=FFFFFF — Lucide PNGs are black-on-transparent, so without a tint
|
||||
// the icon would render as invisible black; white is the typical choice for UI overlays.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { PNG } = require('pngjs');
|
||||
|
||||
function rgb565(r, g, b) {
|
||||
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
|
||||
}
|
||||
|
||||
function convert(pngPath, symbol, wMacro, hMacro, tint) {
|
||||
const buf = fs.readFileSync(pngPath);
|
||||
const png = PNG.sync.read(buf);
|
||||
const w = png.width, h = png.height;
|
||||
const total = w * h;
|
||||
|
||||
const colorBytes = Buffer.alloc(total * 2);
|
||||
const alphaBytes = Buffer.alloc(total);
|
||||
|
||||
const tr = tint ? (tint >> 16) & 0xFF : null;
|
||||
const tg = tint ? (tint >> 8) & 0xFF : null;
|
||||
const tb = tint ? tint & 0xFF : null;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const r = tint !== null ? tr : png.data[i * 4 + 0];
|
||||
const g = tint !== null ? tg : png.data[i * 4 + 1];
|
||||
const b = tint !== null ? tb : png.data[i * 4 + 2];
|
||||
const a = png.data[i * 4 + 3];
|
||||
const c = rgb565(r, g, b);
|
||||
colorBytes[i * 2 + 0] = c & 0xFF;
|
||||
colorBytes[i * 2 + 1] = (c >> 8) & 0xFF;
|
||||
alphaBytes[i] = a;
|
||||
}
|
||||
|
||||
const all = Buffer.concat([colorBytes, alphaBytes]);
|
||||
|
||||
let out = `#define ${wMacro} ${w}\n#define ${hMacro} ${h}\n`;
|
||||
out += `// RGB565A8: ${total} RGB565 pixels (little-endian) followed by ${total} alpha bytes\n`;
|
||||
out += `static const uint8_t ${symbol}[${all.length}] = {\n `;
|
||||
const lines = [];
|
||||
for (let i = 0; i < all.length; i += 16) {
|
||||
const row = [];
|
||||
for (let j = 0; j < 16 && i + j < all.length; j++) {
|
||||
row.push('0x' + all[i + j].toString(16).padStart(2, '0').toUpperCase());
|
||||
}
|
||||
lines.push(row.join(', '));
|
||||
}
|
||||
out += lines.join(',\n ');
|
||||
out += '\n};\n';
|
||||
return out;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
let tint = 0xFFFFFF;
|
||||
const positional = [];
|
||||
for (const a of args) {
|
||||
if (a.startsWith('--tint=')) tint = parseInt(a.slice(7), 16);
|
||||
else if (a === '--no-tint') tint = null;
|
||||
else positional.push(a);
|
||||
}
|
||||
const [pngPath, symbol, wMacro, hMacro] = positional;
|
||||
if (!pngPath || !symbol) {
|
||||
console.error('Usage: node png_to_lvgl.js <input.png> <symbol_name> [W_MACRO] [H_MACRO] [--tint=RRGGBB | --no-tint]');
|
||||
process.exit(1);
|
||||
}
|
||||
const base = symbol.toUpperCase().replace(/_DATA$/, '');
|
||||
process.stdout.write(convert(pngPath, symbol, wMacro || `${base}_W`, hMacro || `${base}_H`, tint));
|
||||
}
|
||||
|
||||
module.exports = { convert, rgb565 };
|
||||
Reference in New Issue
Block a user