From 9caa3b6a773db68b3298e41d83f8dd2d1b1f845b Mon Sep 17 00:00:00 2001 From: Mateus Demboski Date: Thu, 2 Jul 2026 18:06:10 -0300 Subject: [PATCH 1/5] feat(web): device web UI (Vite + TypeScript) Built-in UI served gzipped from LittleFS. Firmware is the source of truth; the UI fetches its constants (/api/spec) + data (/api/db) at load. - main page: brand -> filament -> size -> color -> Write, plus Read pre-fill - config page: Device / Material database / OTA sub-tabs (WiFi/AP/serial, DB upload, firmware OTA) - Vite build emits minified, fixed-name bundles (index.js/config.js/style.css) Co-Authored-By: Claude Opus 4.8 --- web/config.html | 85 +++++ web/index.html | 49 +++ web/package.json | 15 + web/pnpm-lock.yaml | 664 ++++++++++++++++++++++++++++++++++++++++ web/pnpm-workspace.yaml | 4 + web/public/style.css | 82 +++++ web/src/app.ts | 153 +++++++++ web/src/config.ts | 131 ++++++++ web/tsconfig.json | 15 + web/vite.config.ts | 29 ++ 10 files changed, 1227 insertions(+) create mode 100644 web/config.html create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/pnpm-workspace.yaml create mode 100644 web/public/style.css create mode 100644 web/src/app.ts create mode 100644 web/src/config.ts create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/web/config.html b/web/config.html new file mode 100644 index 0000000..1fc7175 --- /dev/null +++ b/web/config.html @@ -0,0 +1,85 @@ + + + + + + SpoolID · Config + + + +
+

SpoolID · Config

+ Back +
+ +
+ + + +
+
+ WiFi (station) + + + +
+ +
+ Access point (fallback) + + +
+ +
+ Serial + + +
+ + +

+
+ + + + + + +
+ + + + diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..accef95 --- /dev/null +++ b/web/index.html @@ -0,0 +1,49 @@ + + + + + + SpoolID + + + +
+

SpoolID

+ Config +
+ +
+ + + + + + +
+ Color +
+ +
+ + #1200F6 +
+
+ + + +
+ +

+
+ + + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..bdd4bb4 --- /dev/null +++ b/web/package.json @@ -0,0 +1,15 @@ +{ + "name": "spoolid-web", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "~5.6.0", + "vite": "^6.0.0" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..f77032a --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,664 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + typescript: + specifier: ~5.6.0 + version: 5.6.3 + vite: + specifier: ^6.0.0 + version: 6.4.3 + +packages: + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@types/estree@1.0.9': {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + nanoid@3.3.15: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + typescript@5.6.3: {} + + vite@6.4.3: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.16 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 0000000..ae578a1 --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +# Allow esbuild (Vite's bundler) to run its install script. pnpm blocks build +# scripts by default; without this, `pnpm install` errors ERR_PNPM_IGNORED_BUILDS. +allowBuilds: + esbuild: true diff --git a/web/public/style.css b/web/public/style.css new file mode 100644 index 0000000..a080768 --- /dev/null +++ b/web/public/style.css @@ -0,0 +1,82 @@ +:root { + --bg: #14161a; + --card: #1e2127; + --fg: #e7e9ee; + --muted: #9aa0aa; + --accent: #3894e1; + --ok: #52d048; + --err: #ee301a; +} +* { box-sizing: border-box; } + +/* [hidden] must win over the element display rules below (e.g. .pane grid). */ +[hidden] { display: none !important; } + +/* Config sub-tabs */ +.subtabs { display: flex; gap: 8px; margin-bottom: 16px; } +.subtab { + background: var(--card); + color: var(--muted); + border: 1px solid #2a2e36; + border-radius: 8px; + padding: 8px 12px; + cursor: pointer; + font: inherit; + font-size: 14px; +} +.subtab:hover { color: var(--fg); } +.subtab.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.pane { display: grid; gap: 16px; } +body { + margin: 0; + font: 16px/1.5 system-ui, sans-serif; + background: var(--bg); + color: var(--fg); +} +header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + background: var(--card); + border-bottom: 1px solid #2a2e36; +} +header h1 { font-size: 20px; margin: 0; } +.nav { color: var(--accent); text-decoration: none; font-size: 14px; } +main { max-width: 460px; margin: 0 auto; padding: 18px; display: grid; gap: 16px; } +label { display: grid; gap: 6px; font-size: 14px; color: var(--muted); } +select, input, button { + font: inherit; + color: var(--fg); + background: var(--card); + border: 1px solid #2a2e36; + border-radius: 8px; + padding: 10px; +} +fieldset { border: 1px solid #2a2e36; border-radius: 10px; } +legend { color: var(--muted); font-size: 14px; padding: 0 6px; } +.swatches { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; } +.swatch { + aspect-ratio: 1; + border-radius: 8px; + border: 2px solid transparent; + cursor: pointer; +} +.swatch.sel { border-color: var(--fg); outline: 2px solid var(--accent); } +.custom { flex-direction: row; align-items: center; margin-top: 12px; } +.preview { display: flex; align-items: center; gap: 10px; margin-top: 10px; } +.chip { width: 28px; height: 28px; border-radius: 6px; border: 1px solid #2a2e36; display: inline-block; } +button { + background: var(--accent); + color: #fff; + border: none; + font-weight: 600; + cursor: pointer; + padding: 13px; +} +button:disabled { opacity: .5; cursor: default; } +.readout { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--muted); min-height: 1.4em; margin: 4px 0 12px; } +.status { min-height: 1.4em; font-size: 14px; } +.status.ok { color: var(--ok); } +.status.err { color: var(--err); } +.row { display: grid; gap: 6px; } diff --git a/web/src/app.ts b/web/src/app.ts new file mode 100644 index 0000000..ad9e10f --- /dev/null +++ b/web/src/app.ts @@ -0,0 +1,153 @@ +// SpoolID device web UI — main page (brand -> filament -> size -> color -> Write). +// Firmware is the source of truth: constants come from /api/spec, data from /api/db. +// (style.css is served as a static asset via , not imported.) + +// Helpers are inlined (not a shared module) so each page bundles to a single +// self-contained file — no shared chunk for the device to route. +const $ = (id: string): T => + document.getElementById(id) as T; +const sel = (id: string) => $(id); +const inp = (id: string) => $(id); +const btn = (id: string) => $(id); + +function setStatus(msg: string, err = false): void { + const s = $("status"); + s.textContent = msg; + s.className = "status" + (err ? " err" : msg ? " ok" : ""); +} + +interface Material { + id: string; + brand: string; + name: string; + type: string; +} + +let items: Material[] = []; +let swatches: string[] = []; // hex strings with a leading '#' +let color = "#1200F6"; +let polling: number | null = null; + +function setColor(hex: string): void { + color = hex.toUpperCase(); + $("hex").textContent = color; + $("chip").style.background = color; + document.querySelectorAll(".swatch").forEach((s) => + s.classList.toggle("sel", s.dataset.hex === color)); +} + +function buildSwatches(): void { + const wrap = $("swatches"); + wrap.innerHTML = ""; + swatches.forEach((hex) => { + const d = document.createElement("div"); + d.className = "swatch"; + d.dataset.hex = hex; + d.style.background = hex; + d.title = hex; + d.onclick = () => { setColor(hex); inp("custom").value = hex; }; + wrap.appendChild(d); + }); + inp("custom").oninput = (e) => setColor((e.target as HTMLInputElement).value); +} + +async function loadSpec(): Promise { + try { + const spec = await (await fetch("/api/spec")).json(); + swatches = (spec.colorSwatches || []).map((h: string) => "#" + h); + sel("size").innerHTML = (spec.weightLabels || []) + .map((w: string) => ``).join(""); + buildSwatches(); + if (swatches.length) setColor(swatches[0]); + } catch { + setStatus("failed to load device spec", true); + } +} + +function fillFilaments(): void { + const brand = sel("brand").value; + const list = items.filter((i) => i.brand === brand); + sel("filament").innerHTML = list + .map((i) => ``).join(""); +} + +function fillBrands(): void { + const brands = [...new Set(items.map((i) => i.brand))].sort(); + sel("brand").innerHTML = brands.map((b) => ``).join(""); + fillFilaments(); +} + +async function loadDb(): Promise { + try { + const doc = await (await fetch("/api/db")).json(); // gzip auto-decoded + items = (doc.result?.list || []).map((it: any) => ({ + id: it.base.id, brand: it.base.brand, name: it.base.name, type: it.base.meterialType, + })); + fillBrands(); + } catch { + setStatus("failed to load material DB", true); + } +} + +async function pollStatus(): Promise { + const st = await (await fetch("/api/status")).json(); + if (st.last && st.last.done) { + if (polling !== null) { clearInterval(polling); polling = null; } + btn("write").disabled = false; + if (st.last.ok) { + setStatus(`written ✓ UID ${st.last.uid}${st.last.encrypted ? " (re-encrypted)" : ""}`, false); + } else { + setStatus(`error: ${st.last.error}`, true); + } + } +} + +function applyRead(j: any): void { + const item = items.find((i) => i.id === j.materialId); + if (item) { sel("brand").value = item.brand; fillFilaments(); sel("filament").value = item.id; } + if (j.weight) sel("size").value = j.weight; + if (j.color) { setColor("#" + j.color); inp("custom").value = "#" + j.color; } + const name = item ? `${item.brand} ${item.name}` : `id ${j.materialId}`; + $("readout").textContent = + `read: ${name} · ${j.weight || "?"} · #${j.color}${j.encrypted ? " · locked" : ""}`; +} + +async function readTag(): Promise { + btn("read").disabled = true; + setStatus("reading… tap a tag on the reader", false); + try { + const j = await (await fetch("/api/read")).json(); + if (!j.ok) { setStatus(`read failed: ${j.error}`, true); return; } + applyRead(j); + setStatus("tag read ✓ — review values and Write", false); + } catch { + setStatus("read error", true); + } finally { + btn("read").disabled = false; + } +} + +async function write(): Promise { + btn("write").disabled = true; + setStatus("staging… tap a tag on the reader", false); + const body = { + materialId: sel("filament").value, + color: color.replace("#", ""), + weight: sel("size").value, + }; + const j = await (await fetch("/api/write", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + })).json(); + if (!j.ok) { btn("write").disabled = false; setStatus(`error: ${j.error}`, true); return; } + if (polling === null) polling = window.setInterval(pollStatus, 700); +} + +sel("brand").onchange = fillFilaments; +btn("write").onclick = write; +btn("read").onclick = readTag; +void loadSpec(); +void loadDb(); + +export {}; diff --git a/web/src/config.ts b/web/src/config.ts new file mode 100644 index 0000000..c4a27ab --- /dev/null +++ b/web/src/config.ts @@ -0,0 +1,131 @@ +// SpoolID device web UI — config page: Device / Material database / OTA sub-tabs. +// Firmware is the source of truth: the selects come from /api/spec. +// (style.css is served as a static asset via , not imported.) + +// Helpers are inlined (not a shared module) so each page bundles to a single +// self-contained file — no shared chunk for the device to route. +const $ = (id: string): T => + document.getElementById(id) as T; +const sel = (id: string) => $(id); +const inp = (id: string) => $(id); +const btn = (id: string) => $(id); + +function setStatus(node: HTMLElement, msg: string, err = false): void { + node.textContent = msg; + node.className = "status" + (err ? " err" : msg ? " ok" : ""); +} + +const statusEl = () => $("status"); +const dbStatusEl = () => $("dbStatus"); +const otaStatusEl = () => $("otaStatus"); + +// ---- sub-tabs ---- +function showPane(which: "device" | "db" | "ota"): void { + $("paneDevice").hidden = which !== "device"; + $("paneDb").hidden = which !== "db"; + $("paneOta").hidden = which !== "ota"; + btn("subDevice").classList.toggle("active", which === "device"); + btn("subDb").classList.toggle("active", which === "db"); + btn("subOta").classList.toggle("active", which === "ota"); +} +btn("subDevice").onclick = () => showPane("device"); +btn("subDb").onclick = () => showPane("db"); +btn("subOta").onclick = () => showPane("ota"); +showPane("device"); + +// ---- device settings ---- +async function loadSpec(): Promise { + const spec = await (await fetch("/api/spec")).json(); + sel("logLevel").innerHTML = (spec.logLevels || []) + .map((l: string, i: number) => ``).join(""); + sel("baud").innerHTML = (spec.baudRates || []) + .map((b: number) => ``).join(""); +} + +async function load(): Promise { + const c = await (await fetch("/api/config")).json(); + inp("wifiSsid").value = c.wifiSsid || ""; + inp("hostname").value = c.hostname || ""; + inp("apSsid").value = c.apSsid || ""; + sel("logLevel").value = String(c.logLevel ?? 3); + sel("baud").value = String(c.baud ?? 115200); +} + +btn("save").onclick = async () => { + const body: Record = { + apSsid: inp("apSsid").value, + wifiSsid: inp("wifiSsid").value, + hostname: inp("hostname").value, + logLevel: Number(sel("logLevel").value), + baud: Number(sel("baud").value), + }; + if (inp("wifiPass").value) body.wifiPass = inp("wifiPass").value; + if (inp("apPass").value) body.apPass = inp("apPass").value; + const j = await (await fetch("/api/config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + })).json(); + setStatus(statusEl(), j.ok ? "saved ✓ reboot to apply WiFi changes" : "save failed", !j.ok); +}; + +// ---- material database upload ---- +btn("upload").onclick = async () => { + const f = inp("dbfile").files?.[0]; + if (!f) return setStatus(dbStatusEl(), "pick a file first", true); + setStatus(dbStatusEl(), "gzipping…", false); + // Compress in the browser; the ESP32 has no runtime gzip compressor. + const gz = await new Response( + f.stream().pipeThrough(new CompressionStream("gzip")) + ).blob(); + const res = await fetch("/api/db", { + method: "POST", + headers: { "Content-Type": "application/octet-stream" }, + body: gz, + }); + setStatus(dbStatusEl(), (await res.json()).ok ? "DB uploaded ✓" : "upload failed", !res.ok); +}; + +// ---- firmware OTA (upload a .bin; the device self-flashes via Update) ---- +// Auto-pick the image type from the file name (littlefs.bin -> fs, else app). +inp("otaFile").onchange = () => { + const name = (inp("otaFile").files?.[0]?.name || "").toLowerCase(); + if (name) sel("otaTarget").value = /littlefs|spiffs|filesystem/.test(name) ? "fs" : "app"; +}; + +// XHR (not fetch) for upload progress. MD5 is optional firmware-side, so the +// browser path skips it (SubtleCrypto has no MD5). +btn("flash").onclick = () => { + const f = inp("otaFile").files?.[0]; + if (!f) return setStatus(otaStatusEl(), "pick a .bin file first", true); + const target = sel("otaTarget").value; + const prog = $("otaProg"); + prog.hidden = false; + prog.value = 0; + btn("flash").disabled = true; + setStatus(otaStatusEl(), "uploading + flashing — do not power off the device…", false); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", `/api/ota?target=${target}`); + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) prog.value = (e.loaded / e.total) * 100; + }; + xhr.onload = () => { + btn("flash").disabled = false; + prog.hidden = true; + let ok = false, error = ""; + try { const j = JSON.parse(xhr.responseText); ok = j.ok; error = j.error || ""; } catch { /* non-JSON */ } + if (ok) setStatus(otaStatusEl(), "flashed ✓ device rebooting — reconnect when it's back", false); + else setStatus(otaStatusEl(), `flash failed: ${error || "HTTP " + xhr.status}`, true); + }; + xhr.onerror = () => { + btn("flash").disabled = false; + prog.hidden = true; + setStatus(otaStatusEl(), "upload failed (connection lost?)", true); + }; + xhr.send(f); +}; + +void loadSpec().then(load); + +export {}; diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..8e0b44e --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..e4bf5e0 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "vite"; + +// The device serves fixed gzipped filenames from LittleFS, so emit predictable, +// unhashed names: index page -> app.js, config page -> config.js, shared css -> +// style.css. The two entries are self-contained (no shared import) so there's no +// extra chunk for the device to route. +export default defineConfig({ + base: "/", + build: { + outDir: "dist", + emptyOutDir: true, + minify: "esbuild", + cssCodeSplit: false, + // No benefit on a local device app; avoids an extra polyfill chunk the + // firmware would otherwise have to serve. + modulePreload: false, + rollupOptions: { + input: { + index: "index.html", + config: "config.html", + }, + output: { + entryFileNames: "[name].js", + chunkFileNames: "[name].js", + assetFileNames: "style.css", + }, + }, + }, +}); From 00f4ae1d984152a86b74f96e0f3d8b4245a53ebe Mon Sep 17 00:00:00 2001 From: Mateus Demboski Date: Thu, 2 Jul 2026 18:06:10 -0300 Subject: [PATCH 2/5] =?UTF-8?q?feat(desktop):=20SpoolID=20desktop=20app=20?= =?UTF-8?q?(Wails=20=E2=80=94=20Go=20+=20Vite/TS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-platform desktop client driving the device over USB serial. Go backend (serial-only) + a Vite/TypeScript frontend; the material DB is parsed in the frontend like the web client. - device auto-discovery (labels the ESP32-C3), gated connect flow + loading - Write/Read, and Config sub-tabs: Device / Material database / OTA - material DB: upload a file or pull from a printer (desktop fetches over HTTP, pushes to the device over serial) - firmware OTA relay with progress + release auto-discovery (GitHub), and a desktop/firmware version compatibility gate Co-Authored-By: Claude Opus 4.8 --- desktop/app.go | 402 ++++++++ desktop/build/appicon.png | Bin 0 -> 165099 bytes desktop/build/darwin/Info.plist | 63 ++ desktop/frontend/eslint.config.js | 20 + desktop/frontend/index.html | 150 +++ desktop/frontend/package.json | 20 + desktop/frontend/pnpm-lock.yaml | 1408 ++++++++++++++++++++++++++ desktop/frontend/pnpm-workspace.yaml | 4 + desktop/frontend/src/db.ts | 43 + desktop/frontend/src/main.ts | 635 ++++++++++++ desktop/frontend/src/serial.ts | 65 ++ desktop/frontend/src/theme.css | 194 ++++ desktop/frontend/tsconfig.json | 17 + desktop/frontend/vite.config.ts | 13 + desktop/go.mod | 40 + desktop/go.sum | 87 ++ desktop/main.go | 41 + desktop/wails.json | 18 + 18 files changed, 3220 insertions(+) create mode 100644 desktop/app.go create mode 100644 desktop/build/appicon.png create mode 100644 desktop/build/darwin/Info.plist create mode 100644 desktop/frontend/eslint.config.js create mode 100644 desktop/frontend/index.html create mode 100644 desktop/frontend/package.json create mode 100644 desktop/frontend/pnpm-lock.yaml create mode 100644 desktop/frontend/pnpm-workspace.yaml create mode 100644 desktop/frontend/src/db.ts create mode 100644 desktop/frontend/src/main.ts create mode 100644 desktop/frontend/src/serial.ts create mode 100644 desktop/frontend/src/theme.css create mode 100644 desktop/frontend/tsconfig.json create mode 100644 desktop/frontend/vite.config.ts create mode 100644 desktop/go.mod create mode 100644 desktop/go.sum create mode 100644 desktop/main.go create mode 100644 desktop/wails.json diff --git a/desktop/app.go b/desktop/app.go new file mode 100644 index 0000000..93e33c9 --- /dev/null +++ b/desktop/app.go @@ -0,0 +1,402 @@ +package main + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/wailsapp/wails/v2/pkg/runtime" + "go.bug.st/serial" + "go.bug.st/serial/enumerator" +) + +// espVID is the USB vendor ID of the XIAO ESP32-C3's built-in USB-Serial-JTAG. +const espVID = "303A" + +// ghRepo is the GitHub repo queried for release auto-discovery. +const ghRepo = "mateusdemboski/SpoolID" + +// App holds the serial connection and exposes bound methods to the frontend. +// Every method just moves a line-JSON command to the device and parses the +// single-line JSON reply — the firmware builds and encrypts everything. +type App struct { + ctx context.Context + mu sync.Mutex + port serial.Port +} + +// NewApp creates the application struct bound to the frontend. +func NewApp() *App { return &App{} } + +// Version returns the desktop app's semantic version (set via -ldflags at +// release build). The frontend gates compatibility on the major.minor pair +// against the firmware version reported by getspec. +func (a *App) Version() string { return version } + +// pushDB gzips a material_database.json and streams it to the device over serial +// (dbbegin -> dbdata -> dbend); the device stores it to LittleFS for its web UI. +func (a *App) pushDB(raw []byte) error { + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + if _, err := gw.Write(raw); err != nil { + return err + } + if err := gw.Close(); err != nil { + return err + } + data := buf.Bytes() + + a.mu.Lock() + defer a.mu.Unlock() + + begin, err := a.sendLocked(map[string]interface{}{"cmd": "dbbegin", "size": len(data)}, 10*time.Second) + if err != nil { + return fmt.Errorf("dbbegin: %w", err) + } + if ok, _ := begin["ok"].(bool); !ok { + return fmt.Errorf("dbbegin rejected: %v", begin["error"]) + } + + const chunk = 4096 // matches OTA_CHUNK in the firmware + for off := 0; off < len(data); off += chunk { + end := off + chunk + if end > len(data) { + end = len(data) + } + b64 := base64.StdEncoding.EncodeToString(data[off:end]) + r, err := a.sendLocked(map[string]interface{}{"cmd": "dbdata", "b": b64}, 10*time.Second) + if err != nil { + return fmt.Errorf("dbdata @%d: %w", off, err) + } + if ok, _ := r["ok"].(bool); !ok { + return fmt.Errorf("dbdata rejected: %v", r["error"]) + } + } + + fin, err := a.sendLocked(map[string]interface{}{"cmd": "dbend"}, 10*time.Second) + if err != nil { + return fmt.Errorf("dbend: %w", err) + } + if ok, _ := fin["ok"].(bool); !ok { + return fmt.Errorf("dbend rejected: %v", fin["error"]) + } + return nil +} + +// UploadDB opens a file picker for a material_database.json and pushes it to the +// device. Returns the uploaded file name, or "" if the picker was cancelled. +func (a *App) UploadDB() (string, error) { + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select material_database.json", + Filters: []runtime.FileFilter{{DisplayName: "JSON (*.json)", Pattern: "*.json"}}, + }) + if err != nil { + return "", fmt.Errorf("file dialog: %w", err) + } + if path == "" { + return "", nil // cancelled + } + raw, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("read: %w", err) + } + if err := a.pushDB(raw); err != nil { + return "", err + } + return filepath.Base(path), nil +} + +// PullDBFromPrinter fetches material_database.json from a Creality printer over +// the network (TLS/HTTP on the desktop) and pushes it to the device. host is +// "ip" or "ip:port". +func (a *App) PullDBFromPrinter(host string) error { + url := "http://" + host + "/downloads/defData/material_database.json" + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("printer: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("printer: HTTP %d", resp.StatusCode) + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("printer: %w", err) + } + if len(raw) == 0 { + return fmt.Errorf("printer returned an empty database") + } + return a.pushDB(raw) +} + +// Release describes the latest GitHub release for update auto-discovery. +type Release struct { + Version string `json:"version"` // tag without a leading "v" + Tag string `json:"tag"` // the raw tag (vX.Y.Z) + URL string `json:"url"` // release page (html_url) + Firmware string `json:"firmware"` // firmware.bin download URL ("" if absent) + Filesystem string `json:"filesystem"` // littlefs.bin download URL ("" if absent) +} + +// CheckUpdate queries the GitHub Releases API for the latest release and returns +// its version + the firmware/filesystem asset URLs, so the UI can offer a +// one-click update instead of a pasted URL. +func (a *App) CheckUpdate() (*Release, error) { + req, _ := http.NewRequest(http.MethodGet, + "https://api.github.com/repos/"+ghRepo+"/releases/latest", nil) + req.Header.Set("Accept", "application/vnd.github+json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("github: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("no releases published yet") + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("github: HTTP %d", resp.StatusCode) + } + + var gh struct { + TagName string `json:"tag_name"` + HTMLURL string `json:"html_url"` + Assets []struct { + Name string `json:"name"` + URL string `json:"browser_download_url"` + } `json:"assets"` + } + if err := json.NewDecoder(resp.Body).Decode(&gh); err != nil { + return nil, fmt.Errorf("github: %w", err) + } + + rel := &Release{Tag: gh.TagName, Version: strings.TrimPrefix(gh.TagName, "v"), URL: gh.HTMLURL} + for _, as := range gh.Assets { + switch as.Name { + case "firmware.bin": + rel.Firmware = as.URL + case "littlefs.bin": + rel.Filesystem = as.URL + } + } + return rel, nil +} + +func (a *App) startup(ctx context.Context) { a.ctx = ctx } + +func (a *App) shutdown(_ context.Context) { + a.mu.Lock() + defer a.mu.Unlock() + a.closeLocked() +} + +func (a *App) closeLocked() { + if a.port != nil { + _ = a.port.Close() + a.port = nil + } +} + +// PortInfo describes a serial port for the picker. IsDevice flags a likely +// SpoolID writer (the XIAO ESP32-C3 USB-Serial-JTAG), so the UI can label it and +// auto-connect when it's the only one. +type PortInfo struct { + Name string `json:"name"` + Label string `json:"label"` + IsDevice bool `json:"isDevice"` +} + +// ListPorts returns the available serial ports with friendly labels, flagging +// the ones that look like a SpoolID device (by USB VID). +func (a *App) ListPorts() ([]PortInfo, error) { + ports, err := enumerator.GetDetailedPortsList() + if err != nil { + return nil, fmt.Errorf("enumerate ports: %w", err) + } + out := make([]PortInfo, 0, len(ports)) + for _, p := range ports { + info := PortInfo{Name: p.Name, Label: p.Name} + if p.IsUSB { + if strings.EqualFold(p.VID, espVID) { + info.IsDevice = true + info.Label = "SpoolID device — " + p.Name + } else if p.Product != "" { + info.Label = p.Product + " — " + p.Name + } + } + out = append(out, info) + } + return out, nil +} + +// Connect opens a serial port. DTR/RTS are deasserted so the ESP32-C3 +// USB-Serial-JTAG isn't held in reset. +func (a *App) Connect(port string, baud int) error { + a.mu.Lock() + defer a.mu.Unlock() + a.closeLocked() + + p, err := serial.Open(port, &serial.Mode{BaudRate: baud}) + if err != nil { + return fmt.Errorf("open %s: %w", port, err) + } + if err := p.SetReadTimeout(300 * time.Millisecond); err != nil { + _ = p.Close() + return fmt.Errorf("set timeout: %w", err) + } + _ = p.SetDTR(false) + _ = p.SetRTS(false) + time.Sleep(300 * time.Millisecond) + _ = p.ResetInputBuffer() + a.port = p + return nil +} + +// Disconnect closes the current serial port, if any. +func (a *App) Disconnect() error { + a.mu.Lock() + defer a.mu.Unlock() + a.closeLocked() + return nil +} + +// Send writes one JSON command line and returns the device's JSON reply. +func (a *App) Send(cmd map[string]interface{}) (map[string]interface{}, error) { + a.mu.Lock() + defer a.mu.Unlock() + return a.sendLocked(cmd, 8*time.Second) +} + +// sendLocked is the request/response core; the caller must hold a.mu. Firmware +// log lines start with '['; only a line starting with '{' is a reply. +func (a *App) sendLocked(cmd map[string]interface{}, timeout time.Duration) (map[string]interface{}, error) { + if a.port == nil { + return nil, fmt.Errorf("not connected") + } + + line, err := json.Marshal(cmd) + if err != nil { + return nil, err + } + _ = a.port.ResetInputBuffer() + if _, err := a.port.Write(append(line, '\n')); err != nil { + return nil, fmt.Errorf("write: %w", err) + } + + deadline := time.Now().Add(timeout) + buf := make([]byte, 512) + var acc []byte + for time.Now().Before(deadline) { + n, err := a.port.Read(buf) + if err != nil { + return nil, fmt.Errorf("read: %w", err) + } + if n == 0 { + continue // read timeout; keep waiting until the deadline + } + acc = append(acc, buf[:n]...) + for { + i := bytes.IndexByte(acc, '\n') + if i < 0 { + break + } + raw := bytes.TrimSpace(acc[:i]) + acc = acc[i+1:] + if len(raw) == 0 || raw[0] != '{' { + continue // log line or noise + } + var reply map[string]interface{} + if err := json.Unmarshal(raw, &reply); err != nil { + continue + } + return reply, nil + } + } + return nil, fmt.Errorf("no JSON response from device") +} + +// FlashFirmware downloads an image (TLS handled here on the desktop) and relays +// it to the device over serial: otabegin -> otadata(base64) * N -> otaend. The +// device self-flashes via ESP Update (MD5-verified, A/B rollback). Progress is +// emitted as the "ota:progress" event (0..1). `filesystem` targets the LittleFS +// image instead of the app. The device reboots on success, so the port is +// dropped for the UI to reconnect. +func (a *App) FlashFirmware(url string, filesystem bool) error { + runtime.EventsEmit(a.ctx, "ota:progress", 0.0) + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("download: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download: HTTP %d", resp.StatusCode) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("download: %w", err) + } + if len(data) == 0 { + return fmt.Errorf("empty image") + } + sum := md5.Sum(data) + md5hex := hex.EncodeToString(sum[:]) + + target := "app" + if filesystem { + target = "fs" + } + + a.mu.Lock() + defer a.mu.Unlock() + + begin, err := a.sendLocked(map[string]interface{}{ + "cmd": "otabegin", "size": len(data), "target": target, "md5": md5hex, + }, 10*time.Second) + if err != nil { + return fmt.Errorf("otabegin: %w", err) + } + if ok, _ := begin["ok"].(bool); !ok { + return fmt.Errorf("otabegin rejected: %v", begin["error"]) + } + + const chunk = 4096 // matches OTA_CHUNK in the firmware + for off := 0; off < len(data); off += chunk { + end := off + chunk + if end > len(data) { + end = len(data) + } + b64 := base64.StdEncoding.EncodeToString(data[off:end]) + r, err := a.sendLocked(map[string]interface{}{"cmd": "otadata", "b": b64}, 10*time.Second) + if err != nil { + return fmt.Errorf("otadata @%d: %w", off, err) + } + if ok, _ := r["ok"].(bool); !ok { + return fmt.Errorf("otadata rejected: %v", r["error"]) + } + runtime.EventsEmit(a.ctx, "ota:progress", float64(end)/float64(len(data))) + } + + fin, err := a.sendLocked(map[string]interface{}{"cmd": "otaend"}, 20*time.Second) + if err != nil { + return fmt.Errorf("otaend: %w", err) + } + if ok, _ := fin["ok"].(bool); !ok { + return fmt.Errorf("otaend rejected: %v", fin["error"]) + } + + a.closeLocked() // device reboots + re-enumerates USB; UI reconnects + runtime.EventsEmit(a.ctx, "ota:progress", 1.0) + return nil +} diff --git a/desktop/build/appicon.png b/desktop/build/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..99b303b9b1438ef61543bd46389f47e22972bf94 GIT binary patch literal 165099 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

(n_$i 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%0Db5O$9PQ_Q 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%0D;P4+^b*5u<1CGMOaUm+2w?BW__La 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 z�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 literal 0 HcmV?d00001 diff --git a/desktop/build/darwin/Info.plist b/desktop/build/darwin/Info.plist new file mode 100644 index 0000000..d17a747 --- /dev/null +++ b/desktop/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/desktop/frontend/eslint.config.js b/desktop/frontend/eslint.config.js new file mode 100644 index 0000000..cfd115a --- /dev/null +++ b/desktop/frontend/eslint.config.js @@ -0,0 +1,20 @@ +// @ts-check +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +// Frontend lint (ESLint + typescript-eslint, flat config). Generated code +// (wailsjs bindings), build output, and the copied DB are ignored. +export default tseslint.config( + { + ignores: ["dist", "wailsjs", "node_modules", "material_database.json"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + // The serial/HTTP layer deals in dynamic JSON, so `any` at those + // boundaries (Reply, invoke wrappers) is intentional. + "@typescript-eslint/no-explicit-any": "off", + }, + }, +); diff --git a/desktop/frontend/index.html b/desktop/frontend/index.html new file mode 100644 index 0000000..bbfc743 --- /dev/null +++ b/desktop/frontend/index.html @@ -0,0 +1,150 @@ + + + + + + SpoolID + + +

+

SpoolID

+ Creality RFID spool writer + +
+ + +
+
+

Connect a device

+

Scanning for SpoolID devices…

+
+ + +
+ + +
+
+
+ + + + + + +
+ +
+ + + + +
+ Color +
+
+ + #000000 + +
+
+ +
+ + +
+
+

+
+ + + +
+ + + + diff --git a/desktop/frontend/package.json b/desktop/frontend/package.json new file mode 100644 index 0000000..f1159a8 --- /dev/null +++ b/desktop/frontend/package.json @@ -0,0 +1,20 @@ +{ + "name": "spoolid-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "copy-db": "node -e \"require('fs').copyFileSync('../../material_database.json','material_database.json')\"", + "dev": "pnpm copy-db && vite", + "build": "pnpm copy-db && tsc && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.6.0", + "typescript": "~5.6.0", + "typescript-eslint": "^8.62.1", + "vite": "^6.0.0" + } +} diff --git a/desktop/frontend/pnpm-lock.yaml b/desktop/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..e16e009 --- /dev/null +++ b/desktop/frontend/pnpm-lock.yaml @@ -0,0 +1,1408 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.6.0) + eslint: + specifier: ^10.6.0 + version: 10.6.0 + typescript: + specifier: ~5.6.0 + version: 5.6.3 + typescript-eslint: + specifier: ^8.62.1 + version: 8.62.1(eslint@10.6.0)(typescript@5.6.3) + vite: + specifier: ^6.0.0 + version: 6.4.3 + +packages: + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.62.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.62.1: + resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + dependencies: + eslint: 10.6.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.6.0)': + optionalDependencies: + eslint: 10.6.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0)(typescript@5.6.3))(eslint@10.6.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.62.1 + eslint: 10.6.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.62.1(eslint@10.6.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3 + eslint: 10.6.0 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.62.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.6.3) + '@typescript-eslint/types': 8.62.1 + debug: 4.4.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.6.3)': + dependencies: + typescript: 5.6.3 + + '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0)(typescript@5.6.3) + debug: 4.4.3 + eslint: 10.6.0 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.62.1': {} + + '@typescript-eslint/typescript-estree@8.62.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/project-service': 8.62.1(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.6.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.62.1(eslint@10.6.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) + eslint: 10.6.0 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + eslint-visitor-keys: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + balanced-match@4.0.4: {} + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escape-string-regexp@4.0.0: {} + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.6.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.62.1(eslint@10.6.0)(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0)(typescript@5.6.3))(eslint@10.6.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0)(typescript@5.6.3) + eslint: 10.6.0 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + typescript@5.6.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@6.4.3: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.16 + rollup: 4.62.2 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/desktop/frontend/pnpm-workspace.yaml b/desktop/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..ae578a1 --- /dev/null +++ b/desktop/frontend/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +# Allow esbuild (Vite's bundler) to run its install script. pnpm blocks build +# scripts by default; without this, `pnpm install` errors ERR_PNPM_IGNORED_BUILDS. +allowBuilds: + esbuild: true diff --git a/desktop/frontend/src/db.ts b/desktop/frontend/src/db.ts new file mode 100644 index 0000000..756de36 --- /dev/null +++ b/desktop/frontend/src/db.ts @@ -0,0 +1,43 @@ +// Material database: parsed in the frontend from the bundled Creality JSON +// (mirrors the web client, which fetches /api/db and parses in the browser). +// The JSON is copied from the repo root into frontend/ by the `copy-db` script. +import rawDb from "../material_database.json"; + +export interface Material { + id: string; + brand: string; + name: string; + type: string; +} + +interface DbDoc { + result?: { + list?: Array<{ + base?: { id?: string; brand?: string; name?: string; meterialType?: string }; + }>; + }; +} + +// loadDb flattens result.list[].base into Material rows. Note the DB's +// misspelled type key, "meterialType". +export function loadDb(): Material[] { + const doc = rawDb as DbDoc; + const out: Material[] = []; + for (const it of doc.result?.list ?? []) { + const b = it.base; + if (!b?.id) continue; + out.push({ id: b.id, brand: b.brand ?? "", name: b.name ?? "", type: b.meterialType ?? "" }); + } + return out; +} + +export const brands = (items: Material[]): string[] => + [...new Set(items.map((m) => m.brand))].sort(); + +export const byBrand = (items: Material[], brand: string): Material[] => + items.filter((m) => m.brand === brand); + +export const nameFor = (items: Material[], id: string): string => { + const m = items.find((x) => x.id === id); + return m ? `${m.brand} ${m.name}` : ""; +}; diff --git a/desktop/frontend/src/main.ts b/desktop/frontend/src/main.ts new file mode 100644 index 0000000..60d5b4f --- /dev/null +++ b/desktop/frontend/src/main.ts @@ -0,0 +1,635 @@ +// SpoolID desktop UI. Firmware is the source of truth: on connect we fetch the +// UI lists (swatches, sizes, log levels, baud rates) and current config over +// serial, then drive the same brand -> filament -> size -> color -> Write flow +// as the web UI. Serial calls are bound Go methods (see serial.ts). +import "./theme.css"; +import { + appVersion, + checkUpdate, + connect, + disconnect, + flashFirmware, + listPorts, + onOtaProgress, + pullDb, + send, + uploadDb, +} from "./serial"; +import type { PortInfo, Release, Reply } from "./serial"; +import { brands, byBrand, loadDb, nameFor } from "./db"; +import type { Material } from "./db"; + +const BAUD = 115200; + +// Friendly names for the Creality swatch palette (presentation only; the hex +// list comes from the device spec). Unknown hex shows as "Custom". +const COLOR_NAMES: Record = { + "1200F6": "Blue", + "3894E1": "Light Blue", + "FEFF01": "Yellow", + "F8D531": "Gold", + "F38E24": "Orange", + "52D048": "Green", + "00FEBE": "Teal", + "B700F3": "Purple", + "EE301A": "Red", + "FA5959": "Coral", + "FFFFFF": "White", + "D8D8D8": "Light Gray", + "4C4C4C": "Dark Gray", + "782543": "Maroon", + "000000": "Black", +}; + +const $ = (id: string): T => + document.getElementById(id) as T; + +// ---- state ---- +let items: Material[] = []; +let swatches: string[] = []; // hex strings, no leading '#' +let color = "000000"; +let polling: number | null = null; +let appVer = ""; // desktop version, for the compat gate +let latest: Release | null = null; // latest GitHub release (auto-discovery) + +// ---- element refs ---- +const el = { + port: $("port"), + rescan: $("rescan"), + connect: $("connect"), + disconnect: $("disconnect"), + connStatus: $("connStatus"), + gateHint: $("gateHint"), + spinner: $("spinner"), + navWrite: $("navWrite"), + navConfig: $("navConfig"), + writeView: $("writeView"), + configView: $("configView"), + brand: $("brand"), + filament: $("filament"), + size: $("size"), + swatches: $("swatches"), + chip: $("chip"), + hex: $("hex"), + custom: $("custom"), + write: $("write"), + read: $("read"), + readout: $("readout"), + writeStatus: $("writeStatus"), + wifiSsid: $("wifiSsid"), + wifiPass: $("wifiPass"), + hostname: $("hostname"), + apSsid: $("apSsid"), + apPass: $("apPass"), + logLevel: $("logLevel"), + baud: $("baud"), + save: $("save"), + configStatus: $("configStatus"), + subDevice: $("subDevice"), + subDb: $("subDb"), + subOta: $("subOta"), + paneDevice: $("paneDevice"), + paneDb: $("paneDb"), + paneOta: $("paneOta"), + uploadDb: $("uploadDb"), + dbHost: $("dbHost"), + dbPull: $("dbPull"), + dbStatus: $("dbStatus"), + compat: $("compat"), + otaUrl: $("otaUrl"), + otaTarget: $("otaTarget"), + flash: $("flash"), + otaBar: $("otaBar"), + otaStatus: $("otaStatus"), + checkUpdate: $("checkUpdate"), + updateInfo: $("updateInfo"), + updateActions: $("updateActions"), + flashLatestApp: $("flashLatestApp"), + flashLatestFs: $("flashLatestFs"), +}; + +// Controls that require an open connection. +const gated = (): (HTMLInputElement | HTMLSelectElement | HTMLButtonElement)[] => [ + el.brand, el.filament, el.size, el.logLevel, el.baud, el.custom, + el.write, el.read, el.save, el.uploadDb, el.dbHost, el.dbPull, + el.wifiSsid, el.wifiPass, el.hostname, el.apSsid, el.apPass, + el.otaUrl, el.otaTarget, el.flash, el.flashLatestApp, el.flashLatestFs, +]; + +// ---- helpers ---- +function setConn(msg: string, err: boolean): void { + el.connStatus.textContent = msg; + el.connStatus.className = "conn " + (err ? "err" : "ok"); +} + +function setStatus(node: HTMLElement, msg: string, kind: "" | "ok" | "err" = ""): void { + node.textContent = msg; + node.className = "status" + (kind ? " " + kind : ""); +} + +function options(node: HTMLSelectElement, values: string[]): void { + node.innerHTML = values.map((v) => ``).join(""); +} + +function setEnabled(on: boolean): void { + for (const node of gated()) node.disabled = !on; +} + +// ---- view switch ---- +function show(which: "write" | "config"): void { + el.writeView.hidden = which !== "write"; + el.configView.hidden = which !== "config"; + el.navWrite.classList.toggle("active", which === "write"); + el.navConfig.classList.toggle("active", which === "config"); +} + +// ---- color ---- +function colorLabel(hex: string): string { + return `${COLOR_NAMES[hex.toUpperCase()] ?? "Custom"} #${hex}`; +} + +function setColor(hex: string): void { + color = hex.toUpperCase(); + el.chip.style.background = "#" + color; + el.hex.textContent = "#" + color; + document.querySelectorAll(".swatch").forEach((s) => + s.classList.toggle("sel", !s.dataset.custom && s.dataset.hex === color)); +} + +function buildSwatches(): void { + el.swatches.innerHTML = ""; + swatches.forEach((hex) => { + const d = document.createElement("div"); + d.className = "swatch"; + d.dataset.hex = hex; + d.style.background = "#" + hex; + d.title = colorLabel(hex); + d.onclick = () => selectSwatch(hex); + el.swatches.appendChild(d); + }); + // Trailing "custom" tile reveals the hex input. + const c = document.createElement("div"); + c.className = "swatch"; + c.dataset.custom = "1"; + c.title = "Custom hex"; + c.textContent = "+"; + c.style.display = "grid"; + c.style.placeItems = "center"; + c.style.border = "2px dashed var(--border)"; + c.style.color = "var(--muted)"; + c.onclick = () => selectCustom(); + el.swatches.appendChild(c); +} + +function selectSwatch(hex: string): void { + el.custom.hidden = true; + setColor(hex); +} + +function selectCustom(): void { + el.custom.hidden = false; + document.querySelectorAll(".swatch").forEach((s) => s.classList.remove("sel")); + el.custom.focus(); + applyCustom(); +} + +function applyCustom(): void { + const hex = el.custom.value.trim().replace(/^#/, ""); + if (/^[0-9a-fA-F]{6}$/.test(hex)) setColor(hex); +} + +// ---- device spec + config ---- +function applySpec(spec: Reply): void { + swatches = (spec.colorSwatches ?? []) as string[]; + options(el.size, (spec.weightLabels ?? []) as string[]); + options(el.logLevel, (spec.logLevels ?? []) as string[]); + options(el.baud, ((spec.baudRates ?? []) as number[]).map(String)); + buildSwatches(); + if (swatches.length) selectSwatch(swatches[0]); + checkCompat(String(spec.version ?? "")); +} + +// ---- version compatibility gate ---- +// Desktop and firmware must share the same major.minor (full match preferred). +// Dev builds (0.0.0-dev / missing) skip the check. +function minorOf(v: string): string | null { + const m = /^(\d+)\.(\d+)\./.exec(v); + return m ? `${m[1]}.${m[2]}` : null; +} + +function checkCompat(fwVer: string): void { + const app = minorOf(appVer); + const fw = minorOf(fwVer); + if (!app || !fw) { + el.compat.hidden = true; // a dev build on either side — don't nag + return; + } + if (app === fw) { + el.compat.hidden = true; + return; + } + el.compat.hidden = false; + el.compat.textContent = + `⚠ version mismatch — desktop v${appVer} vs firmware v${fwVer}. ` + + `Update so both share the same minor (Config → Firmware update).`; +} + +function applyConfig(cfg: Reply): void { + el.wifiSsid.value = cfg.wifiSsid ?? ""; + el.hostname.value = cfg.hostname ?? ""; + el.apSsid.value = cfg.apSsid ?? ""; + if (cfg.logLevel != null) el.logLevel.selectedIndex = Number(cfg.logLevel); + if (cfg.baud != null) el.baud.value = String(cfg.baud); +} + +// ---- brand/filament ---- +function fillBrands(): void { + options(el.brand, brands(items)); + fillFilaments(); +} + +function fillFilaments(): void { + const list = byBrand(items, el.brand.value); + el.filament.innerHTML = list + .map((m) => ``) + .join(""); +} + +// ---- connection ---- +type State = "disconnected" | "connecting" | "connected"; +let pollTimer: number | null = null; +let autoConnect = true; // re-armed when no device is present (unplug -> replug) + +function setState(s: State): void { + document.body.dataset.state = s; + el.disconnect.hidden = s !== "connected"; + el.spinner.hidden = s !== "connecting"; + const busy = s === "connecting"; + el.connect.disabled = busy; + el.port.disabled = busy; + el.rescan.disabled = busy; +} + +// Refresh the port list, keeping the current pick (or preferring a device), and +// update the gate hint. Returns the discovered ports. +async function refreshPorts(): Promise { + let ports: PortInfo[]; + try { + ports = await listPorts(); + } catch { + return []; + } + const prev = el.port.value; + el.port.innerHTML = ports + .map((p) => ``).join(""); + const device = ports.find((p) => p.isDevice); + if (ports.some((p) => p.name === prev)) el.port.value = prev; + else if (device) el.port.value = device.name; + + el.gateHint.textContent = ports.length === 0 + ? "Scanning for a SpoolID device… plug one in over USB." + : device + ? (autoConnect ? "Found a SpoolID device — connecting…" : "SpoolID device found — click Connect.") + : "Select a serial port, then Connect."; + return ports; +} + +// While disconnected, poll for ports and auto-connect a lone SpoolID device. +// autoConnect re-arms once no device is present, so a manual Disconnect doesn't +// immediately reconnect, but unplugging + replugging does. +async function poll(): Promise { + if (document.body.dataset.state !== "disconnected") return; + const ports = await refreshPorts(); + const devices = ports.filter((p) => p.isDevice); + if (devices.length === 0) { + autoConnect = true; + return; + } + if (devices.length === 1 && autoConnect) { + el.port.value = devices[0].name; + await doConnect(); + } +} + +function startPolling(): void { + if (pollTimer === null) pollTimer = window.setInterval(() => void poll(), 2000); +} + +function stopPolling(): void { + if (pollTimer !== null) { clearInterval(pollTimer); pollTimer = null; } +} + +async function doConnect(): Promise { + const port = el.port.value; + if (!port) return setConn("pick a serial port", true); + stopPolling(); + setState("connecting"); + setConn("", false); + try { + await connect(port, BAUD); + } catch (e) { + setState("disconnected"); + setConn(`open failed: ${e}`, true); + startPolling(); + return; + } + try { + const spec = await send({ cmd: "getspec" }); + applySpec(spec); + const cfg = await send({ cmd: "getconfig" }); + applyConfig(cfg); + fillBrands(); + } catch (e) { + setState("disconnected"); + setConn(`serial error: ${e}`, true); + startPolling(); + return; + } + setEnabled(true); + setState("connected"); +} + +async function doDisconnect(): Promise { + try { + await disconnect(); + } catch { + /* already gone */ + } + autoConnect = false; // don't immediately reconnect the same device + setEnabled(false); + el.compat.hidden = true; + setState("disconnected"); + setConn("", false); + startPolling(); + void refreshPorts(); +} + +// ---- write / read ---- +async function doWrite(): Promise { + const materialId = el.filament.value; + const weight = el.size.value; + if (!materialId || !weight) { + return setStatus(el.writeStatus, "pick a filament and size first", "err"); + } + el.write.disabled = true; + setStatus(el.writeStatus, "staging — tap a tag on the reader…"); + try { + const resp = await send({ cmd: "write", materialId, color, weight }); + if (!resp.ok) { + setStatus(el.writeStatus, `error: ${resp.error}`, "err"); + el.write.disabled = false; + return; + } + if (polling == null) polling = window.setInterval(pollStatus, 700); + } catch (e) { + setStatus(el.writeStatus, `serial error: ${e}`, "err"); + el.write.disabled = false; + } +} + +async function pollStatus(): Promise { + try { + const st = await send({ cmd: "status" }); + const last = st.last; + if (!last || !last.done) return; + if (polling != null) { clearInterval(polling); polling = null; } + el.write.disabled = false; + if (last.ok) { + const enc = last.encrypted ? " (re-encrypted)" : ""; + setStatus(el.writeStatus, `written ✓ UID ${last.uid}${enc}`, "ok"); + } else { + setStatus(el.writeStatus, `error: ${last.error}`, "err"); + } + } catch (e) { + if (polling != null) { clearInterval(polling); polling = null; } + el.write.disabled = false; + setStatus(el.writeStatus, `serial error: ${e}`, "err"); + } +} + +async function doRead(): Promise { + el.read.disabled = true; + setStatus(el.writeStatus, "reading — tap a tag on the reader…"); + try { + const j = await send({ cmd: "read" }); + if (!j.ok) { + setStatus(el.writeStatus, `read failed: ${j.error}`, "err"); + return; + } + applyRead(j); + setStatus(el.writeStatus, "tag read ✓ — review values and Write", "ok"); + } catch (e) { + setStatus(el.writeStatus, `serial error: ${e}`, "err"); + } finally { + el.read.disabled = false; + } +} + +function applyRead(j: Reply): void { + const materialId = String(j.materialId ?? ""); + const m = items.find((x) => x.id === materialId); + if (m) { + el.brand.value = m.brand; + fillFilaments(); + el.filament.value = m.id; + } + if (j.weight) el.size.value = j.weight; + + const hex = String(j.color ?? "").toUpperCase(); + if (hex) { + if (swatches.some((s) => s.toUpperCase() === hex)) { + selectSwatch(hex); + } else { + selectCustom(); + el.custom.value = hex; + applyCustom(); + } + } + + const label = m ? `${m.brand} ${m.name}` : nameFor(items, materialId) || `id ${materialId}`; + const locked = j.encrypted ? " · locked" : ""; + el.readout.textContent = `read: ${label} · ${j.weight ?? "?"} · #${hex}${locked}`; +} + +// ---- config ---- +async function doSave(): Promise { + const cmd: Record = { + cmd: "setconfig", + wifiSsid: el.wifiSsid.value, + hostname: el.hostname.value, + apSsid: el.apSsid.value, + logLevel: el.logLevel.selectedIndex, + }; + if (el.baud.value) cmd.baud = Number(el.baud.value); + if (el.wifiPass.value) cmd.wifiPass = el.wifiPass.value; + if (el.apPass.value) cmd.apPass = el.apPass.value; + await simple(cmd, el.configStatus, "saved ✓ reboot device to apply WiFi", "save failed"); +} + +// ---- config sub-tabs (Device / Material database / OTA) ---- +function showConfigPane(which: "device" | "db" | "ota"): void { + el.paneDevice.hidden = which !== "device"; + el.paneDb.hidden = which !== "db"; + el.paneOta.hidden = which !== "ota"; + el.subDevice.classList.toggle("active", which === "device"); + el.subDb.classList.toggle("active", which === "db"); + el.subOta.classList.toggle("active", which === "ota"); +} + +// ---- material database: upload a file, or pull from a printer ---- +async function doUploadDb(): Promise { + el.uploadDb.disabled = true; + setStatus(el.dbStatus, "uploading to device…"); + try { + const name = await uploadDb(); + if (name) setStatus(el.dbStatus, `uploaded ${name} ✓`, "ok"); + else setStatus(el.dbStatus, ""); // cancelled + } catch (e) { + setStatus(el.dbStatus, `upload failed: ${e}`, "err"); + } finally { + el.uploadDb.disabled = false; + } +} + +async function doPullDb(): Promise { + const host = el.dbHost.value.trim(); + if (!host) return setStatus(el.dbStatus, "enter the printer host/IP", "err"); + el.dbPull.disabled = true; + setStatus(el.dbStatus, "fetching from printer + uploading…"); + try { + await pullDb(host); + setStatus(el.dbStatus, "pulled from printer ✓", "ok"); + } catch (e) { + setStatus(el.dbStatus, `pull failed: ${e}`, "err"); + } finally { + el.dbPull.disabled = false; + } +} + +async function simple(cmd: Record, node: HTMLElement, okMsg: string, failMsg: string): Promise { + try { + const resp = await send(cmd); + if (resp.ok) setStatus(node, okMsg, "ok"); + else setStatus(node, `${failMsg}: ${resp.error}`, "err"); + } catch (e) { + setStatus(node, `serial error: ${e}`, "err"); + } +} + +// ---- firmware OTA (desktop downloads an image + relays it to the device) ---- +let flashing = false; + +function setProgress(pct: number): void { + const wrap = el.otaBar.parentElement as HTMLElement; + wrap.hidden = pct <= 0 || pct >= 1; + el.otaBar.style.width = `${Math.round(pct * 100)}%`; +} + +async function runFlash(url: string, fs: boolean): Promise { + if (flashing) return; + if (!url) return setStatus(el.otaStatus, "no image URL", "err"); + flashing = true; + el.flash.disabled = true; + setProgress(0.001); + setStatus(el.otaStatus, "downloading + flashing — do not unplug the device…"); + try { + await flashFirmware(url, fs); + setStatus(el.otaStatus, "flashed ✓ device rebooting — reconnect when it's back", "ok"); + setConn("device rebooting after update", true); + setEnabled(false); + } catch (e) { + setStatus(el.otaStatus, `flash failed: ${e}`, "err"); + } finally { + flashing = false; + el.flash.disabled = false; + setProgress(0); + } +} + +function doFlash(): void { + void runFlash(el.otaUrl.value.trim(), el.otaTarget.value === "fs"); +} + +// ---- update auto-discovery (GitHub Releases) ---- +async function doCheckUpdate(): Promise { + el.checkUpdate.disabled = true; + setStatus(el.updateInfo, "checking GitHub for the latest release…"); + try { + latest = await checkUpdate(); + const newer = appVer && latest.version && cmpVer(latest.version, appVer) > 0; + const tag = latest.tag || `v${latest.version}`; + setStatus( + el.updateInfo, + newer ? `${tag} available (you have v${appVer})` : `up to date (latest ${tag})`, + newer ? "ok" : "", + ); + el.updateActions.hidden = false; + el.flashLatestApp.disabled = !latest.firmware; + el.flashLatestFs.disabled = !latest.filesystem; + } catch (e) { + latest = null; + el.updateActions.hidden = true; + setStatus(el.updateInfo, `update check failed: ${e}`, "err"); + } finally { + el.checkUpdate.disabled = false; + } +} + +// Compare dotted numeric versions (prerelease suffixes ignored). >0 if a > b. +function cmpVer(a: string, b: string): number { + const pa = a.split(/[.\-+]/).map(Number); + const pb = b.split(/[.\-+]/).map(Number); + for (let i = 0; i < 3; i++) { + const d = (pa[i] || 0) - (pb[i] || 0); + if (d) return d; + } + return 0; +} + +// ---- boot ---- +async function boot(): Promise { + el.rescan.onclick = () => void refreshPorts(); + el.connect.onclick = () => void doConnect(); + el.disconnect.onclick = () => void doDisconnect(); + el.navWrite.onclick = () => show("write"); + el.navConfig.onclick = () => show("config"); + el.brand.onchange = fillFilaments; + el.custom.oninput = applyCustom; + el.write.onclick = () => void doWrite(); + el.read.onclick = () => void doRead(); + el.save.onclick = () => void doSave(); + el.subDevice.onclick = () => showConfigPane("device"); + el.subDb.onclick = () => showConfigPane("db"); + el.subOta.onclick = () => showConfigPane("ota"); + el.uploadDb.onclick = () => void doUploadDb(); + el.dbPull.onclick = () => void doPullDb(); + el.flash.onclick = () => doFlash(); + // Auto-pick the image type from the URL (littlefs.bin -> fs, else app). + el.otaUrl.oninput = () => { + const u = el.otaUrl.value.toLowerCase(); + if (u) el.otaTarget.value = /littlefs|spiffs|filesystem/.test(u) ? "fs" : "app"; + }; + el.checkUpdate.onclick = () => void doCheckUpdate(); + el.flashLatestApp.onclick = () => void runFlash(latest?.firmware ?? "", false); + el.flashLatestFs.onclick = () => void runFlash(latest?.filesystem ?? "", true); + onOtaProgress((pct) => setProgress(pct)); + + setEnabled(false); + show("write"); + showConfigPane("device"); + setState("disconnected"); + + appVer = await appVersion().catch(() => ""); + + try { + items = loadDb(); + } catch (e) { + setConn(`failed to load material DB: ${e}`, true); + } + + // Discover ports, auto-connect a lone device, and keep scanning otherwise. + await poll(); + startPolling(); +} + +void boot(); diff --git a/desktop/frontend/src/serial.ts b/desktop/frontend/src/serial.ts new file mode 100644 index 0000000..2ed1af7 --- /dev/null +++ b/desktop/frontend/src/serial.ts @@ -0,0 +1,65 @@ +// Typed wrappers around the Wails-generated Go bindings. The generated files in +// ../wailsjs/ are produced by `wails dev` / `wails build` / `wails generate module`. +// The Go backend is serial-only; the material DB is parsed in the frontend (db.ts). +import { + ListPorts, + Connect, + Disconnect, + Send, + Version, + FlashFirmware, + CheckUpdate, + UploadDB, + PullDBFromPrinter, +} from "../wailsjs/go/main/App"; +import { EventsOn } from "../wailsjs/runtime/runtime"; + +export type Cmd = Record; +export type Reply = Record; + +export interface Release { + version: string; + tag: string; + url: string; + firmware: string; + filesystem: string; +} + +export interface PortInfo { + name: string; + label: string; + isDevice: boolean; +} + +export const listPorts = (): Promise => + ListPorts() as Promise; + +export const connect = (port: string, baud: number): Promise => + Connect(port, baud); + +export const disconnect = (): Promise => Disconnect(); + +export const send = (cmd: Cmd): Promise => + Send(cmd as { [key: string]: any }); + +// Desktop app version (for the firmware/desktop compatibility gate). +export const appVersion = (): Promise => Version(); + +// Download a firmware/filesystem image and relay it to the device over serial. +export const flashFirmware = (url: string, filesystem: boolean): Promise => + FlashFirmware(url, filesystem); + +// Subscribe to OTA progress (0..1) emitted during flashFirmware. +export const onOtaProgress = (cb: (pct: number) => void): void => { + EventsOn("ota:progress", cb); +}; + +// Query the GitHub Releases API for the latest release + asset URLs. +export const checkUpdate = (): Promise => CheckUpdate() as Promise; + +// Pick a material_database.json and upload it to the device (returns its name, +// or "" if cancelled). +export const uploadDb = (): Promise => UploadDB(); + +// Fetch the material DB from a printer host and push it to the device. +export const pullDb = (host: string): Promise => PullDBFromPrinter(host); diff --git a/desktop/frontend/src/theme.css b/desktop/frontend/src/theme.css new file mode 100644 index 0000000..90cfeaf --- /dev/null +++ b/desktop/frontend/src/theme.css @@ -0,0 +1,194 @@ +/* CrealityPrint-inspired dark theme (palette lifted from the old Tkinter gui.py). */ +:root { + --accent: #15c059; + --accent-act: #17cc5f; + --accent-text: #08130c; + --bg: #1b1b1b; + --surface: #2e2e2e; + --border: #3e3e45; + --text: #e8e8e8; + --muted: #bbbbbe; + --err: #e5484d; +} + +* { box-sizing: border-box; } + +/* The [hidden] attribute must win over element display rules below (e.g. the + `section { display: grid }` used for the two views and `.hexinput`). */ +[hidden] { display: none !important; } + +html, body { height: 100%; } + +body { + margin: 0; + font: 15px/1.5 system-ui, -apple-system, sans-serif; + background: var(--bg); + color: var(--text); + -webkit-user-select: none; + user-select: none; +} + +/* Header */ +header { + display: flex; + align-items: baseline; + gap: 10px; + padding: 16px 18px 6px; +} +header h1 { margin: 0; font-size: 24px; font-weight: 700; color: var(--accent); } +.subtitle { color: var(--muted); font-size: 13px; } + +/* Connection bar */ +.connbar { + display: flex; + gap: 8px; + padding: 8px 18px 0; +} +.connbar .grow { flex: 1; } +.conn { + padding: 4px 18px 0; + font-size: 13px; + min-height: 1.4em; +} +.conn.err { color: var(--err); } +.conn.ok { color: var(--accent-act); } + +/* Connect gate (shown until a device is connected) */ +#disconnect { margin-left: auto; } +.ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--border); + padding: 6px 12px; +} +.ghost:hover:not(:disabled) { background: var(--surface); color: var(--text); } +.gate { display: grid; place-items: center; padding: 40px 18px; } +.card { + width: 100%; + max-width: 420px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + padding: 22px; + display: grid; + gap: 12px; +} +.card h2 { margin: 0; font-size: 17px; font-weight: 600; } +.hint { margin: 0; color: var(--muted); font-size: 13px; } +.card .connbar { padding: 0; } +.card .conn { padding: 0; min-height: 0; } + +/* Loading spinner */ +.spinner { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; } +.ring { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid var(--border); + border-top-color: var(--accent); + animation: spin 0.7s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } +@media (prefers-reduced-motion: reduce) { .ring { animation: none; } } + +/* View gating: hide the app until connected. */ +#nav, #app { display: none; } +body[data-state="connected"] .gate { display: none; } +body[data-state="connected"] #nav { display: flex; } +body[data-state="connected"] #app { display: block; } + +/* Tabs */ +.tabs { + display: flex; + gap: 6px; + padding: 12px 18px 8px; + border-bottom: 1px solid var(--border); + margin-bottom: 8px; +} +.tab { + background: transparent; + color: var(--muted); + border: none; + padding: 6px 14px; + border-radius: 8px; + cursor: pointer; + font: inherit; +} +.tab:hover { background: var(--surface); } +.tab.active { background: var(--accent); color: var(--accent-text); font-weight: 600; } + +/* Config sub-tabs */ +.subtabs { display: flex; gap: 6px; margin-bottom: 14px; } +.subtab { + background: var(--surface); + color: var(--muted); + border: 1px solid var(--border); + padding: 6px 12px; + border-radius: 8px; + cursor: pointer; + font: inherit; + font-size: 13px; +} +.subtab:hover { color: var(--text); } +.subtab.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); font-weight: 600; } +.pane { display: grid; gap: 14px; } + +/* Layout */ +main { padding: 4px 18px 22px; display: block; } +section { display: grid; gap: 14px; } + +label { display: grid; gap: 6px; font-size: 13px; color: var(--muted); } + +select, input, button { + font: inherit; + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 9px 10px; +} +select:disabled, input:disabled, button:disabled { opacity: .45; cursor: default; } +input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: -1px; } + +fieldset { border: 1px solid var(--border); border-radius: 10px; display: grid; gap: 12px; } +legend { color: var(--muted); font-size: 13px; padding: 0 6px; } + +/* Buttons */ +button { cursor: pointer; font-weight: 500; } +button:hover:not(:disabled) { background: var(--border); } +button.accent { + background: var(--accent); + color: var(--accent-text); + border-color: var(--accent); + font-weight: 600; +} +button.accent:hover:not(:disabled) { background: var(--accent-act); border-color: var(--accent-act); } +button.icon { padding: 9px 12px; } +.btnrow { display: flex; gap: 10px; } +.btnrow .accent { flex: 1; } + +/* Color picker */ +.swatches { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; } +.swatch { + aspect-ratio: 1; + border-radius: 8px; + border: 2px solid transparent; + cursor: pointer; +} +.swatch.sel { border-color: var(--text); outline: 2px solid var(--accent); } +.preview { display: flex; align-items: center; gap: 10px; } +.chip { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border); display: inline-block; } +.hexinput { width: 8em; text-transform: uppercase; } + +/* Compatibility banner (firmware/desktop version mismatch) */ +.compat { padding: 4px 18px 0; font-size: 13px; color: #e5a03d; } + +/* OTA progress bar */ +.progress { height: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; } +.bar { height: 100%; width: 0; background: var(--accent); transition: width .12s linear; } + +/* Status text */ +.readout { font-size: 13px; color: var(--accent-act); min-height: 1.4em; } +.status { min-height: 1.4em; font-size: 13px; color: var(--muted); margin: 0; } +.status.err { color: var(--err); } +.status.ok { color: var(--accent-act); } diff --git a/desktop/frontend/tsconfig.json b/desktop/frontend/tsconfig.json new file mode 100644 index 0000000..e00b575 --- /dev/null +++ b/desktop/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/desktop/frontend/vite.config.ts b/desktop/frontend/vite.config.ts new file mode 100644 index 0000000..fc0aa12 --- /dev/null +++ b/desktop/frontend/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; + +// Wails serves the built frontend from the embedded frontend/dist over its own +// asset protocol, so keep asset paths relative and don't auto-open a browser. +export default defineConfig({ + base: "./", + clearScreen: false, + build: { + target: "es2021", + outDir: "dist", + emptyOutDir: true, + }, +}); diff --git a/desktop/go.mod b/desktop/go.mod new file mode 100644 index 0000000..e2cfc7f --- /dev/null +++ b/desktop/go.mod @@ -0,0 +1,40 @@ +module spoolid + +go 1.23 + +require ( + github.com/wailsapp/wails/v2 v2.12.0 + go.bug.st/serial v1.6.4 +) + +require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) diff --git a/desktop/go.sum b/desktop/go.sum new file mode 100644 index 0000000..4ceecd6 --- /dev/null +++ b/desktop/go.sum @@ -0,0 +1,87 @@ +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= +github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/desktop/main.go b/desktop/main.go new file mode 100644 index 0000000..1c175e6 --- /dev/null +++ b/desktop/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "embed" + "log" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +// version is the app's semantic version. Release CI overrides it via +// -ldflags "-X main.version=X.Y.Z"; local builds keep the dev fallback. +var version = "0.0.0-dev" + +// SpoolID desktop app: a thin serial client for the ESP32-C3 writer. The Go +// backend owns only the USB link + the embedded material DB; the firmware +// remains the source of truth for crypto/format/logic. UI is web tech served +// from the embedded frontend build. +func main() { + app := NewApp() + + err := wails.Run(&options.App{ + Title: "SpoolID", + Width: 600, + Height: 895, + MinWidth: 480, + MinHeight: 640, + BackgroundColour: &options.RGBA{R: 27, G: 27, B: 27, A: 255}, + AssetServer: &assetserver.Options{Assets: assets}, + OnStartup: app.startup, + OnShutdown: app.shutdown, + Bind: []interface{}{app}, + }) + if err != nil { + log.Fatal(err) + } +} diff --git a/desktop/wails.json b/desktop/wails.json new file mode 100644 index 0000000..5f3df08 --- /dev/null +++ b/desktop/wails.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "SpoolID", + "outputfilename": "SpoolID", + "frontend:install": "pnpm install", + "frontend:build": "pnpm build", + "frontend:dev:watcher": "pnpm dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Mateus Demboski", + "email": "mateus@demboski.dev" + }, + "info": { + "productName": "SpoolID", + "productVersion": "1.0.0", + "comments": "Creality RFID spool tag writer — desktop serial client" + } +} From efbd706be958c8ee0f15c822aaa01c288966dac6 Mon Sep 17 00:00:00 2001 From: Mateus Demboski Date: Thu, 2 Jul 2026 18:06:10 -0300 Subject: [PATCH 3/5] ci: automated versioning + release pipeline - release-please (Conventional Commits, Angular) drives the version + CHANGELOG and tags releases; commitlint enforces the convention - Release workflow builds + attaches firmware (firmware.bin, littlefs.bin + checksums) and desktop (macOS/Windows/Linux, UPX-compressed on Linux/Windows) - CI builds firmware / web / desktop with per-job path filters; ESLint on the desktop frontend; dependabot for gomod + pnpm + actions - firmware and desktop share one lockstep version (must match on the minor) Co-Authored-By: Claude Opus 4.8 --- .commitlintrc.json | 7 ++ .github/dependabot.yml | 41 +++++++ .github/workflows/ci.yml | 112 +++++++++++++++++++ .github/workflows/claude-code-review.yml | 44 ++++++++ .github/workflows/claude.yml | 50 +++++++++ .github/workflows/commitlint.yml | 30 ++++++ .github/workflows/release.yml | 131 +++++++++++++++++++++++ .release-please-manifest.json | 3 + release-please-config.json | 24 +++++ 9 files changed, 442 insertions(+) create mode 100644 .commitlintrc.json create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/claude-code-review.yml create mode 100644 .github/workflows/claude.yml create mode 100644 .github/workflows/commitlint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..860e27a --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,7 @@ +{ + "extends": ["@commitlint/config-conventional"], + "rules": { + "header-max-length": [2, "always", 100], + "body-max-line-length": [1, "always", 100] + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9e741c1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,41 @@ +version: 2 +updates: + # Desktop Go backend (Wails) + - package-ecosystem: gomod + directory: /desktop + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: chore + include: scope + + # Desktop frontend (Vite/TS, pnpm) + - package-ecosystem: npm + directory: /desktop/frontend + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: chore + include: scope + + # Device web UI (Vite/TS, pnpm) + - package-ecosystem: npm + directory: /web + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: chore + include: scope + + # GitHub Actions used by the workflows + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: ci + include: scope diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..741442f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + changes: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + firmware: ${{ steps.filter.outputs.firmware }} + web: ${{ steps.filter.outputs.web }} + desktop: ${{ steps.filter.outputs.desktop }} + steps: + - uses: actions/checkout@v7 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + firmware: + - 'firmware/**' + - 'tools/gen_db_header.py' + - 'material_database.json' + web: + - 'web/**' + - 'tools/build_web.py' + - 'tools/gen_db_header.py' + - 'material_database.json' + desktop: + - 'desktop/**' + + firmware: + needs: changes + if: needs.changes.outputs.firmware == 'true' + name: Build firmware (ESP32-C3) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Cache PlatformIO + uses: actions/cache@v6 + with: + path: | + ~/.platformio + ~/.cache/pip + key: pio-${{ runner.os }}-${{ hashFiles('firmware/platformio.ini') }} + - name: Install PlatformIO + run: pip install platformio + - name: Prepare generated + secret files + run: | + python tools/gen_db_header.py + cp firmware/include/secrets.h.example firmware/include/secrets.h + - name: Build + run: pio run -d firmware + + web: + needs: changes + if: needs.changes.outputs.web == 'true' + name: Build web assets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: actions/setup-node@v6 + with: + node-version: "22" + - name: Enable pnpm (via corepack) + run: corepack enable + - name: Generate DB header + build/gzip web assets + # build_web.py runs the Vite build (pnpm) then gzips web/dist. + run: | + python tools/gen_db_header.py + python tools/build_web.py + + desktop: + needs: changes + if: needs.changes.outputs.desktop == 'true' + name: Build desktop app (Wails) + # Pinned to 22.04 for libwebkit2gtk-4.0-dev (dropped on ubuntu-24.04). + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v5 + with: + go-version: "1.23" + - uses: actions/setup-node@v6 + with: + node-version: "22" + - name: Enable pnpm (via corepack) + run: corepack enable + - name: Install Wails system deps + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev + - name: Install Wails CLI + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + - name: Build + # `wails build` generates bindings, installs frontend deps (pnpm), + # type-checks (tsc) + bundles the frontend, then compiles the Go binary. + run: cd desktop && wails build + - name: Lint frontend (ESLint) + # deps + generated bindings already present from the build above. + run: cd desktop/frontend && pnpm lint diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..e8340e0 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,44 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' + plugins: 'code-review@claude-code-plugins' + prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..580e80a --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr *)' + diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..3b68db1 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,30 @@ +name: Commit Lint + +# Enforce Conventional Commits (Angular style) so release-please can derive +# versions + changelog. The PR-title check matters most for squash-merges +# (the title becomes the commit on main); the commits check covers merge commits. +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + pr-title: + name: Lint PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + commits: + name: Lint commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8fba0fe --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +name: Release + +# release-please maintains a Release PR from Conventional Commits; merging it +# tags vX.Y.Z + creates the GitHub Release. The build jobs run in the SAME +# workflow (a GITHUB_TOKEN-created release can't trigger a separate workflow), +# gated on release_created, and attach firmware + desktop artifacts to the tag. +# Firmware and desktop share one lockstep version (from the tag) so they always +# match (same minor is the compatibility requirement). +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.rp.outputs.release_created }} + tag_name: ${{ steps.rp.outputs.tag_name }} + version: ${{ steps.rp.outputs.version }} + steps: + - uses: googleapis/release-please-action@v4 + id: rp + with: + token: ${{ secrets.GITHUB_TOKEN }} + + firmware: + needs: release-please + if: ${{ needs.release-please.outputs.release_created == 'true' }} + name: Build + attach firmware + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: actions/setup-node@v6 + with: + node-version: "22" + - name: Enable pnpm (via corepack) + run: corepack enable + - name: Install PlatformIO + run: pip install platformio + - name: Prepare generated + secret files + # build_web.py runs the Vite build (pnpm) then gzips web/dist. + run: | + python tools/gen_db_header.py + python tools/build_web.py + cp firmware/include/secrets.h.example firmware/include/secrets.h + - name: Build firmware + filesystem (version from tag) + env: + PLATFORMIO_BUILD_FLAGS: -DFW_VERSION='"${{ needs.release-please.outputs.version }}"' + run: | + pio run -d firmware + pio run -d firmware -t buildfs + - name: Collect artifacts + checksums + run: | + out=firmware/.pio/build/xiao_esp32c3 + cp "$out/firmware.bin" firmware.bin + cp "$out/littlefs.bin" littlefs.bin 2>/dev/null || cp "$out/spiffs.bin" littlefs.bin + md5sum firmware.bin littlefs.bin | tee checksums-md5.txt + sha256sum firmware.bin littlefs.bin | tee checksums-sha256.txt + - name: Attach to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload "${{ needs.release-please.outputs.tag_name }}" \ + firmware.bin littlefs.bin checksums-md5.txt checksums-sha256.txt --clobber + + desktop: + needs: release-please + if: ${{ needs.release-please.outputs.release_created == 'true' }} + name: Build + attach desktop (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + include: + # UPX is skipped on macOS: modern macOS kills UPX-packed binaries and + # it breaks code signing. Linux/Windows compress fine. + - os: macos-latest + artifact: SpoolID-macos.zip + upx: "" + - os: windows-latest + artifact: SpoolID-windows.zip + upx: "-upx" + - os: ubuntu-22.04 + artifact: SpoolID-linux.zip + upx: "-upx" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v5 + with: + go-version: "1.23" + - uses: actions/setup-node@v6 + with: + node-version: "22" + - name: Enable pnpm (via corepack) + run: corepack enable + - name: Install Wails system deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.0-dev + - name: Install Wails CLI + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + - name: Install UPX + if: matrix.upx != '' + shell: bash + run: | + if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y upx-ucl; fi + if [ "$RUNNER_OS" = "Windows" ]; then choco install upx -y; fi + - name: Build (version from tag) + run: cd desktop && wails build ${{ matrix.upx }} -ldflags "-X main.version=${{ needs.release-please.outputs.version }}" + - name: Package artifact + shell: bash + run: | + cd desktop/build/bin + case "${{ runner.os }}" in + macOS) zip -ry "$GITHUB_WORKSPACE/${{ matrix.artifact }}" SpoolID.app ;; + Windows) 7z a "$GITHUB_WORKSPACE/${{ matrix.artifact }}" SpoolID.exe ;; + Linux) zip -j "$GITHUB_WORKSPACE/${{ matrix.artifact }}" SpoolID ;; + esac + - name: Attach to release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "${{ needs.release-please.outputs.tag_name }}" "${{ matrix.artifact }}" --clobber diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e18ee07 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..00d7c4d --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "simple", + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "packages": { + ".": { + "changelog-path": "CHANGELOG.md" + } + }, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "build", "section": "Build System", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true } + ] +} From ae959aca855573964595a05a9c38ea2a0a15c6e2 Mon Sep 17 00:00:00 2001 From: Mateus Demboski Date: Thu, 2 Jul 2026 18:06:10 -0300 Subject: [PATCH 4/5] docs: project docs, license, and gitignore README + CLAUDE.md (architecture, build/run, serial protocol, OTA + releases), the project license, and .gitignore for the generated build artifacts. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 40 ++++++++++++++ CLAUDE.md | 104 +++++++++++++++++++++++++++++++++++ LICENSE | 21 ++++++++ README.md | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b653bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# macOS junk +.DS_Store + +# secrets (real creds; commit only the .example) +firmware/include/secrets.h + +# python +.venv/ +__pycache__/ + +# platformio +firmware/.pio/ + +# generated assets (regenerated by tools/) +firmware/data/*.gz +firmware/src/material_db_default.h + +# generated from shared/spec.json by tools/gen_spec.py +firmware/src/generated_spec.h +web/spec.js + +# desktop (Wails: Go backend + Vite/TS frontend) — generated, rebuilt by wails +desktop/build/bin/ +# stray `go build ./...` output (module is named spoolid) +desktop/spoolid +desktop/frontend/node_modules/ +# vite output (embedded by go:embed at build) +desktop/frontend/dist/ +# generated Go<->JS bindings (wails) +desktop/frontend/wailsjs/ +# copied from repo root by the copy-db npm script +desktop/frontend/material_database.json +# wails frontend-change hash cache +desktop/frontend/package.json.md5 +# wails dev-mode Info.plist variant (generated by `wails dev`) +desktop/build/darwin/Info.dev.plist + +# web (Vite/TS UI; built + gzipped into firmware/data by tools/build_web.py) +web/node_modules/ +web/dist/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c7cb989 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +ESP32-C3 + RC522 writer that programs Creality-format MIFARE Classic 1K RFID tags so a +Creality K2/K1 printer auto-recognizes third-party / refilled filament spools. Three +parts: **firmware** (the device), **web** UI (served by the device), **desktop** Wails app +— Go serial backend + Vite/TS web UI (drives the device over USB serial). + +## Architecture (the big picture) + +**Firmware is the single source of truth for all data and logic.** It owns the AES keys, +the 48-char spoolData format, weight→length codes, and the UI lists (color swatches, spool +sizes, baud rates, log levels). It builds AND encrypts every payload. + +**Web and desktop are thin clients** — they only collect the user's selections and send +them. They hold no baked constants: they fetch the UI lists from the device at startup. +- web → HTTP (`web_server.cpp`): `/api/spec`, `/api/db`, `/api/write`, `/api/read`, `/api/status`, `/api/config`, `/api/ota` +- desktop → line-JSON over USB CDC (`serial_proto.cpp`): `getspec`, `write`, `read`, `status`, `dump`, `getconfig`, `setconfig`, `dbbegin`/`dbdata`/`dbend`, `otabegin`/`otadata`/`otaend` + +OTA: the device self-flashes via ESP `Update` (`ota.cpp`, MD5 + A/B rollback); clients only +relay image bytes (web POST stream, or serial base64 chunks). `getspec` also returns `version`; +the desktop gates compatibility on major.minor. Versioning/releases are automated (release-please ++ the `release` workflow); see the "Releases & over-the-air updates" section in README. + +Read: `read` decrypts blocks 4/5/6 (`rfid::readTag` + `crypto::decryptChunk`) → 48-char spoolData, +parsed by `spool::parse` into materialId/color/weight. Clients pre-fill the write form with those +values so the user reviews and writes them back (no dedicated clone path — write always rebuilds). + +Both transports are thin shells over the same core: `rfid_writer`, `config`, `material_db`, +`ui_spec`, `spool_data`, `creality_crypto`. When adding a capability, add it to the core and +expose it through *both* `web_server.cpp` and `serial_proto.cpp`. + +### Crypto (do not get this wrong — it determines whether the printer reads the tag) +`firmware/src/creality_crypto.cpp` — AES-128-ECB with two fixed keys (`U_KEY`, `D_KEY`, +reverse-engineered from the K2-RFID reference: https://github.com/DnG-Crafts/K2-RFID). Mode 0 (`U_KEY`): the UID replicated to 16 +bytes is the **plaintext**; encrypt it and take the first 6 bytes as the per-tag MIFARE key +("ekey"). Mode 1 (`D_KEY`): encrypt each 16-char spoolData chunk. The UID is never the key. +Keys live only in firmware; clients never see or compute crypto. + +### spoolData (48 hex chars) +`AB124` + `0276` + `A2` + (`1`+5-char material id) + (`0`+RRGGBB) + lengthCode + serial(6) + +`000000` + `00000000`. Built in `spool_data.cpp`. + +### Material database +`material_database.json` shape: `result.list[]`, each item has `.base{id,brand,name,meterialType,colors,...}` +(note the misspelling **meterialType**) and `.kvParam{...}`. `tools/gen_db_header.py` gzips it +into a PROGMEM C header shipped as the default; users can replace it by uploading a +`material_database.json` — the client gzips it (browser `CompressionStream` for web, +`compress/gzip` for the desktop app) since the ESP32 has no runtime gzip compressor, then +sends it to `/api/db` (web) or via `dbbegin`/`dbdata`/`dbend` (serial), stored to LittleFS. + +## Build / run commands + +PlatformIO tooling lives in a repo-root venv (prefer `python3.14`). The desktop app uses +Go + Node + the [Wails CLI](https://wails.io/docs/gettingstarted/installation), not Python. + +```bash +# venv (first time; firmware/tools only) +python3.14 -m venv .venv && .venv/bin/pip install platformio + +# Firmware (run the two generators FIRST — their outputs are git-ignored) +.venv/bin/python tools/gen_db_header.py # -> firmware/src/material_db_default.h +.venv/bin/python tools/build_web.py # web/* -> firmware/data/*.gz +cp firmware/include/secrets.h.example firmware/include/secrets.h # if missing +.venv/bin/pio run -d firmware # compile +.venv/bin/pio run -d firmware -t upload # flash firmware +.venv/bin/pio run -d firmware -t uploadfs # flash LittleFS web assets +.venv/bin/pio device monitor -d firmware # serial @115200 + +# Desktop app (Wails: Go backend + Vite/TS frontend). +# One-time: install Go 1.23+, Node 18+ with pnpm (corepack enable), and the Wails CLI: +# go install github.com/wailsapp/wails/v2/cmd/wails@latest +cd desktop +wails dev # hot-reload dev window +wails build # -> desktop/build/bin/SpoolID.app (== what CI runs) +``` + +After editing anything in `web/`, re-run `tools/build_web.py` (the device serves the gzipped +copies, not the sources). After editing `material_database.json`, re-run `tools/gen_db_header.py`. + +## Gotchas + +- **Generated files are git-ignored and required to build**: `firmware/src/material_db_default.h`, + `firmware/data/*.gz`, `firmware/include/secrets.h`. A fresh checkout won't compile until you + run `gen_db_header.py` + `build_web.py` and create `secrets.h`. CI does this in `.github/workflows/ci.yml`. +- **If `material_database.json` is lost**, recover it by gunzipping the byte array in the + generated `firmware/src/material_db_default.h` (content-identical, reformatted compact). +- **Desktop (Wails) generated files are git-ignored** and rebuilt by `wails build`/`wails dev`: + `desktop/frontend/wailsjs/` (Go↔JS bindings), `desktop/frontend/dist/`, `desktop/build/bin/`, + and `desktop/frontend/material_database.json` (copied from the repo root by the `copy-db` pnpm + script, then bundled into the frontend). The Go backend (`app.go`) is **serial-only**; the + material DB is parsed in the frontend (`frontend/src/db.ts`), like the web client does. +- **Board is XIAO ESP32-C3 with built-in USB-Serial-JTAG** (VID:PID `303A:1001`), not a UART + bridge. `platformio.ini` sets `monitor_rts=0`/`monitor_dtr=0` so the monitor doesn't hold the + chip in reset. The idle `loop()` prints nothing — to confirm the device is alive, send + `{"cmd":"status"}` and expect a JSON reply, or press RST with the monitor open for boot logs. +- **Serial framing**: responses are single-line JSON; firmware log lines are prefixed with `[`. + Clients must ignore non-`{` lines. Commands accept CR, LF, or CRLF line endings. +- Firmware pins (RC522↔XIAO): SS=GPIO6, RST=GPIO5, SCK=GPIO21(D6), MISO=GPIO20(D7), MOSI=GPIO10. + SCK/MISO deliberately avoid GPIO8/GPIO9 — those are C3 boot strapping pins; wiring + the RC522 there holds the chip in download mode at power-up (manual reset needed to boot). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c0feb9d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mateus Demboski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..58a93b0 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# SpoolID + +Write Creality-format RFID filament tags so a Creality K2/K1 auto-recognizes +third-party or refilled spools. ESP32-C3 + RC522 writer with a built-in web UI and +a cross-platform desktop app (Wails — Go backend + web UI) over USB serial. + +## Layout + +``` +firmware/ PlatformIO project (Seeed XIAO ESP32-C3, Arduino) +web/ UI source -> gzipped into firmware/data/ by tools/build_web.py +desktop/ Wails desktop app — Go serial backend + Vite/TS web UI +tools/ gen_db_header.py (PROGMEM DB), build_web.py (asset gzip) +material_database.json Creality material DB (default, baked into firmware) +``` + +### Firmware is the source of truth; web + desktop are thin clients + +The firmware owns all data and logic: AES keys, the spoolData format, weight->length +codes, and the UI lists (color swatches, spool sizes, baud rates, log levels). It +builds and encrypts every payload — clients only send the user's selections. + +Clients hold **no baked constants**. They fetch the UI lists from the device at +startup and render them: + +- web: `GET /api/spec` (see `web/app.js`, `web/config.html`) +- desktop: serial `{"cmd":"getspec"}` (see `desktop/frontend/src/main.ts`) + +The firmware-owned lists live in `firmware/src/ui_spec.cpp`. Behaviour-critical data +(keys/format/codes) stays firmware-only and is never exposed. + +## Hardware + +| RC522 | XIAO ESP32-C3 | +|-------|---------------| +| 3.3V | 3.3V | +| GND | GND | +| SDA/SS| GPIO6 (D4) | +| RST | GPIO5 (D3) | +| MOSI | GPIO10 (D10) | +| MISO | GPIO20 (D7) | +| SCK | GPIO21 (D6) | +| IRQ | NC | + +> SCK/MISO avoid GPIO8/GPIO9 — those are ESP32-C3 boot strapping pins; wiring the +> RC522 to them holds the chip in download mode at power-up (needs a manual reset +> to start). D6/D7 are safe. + +Optional active buzzer on `BUZZER_PIN` in `firmware/src/main.cpp` (default `2` = D0; set `-1` to disable). + +## Build & flash firmware + +```bash +# 1. regenerate the PROGMEM default DB + gzipped web assets +.venv/bin/python tools/gen_db_header.py +.venv/bin/python tools/build_web.py + +# 2. copy secrets template and fill WiFi/AP defaults (optional; configurable later) +cp firmware/include/secrets.h.example firmware/include/secrets.h + +# 3. flash code + filesystem +cd firmware +pio run -t upload # firmware +pio run -t uploadfs # LittleFS web assets (data/*.gz) +pio device monitor # serial @115200 +``` + +First boot tries STA WiFi; if it fails it starts the `SpoolID` access point with a +captive portal at the device IP — open it and use the Config page. Once on WiFi the +device is also reachable at `http://spoolid.local` (mDNS hostname, configurable). + +## Using it + +- **Web:** browse to the device IP. Main page = brand → filament → size → color → + Write, then tap a blank MIFARE Classic 1K tag on the reader. **Read** decrypts a + tapped tag and pre-fills the form with its material/color/weight so you can review + and Write it back. Config page = DB upload/pull, WiFi/AP, log level, baud. +- **Desktop app (Wails — Go + web UI):** pick a serial port and **Connect** in the + window, then use the Write / Config views (same brand → filament → size → color → + Write flow, plus Read pre-fill). Prerequisites: [Go](https://go.dev/dl/) 1.23+, + [Node](https://nodejs.org/) 18+ with **pnpm** (`corepack enable`), and the + [Wails CLI](https://wails.io/docs/gettingstarted/installation) + (`go install github.com/wailsapp/wails/v2/cmd/wails@latest`). + ```bash + cd desktop + wails dev # hot-reload dev window + wails build # -> desktop/build/bin/SpoolID.app (.exe / binary on other OSes) + ``` + `wails build` produces a single native binary per platform (macOS `.app`, Windows + `.exe`, Linux ELF) — no runtime, no bundled interpreter. The material DB is copied + from the repo root into `desktop/frontend/` at build time (`copy-db` pnpm script) + and bundled into the frontend. + +## Serial protocol (USB CDC, line-JSON) + +Responses are single-line JSON; log lines start with `[`. Commands: + +```json +{"cmd":"write","materialId":"01001","color":"1200F6","weight":"1KG"} +{"cmd":"status"} +{"cmd":"read"} // decrypt a tapped tag -> materialId/color/weight +{"cmd":"dump"} // raw (encrypted) blocks 4-6 of a tapped tag +{"cmd":"beep"} // fire the buzzer once (test) +{"cmd":"getspec"} // firmware-owned UI lists + firmware version +{"cmd":"getconfig"} / {"cmd":"setconfig", ...} +{"cmd":"dbbegin","size":N} / {"cmd":"dbdata","b":""} / {"cmd":"dbend"} // upload a gzipped DB +{"cmd":"otabegin","size":N,"target":"app","md5":"..."} // start an OTA session +{"cmd":"otadata","b":""} // stream image bytes (* N) +{"cmd":"otaend"} // verify + reboot into the new image +``` + +`getspec` also returns `version` (firmware semver); clients gate compatibility on +the major.minor pair. The web transport mirrors OTA at `POST /api/ota?target=app|fs&md5=`. + +## Releases & over-the-air updates + +Versioning is automated from Conventional Commits (Angular). `release-please` +maintains a Release PR (version bump + CHANGELOG); merging it tags `vX.Y.Z` and the +Release workflow attaches firmware (`firmware.bin`, `littlefs.bin` + checksums) and +desktop builds (macOS/Windows/Linux; the Linux/Windows binaries are UPX-compressed, +macOS is not — the OS rejects packed binaries). Firmware and desktop share one +**lockstep version** and must match on the minor — the desktop warns on a mismatch. + +Updating a device (no cable re-flash after the first install): + +- **Desktop:** Config → *Firmware update* — paste the release asset URL + (`firmware.bin` or `littlefs.bin`), pick the target, Flash. The app downloads it + and streams it to the device over serial. +- **Web:** Config page → *Firmware update* — select a `.bin` and upload (or + `POST /api/ota?target=app|fs&md5=` directly with the raw image body). + +Either way the device does the flashing itself via ESP `Update` (MD5-verified, A/B +rollback), then reboots. Clients only relay bytes — no TLS/esptool on the MCU. + +> First install and any partition-table change still need a one-time USB flash +> (`pio run -t upload` + `-t uploadfs`), which wipes NVS + LittleFS. + +## spoolData (48 hex chars) + +`AB124` + vendor `0276` + `A2` + filamentId(`1`+base.id) + color(`0`+RRGGBB) + +lengthCode + serial(6) + reserve `000000` + `00000000`. +Length codes: 1KG=0330, 750G=0247, 600G=0198, 500G=0165, 250G=0082. + +## Crypto + +AES-128-ECB with two fixed keys reverse-engineered from the Creality firmware +(matches the [K2-RFID](https://github.com/DnG-Crafts/K2-RFID) reference project): + +- **u_key** — derive the 6-byte MIFARE key: encrypt the UID (replicated to 16 bytes) + and take the first 6 bytes. The UID is the *plaintext*, not the key. +- **d_key** — encrypt each 16-char spoolData chunk written to blocks 4/5/6. + +Both keys live only in `firmware/src/creality_crypto.cpp` (clients never see them). +The reference's hand-rolled AES was verified byte-identical to standard AES-128-ECB, +so the mbedtls implementation here produces the same output the printer expects. From 05b5f303f979bd5cecaefda2cab428e8cb00687a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:07:40 +0000 Subject: [PATCH 5/5] ci(deps): Bump dorny/paths-filter from 3 to 4 Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 3 to 4. - [Release notes](https://github.com/dorny/paths-filter/releases) - [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md) - [Commits](https://github.com/dorny/paths-filter/compare/v3...v4) --- updated-dependencies: - dependency-name: dorny/paths-filter dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 741442f..fe39520 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: desktop: ${{ steps.filter.outputs.desktop }} steps: - uses: actions/checkout@v7 - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@v4 id: filter with: filters: |