velxio/frontend/src/components/velxio-components/Esp32Element.ts

440 lines
14 KiB
TypeScript
Raw Normal View History

/**
* ESP32 Web Components
*
fix(install): unblock self-hosting + drop forced wokwi clones Resolves several install pain points reported by users (#108, #120) and removes the obligatory upstream-clone step that confused contributors and slowed down every Docker build. Install fixes: - nginx: server_name → catch-all default_server, drop Debian's stock site so reverse-proxied users no longer get the "Welcome to nginx" page. - entrypoint: auto-generate SECRET_KEY at first boot, persisted under data/.secret_key. backend/.env is now optional in docker-compose.yml. - backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on some Python builds — caused uvicorn startup failures on WSL). Wokwi libs come from npm: - @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in frontend/package.json. Vite aliases removed. - Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements / wokwi-boards. Frontend stage is just COPY + npm install + build:docker. - Board SVGs vendored under frontend/public/boards/ (10 deduped against existing files, 2 truly new). third-party/wokwi-* clones become reference- only credits — generate-component-metadata.ts skips gracefully when absent. Production config split out: - docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf, update-third-party.bat removed. Production deployment lives in its own repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups + pinned upstream commit). Verified locally: 1161 frontend tests pass, build:docker completes clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:04:11 +07:00
* Uses board SVG assets for realistic rendering.
* Pin positions are in mm × 5 px/mm.
*
* Supports three variants via the `board-kind` attribute:
* - esp32 → ESP32 DevKit V1 (141 × 265 px)
* - esp32-s3 → ESP32-S3 DevKitC-1 (128 × 350 px)
* - esp32-c3 → ESP32-C3 DevKitM-1 (127 × 215 px)
*/
fix(install): unblock self-hosting + drop forced wokwi clones Resolves several install pain points reported by users (#108, #120) and removes the obligatory upstream-clone step that confused contributors and slowed down every Docker build. Install fixes: - nginx: server_name → catch-all default_server, drop Debian's stock site so reverse-proxied users no longer get the "Welcome to nginx" page. - entrypoint: auto-generate SECRET_KEY at first boot, persisted under data/.secret_key. backend/.env is now optional in docker-compose.yml. - backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on some Python builds — caused uvicorn startup failures on WSL). Wokwi libs come from npm: - @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in frontend/package.json. Vite aliases removed. - Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements / wokwi-boards. Frontend stage is just COPY + npm install + build:docker. - Board SVGs vendored under frontend/public/boards/ (10 deduped against existing files, 2 truly new). third-party/wokwi-* clones become reference- only credits — generate-component-metadata.ts skips gracefully when absent. Production config split out: - docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf, update-third-party.bat removed. Production deployment lives in its own repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups + pinned upstream commit). Verified locally: 1161 frontend tests pass, build:docker completes clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:04:11 +07:00
// Board SVG assets are served from frontend/public/boards/ as static URLs.
// No Vite import resolution needed — these resolve at runtime against the
// static asset root.
const esp32SvgUrl = '/boards/esp32-devkit-v1.svg';
const esp32S3SvgUrl = '/boards/esp32-s3.svg';
const esp32C3SvgUrl = '/boards/esp32-c3.svg';
const esp32DevkitCV4SvgUrl = '/boards/esp32-devkit-c-v4.svg';
const esp32CamSvgUrl = '/boards/esp32-cam.svg';
const wemosLolin32SvgUrl = '/boards/wemos-lolin32-lite.svg';
const xiaoEsp32S3SvgUrl = '/boards/xiao-esp32-s3.svg';
const arduinoNanoEsp32SvgUrl = '/boards/arduino-nano-esp32.svg';
const xiaoEsp32C3SvgUrl = '/boards/xiao-esp32-c3.svg';
const aitewinC3SvgUrl = '/boards/esp32c3-supermini.svg';
// ─── Pin positions (mm × 5 px/mm, from board.json) ───────────────────────────
// ESP32 DevKit V1: 28.2 mm × 53 mm → 141 × 265 px
// Left col: x = 1.27 mm → 6 px | Right col: x = 26.8 mm → 134 px
const PINS_ESP32 = [
{ name: 'EN', x: 6, y: 29 },
{ name: 'VN', x: 6, y: 42 },
{ name: 'VP', x: 6, y: 54 },
{ name: '34', x: 6, y: 67 },
{ name: '35', x: 6, y: 80 },
{ name: '32', x: 6, y: 93 },
{ name: '33', x: 6, y: 105 },
{ name: '25', x: 6, y: 118 },
{ name: '26', x: 6, y: 131 },
{ name: '27', x: 6, y: 143 },
{ name: '14', x: 6, y: 156 },
{ name: '12', x: 6, y: 169 },
{ name: '13', x: 6, y: 181 },
{ name: 'GND', x: 6, y: 194 },
{ name: 'VIN', x: 6, y: 207 },
{ name: '3V3', x: 134, y: 207 },
{ name: 'GND2', x: 134, y: 194 },
{ name: '15', x: 134, y: 181 },
{ name: '2', x: 134, y: 169 },
{ name: '4', x: 134, y: 156 },
{ name: 'RX2', x: 134, y: 143 },
// RX2 is the silkscreen label for GPIO 16. Sketches that wire to pin
// '16' (e.g. ledcAttach(16, …) or any example referencing GPIO 16)
// must land on this same coordinate, otherwise pinPositionCalculator
// can't resolve the wire endpoint and the wire visually floats off
// the board. Same story for TX2 / 17 below.
{ name: '16', x: 134, y: 143 },
{ name: 'TX2', x: 134, y: 131 },
{ name: '17', x: 134, y: 131 },
{ name: '5', x: 134, y: 118 },
{ name: '18', x: 134, y: 105 },
{ name: '19', x: 134, y: 93 },
{ name: '21', x: 134, y: 80 },
{ name: 'RX0', x: 134, y: 67 },
{ name: 'TX0', x: 134, y: 54 },
{ name: '22', x: 134, y: 42 },
{ name: '23', x: 134, y: 29 },
];
// ESP32-S3 DevKitC-1: 25.527 mm × 70.057 mm → 128 × 350 px
// Left col: x = 1.343 mm → 7 px | Right col: x = 24.19 mm → 121 px
const PINS_ESP32_S3 = [
{ name: '3V3.1', x: 7, y: 38 },
{ name: '3V3.2', x: 7, y: 51 },
{ name: 'RST', x: 7, y: 64 },
{ name: '4', x: 7, y: 76 },
{ name: '5', x: 7, y: 89 },
{ name: '6', x: 7, y: 102 },
{ name: '7', x: 7, y: 115 },
{ name: '15', x: 7, y: 127 },
{ name: '16', x: 7, y: 140 },
{ name: '17', x: 7, y: 153 },
{ name: '18', x: 7, y: 166 },
{ name: '8', x: 7, y: 178 },
{ name: '3', x: 7, y: 191 },
{ name: '46', x: 7, y: 203 },
{ name: '9', x: 7, y: 216 },
{ name: '10', x: 7, y: 229 },
{ name: '11', x: 7, y: 242 },
{ name: '12', x: 7, y: 254 },
{ name: '13', x: 7, y: 267 },
{ name: '14', x: 7, y: 280 },
{ name: '5V', x: 7, y: 292 },
{ name: 'GND.1', x: 7, y: 305 },
{ name: 'GND.2', x: 121, y: 38 },
{ name: 'TX', x: 121, y: 51 },
{ name: 'RX', x: 121, y: 64 },
{ name: '1', x: 121, y: 76 },
{ name: '2', x: 121, y: 89 },
{ name: '42', x: 121, y: 102 },
{ name: '41', x: 121, y: 115 },
{ name: '40', x: 121, y: 127 },
{ name: '39', x: 121, y: 140 },
{ name: '38', x: 121, y: 153 },
{ name: '37', x: 121, y: 166 },
{ name: '36', x: 121, y: 178 },
{ name: '35', x: 121, y: 191 },
{ name: '0', x: 121, y: 203 },
{ name: '45', x: 121, y: 216 },
{ name: '48', x: 121, y: 229 },
{ name: '47', x: 121, y: 242 },
{ name: '21', x: 121, y: 254 },
{ name: '20', x: 121, y: 267 },
{ name: '19', x: 121, y: 280 },
{ name: 'GND.3', x: 121, y: 292 },
{ name: 'GND.4', x: 121, y: 305 },
];
// ESP32-C3 DevKitM-1: 25.4 mm × 42.91 mm → 127 × 215 px
// Left col: x = 1 mm → 5 px | Right col: x = 24.2 mm → 121 px
const PINS_ESP32_C3 = [
{ name: 'GND.1', x: 5, y: 26 },
{ name: '3V3.1', x: 5, y: 39 },
{ name: '3V3.2', x: 5, y: 51 },
{ name: '2', x: 5, y: 64 },
{ name: '3', x: 5, y: 77 },
{ name: 'GND.2', x: 5, y: 89 },
{ name: 'RST', x: 5, y: 102 },
{ name: 'GND.3', x: 5, y: 115 },
{ name: '0', x: 5, y: 127 },
{ name: '1', x: 5, y: 140 },
{ name: '10', x: 5, y: 153 },
{ name: 'GND.4', x: 5, y: 166 },
{ name: '5V.1', x: 5, y: 178 },
{ name: '5V.2', x: 5, y: 191 },
{ name: 'GND.5', x: 5, y: 204 },
{ name: 'GND.6', x: 121, y: 204 },
{ name: '19', x: 121, y: 191 },
{ name: '18', x: 121, y: 178 },
{ name: 'GND.7', x: 121, y: 166 },
{ name: '4', x: 121, y: 153 },
{ name: '5', x: 121, y: 140 },
{ name: '6', x: 121, y: 127 },
{ name: '7', x: 121, y: 115 },
{ name: 'GND.8', x: 121, y: 102 },
{ name: '8', x: 121, y: 89 },
{ name: '9', x: 121, y: 77 },
{ name: 'GND.9', x: 121, y: 64 },
{ name: 'RX', x: 121, y: 51 },
{ name: 'TX', x: 121, y: 39 },
{ name: 'GND.10', x: 121, y: 26 },
];
// ESP32 DevKit C V4: 27.9 mm × 56.628 mm → 140 × 283 px
// Left col: x = 1.22 mm → 6 px | Right col: x = 26.66 mm → 133 px
const PINS_ESP32_DEVKIT_C_V4 = [
{ name: '3V3', x: 6, y: 38 },
{ name: 'EN', x: 6, y: 51 },
{ name: 'VP', x: 6, y: 64 },
{ name: 'VN', x: 6, y: 76 },
{ name: '34', x: 6, y: 89 },
{ name: '35', x: 6, y: 102 },
{ name: '32', x: 6, y: 114 },
{ name: '33', x: 6, y: 127 },
{ name: '25', x: 6, y: 140 },
{ name: '26', x: 6, y: 152 },
{ name: '27', x: 6, y: 165 },
{ name: '14', x: 6, y: 178 },
{ name: '12', x: 6, y: 191 },
{ name: 'GND.1', x: 6, y: 203 },
{ name: '13', x: 6, y: 216 },
{ name: 'D2', x: 6, y: 229 },
{ name: 'D3', x: 6, y: 241 },
{ name: 'CMD', x: 6, y: 254 },
{ name: '5V', x: 6, y: 267 },
{ name: 'GND.2', x: 133, y: 38 },
{ name: '23', x: 133, y: 51 },
{ name: '22', x: 133, y: 64 },
{ name: 'TX', x: 133, y: 76 },
{ name: 'RX', x: 133, y: 89 },
{ name: '21', x: 133, y: 102 },
{ name: 'GND.3', x: 133, y: 114 },
{ name: '19', x: 133, y: 127 },
{ name: '18', x: 133, y: 140 },
{ name: '5', x: 133, y: 152 },
{ name: '17', x: 133, y: 165 },
{ name: '16', x: 133, y: 178 },
{ name: '4', x: 133, y: 191 },
{ name: '0', x: 133, y: 203 },
{ name: '2', x: 133, y: 216 },
{ name: '15', x: 133, y: 229 },
{ name: 'D1', x: 133, y: 241 },
{ name: 'D0', x: 133, y: 254 },
{ name: 'CLK', x: 133, y: 267 },
];
// ESP32-CAM: 27.2 mm × 40.42 mm → 136 × 202 px
// Left col: x = 1.9 mm → 10 px | Right col: x = 24.76 mm → 124 px
const PINS_ESP32_CAM = [
{ name: '5V.1', x: 10, y: 25 },
{ name: 'GND.1', x: 10, y: 37 },
{ name: '12', x: 10, y: 50 },
{ name: '13', x: 10, y: 63 },
{ name: '15', x: 10, y: 76 },
{ name: '14', x: 10, y: 88 },
{ name: '2', x: 10, y: 101 },
{ name: '4', x: 10, y: 114 },
{ name: '3V3', x: 124, y: 25 },
{ name: '16', x: 124, y: 37 },
{ name: '0', x: 124, y: 50 },
{ name: 'GND.2', x: 124, y: 63 },
{ name: 'VCC', x: 124, y: 76 },
{ name: 'RX', x: 124, y: 88 },
{ name: 'TX', x: 124, y: 101 },
{ name: 'GND.3', x: 124, y: 114 },
];
// Wemos Lolin32 Lite: 25.6 mm × 50 mm → 128 × 250 px
// Left col: x = 1.4 mm → 7 px | Right col: x = 24.26 mm → 121 px
const PINS_WEMOS_LOLIN32 = [
{ name: 'VP', x: 7, y: 37 },
{ name: 'VN', x: 7, y: 50 },
{ name: 'EN', x: 7, y: 62 },
{ name: 'GPIO34', x: 7, y: 75 },
{ name: 'GPIO35', x: 7, y: 88 },
{ name: 'GPIO32', x: 7, y: 100 },
{ name: 'GPIO33', x: 7, y: 113 },
{ name: 'GPIO25', x: 7, y: 126 },
{ name: 'GPIO26', x: 7, y: 139 },
{ name: 'GPIO27', x: 7, y: 151 },
{ name: 'GPIO14', x: 7, y: 164 },
{ name: 'GPIO12', x: 7, y: 177 },
{ name: 'GND', x: 7, y: 189 },
{ name: 'GPIO13', x: 121, y: 189 },
{ name: 'GPIO15', x: 121, y: 177 },
{ name: 'GPIO2', x: 121, y: 164 },
{ name: 'GPIO0', x: 121, y: 151 },
{ name: 'GPIO4', x: 121, y: 139 },
{ name: 'GPIO16', x: 121, y: 126 },
{ name: 'GPIO17', x: 121, y: 113 },
{ name: 'GPIO5', x: 121, y: 100 },
{ name: 'GPIO18', x: 121, y: 88 },
{ name: 'GPIO23', x: 121, y: 75 },
{ name: 'GPIO19', x: 121, y: 62 },
{ name: 'GPIO22', x: 121, y: 50 },
{ name: '3V', x: 121, y: 37 },
];
// Seeed XIAO ESP32-S3: 18.1 mm × 23.4 mm → 91 × 117 px
// Left col: x = 1.43 mm → 7 px | Right col: x = 16.51 mm → 83 px
const PINS_XIAO_ESP32_S3 = [
{ name: 'D0', x: 7, y: 25 },
{ name: 'D1', x: 7, y: 37 },
{ name: 'D2', x: 7, y: 50 },
{ name: 'D3', x: 7, y: 63 },
{ name: 'D4', x: 7, y: 75 },
{ name: 'D5', x: 7, y: 88 },
{ name: 'D6', x: 7, y: 101 },
{ name: 'D7', x: 83, y: 101 },
{ name: 'D8', x: 83, y: 88 },
{ name: 'D9', x: 83, y: 75 },
{ name: 'D10', x: 83, y: 63 },
{ name: '3V3', x: 83, y: 50 },
{ name: 'GND', x: 83, y: 37 },
{ name: '5V', x: 83, y: 25 },
];
// Arduino Nano ESP32: 43.3 mm × 18.05 mm → 217 × 90 px (horizontal layout)
// Top row: y = 1.5 mm → 8 px | Bottom row: y = 16.74 mm → 84 px
const PINS_ARDUINO_NANO_ESP32 = [
{ name: 'D12', x: 24, y: 8 },
{ name: 'D11', x: 37, y: 8 },
{ name: 'D10', x: 49, y: 8 },
{ name: 'D9', x: 62, y: 8 },
{ name: 'D8', x: 75, y: 8 },
{ name: 'D7', x: 88, y: 8 },
{ name: 'D6', x: 100, y: 8 },
{ name: 'D5', x: 113, y: 8 },
{ name: 'D4', x: 126, y: 8 },
{ name: 'D3', x: 138, y: 8 },
{ name: 'D2', x: 151, y: 8 },
{ name: 'GND.1', x: 164, y: 8 },
{ name: 'RST', x: 176, y: 8 },
{ name: 'RX0', x: 189, y: 8 },
{ name: 'TX1', x: 202, y: 8 },
{ name: 'D13', x: 24, y: 84 },
{ name: '3V3', x: 37, y: 84 },
{ name: 'B0', x: 49, y: 84 },
{ name: 'A0', x: 62, y: 84 },
{ name: 'A1', x: 75, y: 84 },
{ name: 'A2', x: 88, y: 84 },
{ name: 'A3', x: 100, y: 84 },
{ name: 'A4', x: 113, y: 84 },
{ name: 'A5', x: 126, y: 84 },
{ name: 'A6', x: 138, y: 84 },
{ name: 'A7', x: 151, y: 84 },
{ name: 'VBUS', x: 164, y: 84 },
{ name: 'B1', x: 176, y: 84 },
{ name: 'GND.2', x: 189, y: 84 },
{ name: 'VIN', x: 202, y: 84 },
];
// Seeed XIAO ESP32-C3: 18.1 mm × 23.4 mm → 91 × 117 px (same form as XIAO ESP32-S3)
const PINS_XIAO_ESP32_C3 = PINS_XIAO_ESP32_S3;
// Aitewinrobot ESP32-C3 SuperMini: 18.0 mm × 24.576 mm → 90 × 123 px
// Left col: x = 1.46 mm → 7 px | Right col: x = 16.70 mm → 84 px
const PINS_AITEWIN_C3 = [
{ name: '5', x: 7, y: 16 },
{ name: '6', x: 7, y: 29 },
{ name: '7', x: 7, y: 42 },
{ name: '8', x: 7, y: 54 },
{ name: '9', x: 7, y: 67 },
{ name: '10', x: 7, y: 80 },
{ name: 'RX', x: 7, y: 92 },
{ name: 'TX', x: 7, y: 105 },
{ name: '5V', x: 84, y: 16 },
{ name: 'GND', x: 84, y: 29 },
{ name: '3V3', x: 84, y: 42 },
{ name: '4', x: 84, y: 54 },
{ name: '3', x: 84, y: 67 },
{ name: '2', x: 84, y: 80 },
{ name: '1', x: 84, y: 92 },
{ name: '0', x: 84, y: 105 },
];
// ─── ADC pin map: GPIO → { adc bank, channel within bank, qemu chn index } ──────
// chn is the index passed to qemu_picsimlab_set_apin():
// 0-7 → ADC1 channels 0-7 (GPIO 36,37,38,39,32,33,34,35)
// 8-17 → ADC2 channels 0-9 (GPIO 4,0,2,15,13,12,14,27,25,26)
export const ESP32_ADC_PIN_MAP: Record<number, { adc: 1 | 2; ch: number; chn: number }> = {
36: { adc: 1, ch: 0, chn: 0 },
37: { adc: 1, ch: 1, chn: 1 },
38: { adc: 1, ch: 2, chn: 2 },
39: { adc: 1, ch: 3, chn: 3 },
32: { adc: 1, ch: 4, chn: 4 },
33: { adc: 1, ch: 5, chn: 5 },
34: { adc: 1, ch: 6, chn: 6 },
35: { adc: 1, ch: 7, chn: 7 },
4: { adc: 2, ch: 0, chn: 8 },
0: { adc: 2, ch: 1, chn: 9 },
2: { adc: 2, ch: 2, chn: 10 },
15: { adc: 2, ch: 3, chn: 11 },
13: { adc: 2, ch: 4, chn: 12 },
12: { adc: 2, ch: 5, chn: 13 },
14: { adc: 2, ch: 6, chn: 14 },
27: { adc: 2, ch: 7, chn: 15 },
25: { adc: 2, ch: 8, chn: 16 },
26: { adc: 2, ch: 9, chn: 17 },
};
// ─── Board config by variant ──────────────────────────────────────────────────
interface BoardConfig {
svgUrl: string;
w: number;
h: number;
pins: { name: string; x: number; y: number }[];
}
const BOARD_CONFIGS: Record<string, BoardConfig> = {
esp32: { svgUrl: esp32SvgUrl, w: 141, h: 265, pins: PINS_ESP32 },
'esp32-s3': { svgUrl: esp32S3SvgUrl, w: 128, h: 350, pins: PINS_ESP32_S3 },
'esp32-c3': { svgUrl: esp32C3SvgUrl, w: 127, h: 215, pins: PINS_ESP32_C3 },
'esp32-devkit-c-v4': {
svgUrl: esp32DevkitCV4SvgUrl,
w: 140,
h: 283,
pins: PINS_ESP32_DEVKIT_C_V4,
},
'esp32-cam': { svgUrl: esp32CamSvgUrl, w: 136, h: 202, pins: PINS_ESP32_CAM },
'wemos-lolin32-lite': { svgUrl: wemosLolin32SvgUrl, w: 128, h: 250, pins: PINS_WEMOS_LOLIN32 },
'xiao-esp32-s3': { svgUrl: xiaoEsp32S3SvgUrl, w: 91, h: 117, pins: PINS_XIAO_ESP32_S3 },
'arduino-nano-esp32': {
svgUrl: arduinoNanoEsp32SvgUrl,
w: 217,
h: 90,
pins: PINS_ARDUINO_NANO_ESP32,
},
'xiao-esp32-c3': { svgUrl: xiaoEsp32C3SvgUrl, w: 91, h: 117, pins: PINS_XIAO_ESP32_C3 },
'aitewinrobot-esp32c3-supermini': {
svgUrl: aitewinC3SvgUrl,
w: 90,
h: 123,
pins: PINS_AITEWIN_C3,
},
};
// ─── Custom element ───────────────────────────────────────────────────────────
class Esp32Element extends HTMLElement {
static get observedAttributes() {
return ['board-kind'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
private get config(): BoardConfig {
const kind = this.getAttribute('board-kind') ?? 'esp32';
return BOARD_CONFIGS[kind] ?? BOARD_CONFIGS['esp32'];
}
get pinInfo() {
return this.config.pins;
}
private render() {
if (!this.shadowRoot) return;
const { svgUrl, w, h } = this.config;
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; line-height: 0; }
img { display: block; }
</style>
<img
src="${svgUrl}"
width="${w}"
height="${h}"
draggable="false"
alt="ESP32 board"
/>
`;
}
}
if (!customElements.get('velxio-esp32')) {
customElements.define('velxio-esp32', Esp32Element);
}