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,124 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Converts scraped JSON animation data to firmware/src/splash_animations.h.
|
||||
*
|
||||
* Per-animation palette (up to 10 entries) is converted to RGB565. Cells in
|
||||
* each frame are palette indices (0..9). Splash module looks up colors via
|
||||
* palette[cell].
|
||||
*
|
||||
* Usage: node convert_to_c.js [--in DIR] [--out FILE]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (k, def) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : def; };
|
||||
|
||||
const IN_DIR = path.resolve(opt('--in', path.join(__dirname, 'claudepix_data')));
|
||||
const OUT_FILE = path.resolve(opt('--out',
|
||||
path.join(__dirname, '..', 'firmware', 'src', 'splash_animations.h')));
|
||||
|
||||
const PALETTE_SIZE = 10;
|
||||
|
||||
function safeIdent(s) {
|
||||
return s.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
function hexToRgb565(hex) {
|
||||
if (!hex || hex === 'transparent') return 0x10A2; // dark bg
|
||||
let h = hex.replace('#', '');
|
||||
if (h.length === 3) h = h.split('').map(c => c + c).join('');
|
||||
const r = parseInt(h.substr(0, 2), 16);
|
||||
const g = parseInt(h.substr(2, 2), 16);
|
||||
const b = parseInt(h.substr(4, 2), 16);
|
||||
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
|
||||
}
|
||||
|
||||
function paletteToRgb565(palette) {
|
||||
const out = new Array(PALETTE_SIZE).fill(0x10A2);
|
||||
for (let i = 0; i < palette.length && i < PALETTE_SIZE; i++) {
|
||||
out[i] = hexToRgb565(palette[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(IN_DIR)) {
|
||||
console.error(`No scraped data at ${IN_DIR}. Run scrape_claudepix.js first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const indexPath = path.join(IN_DIR, '_index.json');
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.error(`Missing ${indexPath}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
||||
console.log(`Converting ${index.length} animations`);
|
||||
|
||||
let out = '';
|
||||
out += '// ============================================================\n';
|
||||
out += '// Splash animations — generated by tools/convert_to_c.js.\n';
|
||||
out += '// Source: https://claudepix.vercel.app (20x20 pixel-art creature\n';
|
||||
out += '// animation library). Frames extracted by tools/scrape_claudepix.js\n';
|
||||
out += '// from per-animation HTML files served by the source site.\n';
|
||||
out += '// Do not edit by hand — re-run the scraper + converter to refresh.\n';
|
||||
out += '// ============================================================\n';
|
||||
out += '// Each animation carries a 10-entry RGB565 palette.\n';
|
||||
out += '// Cell values 0..9 index into palette.\n';
|
||||
out += '#pragma once\n#include <stdint.h>\n\n';
|
||||
|
||||
out += `#define SPLASH_PALETTE_SIZE ${PALETTE_SIZE}\n\n`;
|
||||
|
||||
out += 'typedef struct {\n';
|
||||
out += ' const char *name;\n';
|
||||
out += ' const char *category;\n';
|
||||
out += ' uint16_t frame_count;\n';
|
||||
out += ' const uint16_t *palette;\n';
|
||||
out += ' const uint8_t (*frames)[400];\n';
|
||||
out += ' const uint16_t *holds;\n';
|
||||
out += '} splash_anim_def_t;\n\n';
|
||||
|
||||
const entries = [];
|
||||
|
||||
for (const meta of index) {
|
||||
const ident = safeIdent(meta.filename.replace(/\.html?$/, ''));
|
||||
const dataPath = path.join(IN_DIR, meta.filename.replace(/\.html?$/, '.json'));
|
||||
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
|
||||
|
||||
const pal565 = paletteToRgb565(data.palette);
|
||||
out += `static const uint16_t splash_${ident}_palette[${PALETTE_SIZE}] = {`;
|
||||
out += pal565.map(c => `0x${c.toString(16).toUpperCase().padStart(4, '0')}`).join(',');
|
||||
out += '};\n';
|
||||
|
||||
out += `static const uint8_t splash_${ident}_frames[${data.frames.length}][400] = {\n`;
|
||||
for (const f of data.frames) {
|
||||
const flat = [];
|
||||
for (let r = 0; r < 20; r++)
|
||||
for (let c = 0; c < 20; c++)
|
||||
flat.push(f.grid[r][c]);
|
||||
out += ' {' + flat.join(',') + '},\n';
|
||||
}
|
||||
out += '};\n';
|
||||
|
||||
out += `static const uint16_t splash_${ident}_holds[${data.frames.length}] = {`;
|
||||
out += data.frames.map(f => f.hold).join(',');
|
||||
out += '};\n\n';
|
||||
|
||||
entries.push({ ident, name: data.name, category: data.category, count: data.frames.length });
|
||||
}
|
||||
|
||||
out += `#define SPLASH_ANIM_COUNT ${entries.length}\n`;
|
||||
out += 'static const splash_anim_def_t splash_anims[SPLASH_ANIM_COUNT] = {\n';
|
||||
for (const e of entries) {
|
||||
out += ` {"${e.name}", "${e.category}", ${e.count}, splash_${e.ident}_palette, splash_${e.ident}_frames, splash_${e.ident}_holds},\n`;
|
||||
}
|
||||
out += '};\n';
|
||||
|
||||
fs.writeFileSync(OUT_FILE, out);
|
||||
console.log(`Wrote ${OUT_FILE} (${entries.length} animations, ${(out.length / 1024).toFixed(1)} KB)`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user