commit 50256fa3848ba5817dd455067905644aeb6661c9 Author: Ruben Hensen Date: Sun Jun 7 18:15:06 2026 +0200 Initial scaffold: NixOS + SvelteKit dashboard + Python renderer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f57540 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Nix +result +result-* +.direnv/ + +# Node / SvelteKit +dashboard/node_modules/ +dashboard/.svelte-kit/ +dashboard/build/ +dashboard/.env +dashboard/.env.* +!dashboard/.env.example + +# Python +__pycache__/ +*.pyc +*.pyo +*.so +.venv/ +*.egg-info/ +.pytest_cache/ + +# Editors +.vscode/ +.idea/ +*.swp + +# Secrets (handled via sops-nix / LoadCredential, never plaintext in repo) +*.secret +secrets.local.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7ea84c --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# eink + +NixOS-managed Raspberry Pi 4 driving a Waveshare 12.48" e-paper panel as a +calendar / day-planner. CalDAV data comes from the household Stalwart at +`mail.rubenhensen.nl`. The Pi pulls every commit on `main` via +[comin](https://github.com/nlewo/comin) — never touch the SD card after the +first flash. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Raspberry Pi 4 (NixOS aarch64, hostname: dayplanner) │ +│ │ +│ systemd: eink-dashboard.service systemd: eink-renderer.service │ +│ ┌──────────────────────────┐ ┌──────────────────────────────┐ │ +│ │ SvelteKit (Node adapter) │◄──────│ Playwright → Pillow dither │ │ +│ │ :3000 │ HTTP │ → Waveshare 12.48" driver │ │ +│ │ fetches CalDAV every load│ │ (every REFRESH_SECONDS) │ │ +│ └────────────┬─────────────┘ └───────────────┬──────────────┘ │ +│ │ │ SPI │ +│ ▼ ▼ │ +│ mail.rubenhensen.nl Waveshare 12.48" HAT │ +│ (Stalwart CalDAV) (1304×984, 4 sub-panels) │ +└─────────────────────────────────────────────────────────────────────────┘ + +Deploy path: laptop → git push → Gitea → comin (60s poll) → Pi +``` + +## Layout + +``` +eink/ +├── flake.nix # nixpkgs + comin → nixosConfigurations.dayplanner +├── hosts/dayplanner/ # this Pi's NixOS config (users, comin, hardware) +├── modules/ # eink-dashboard.nix + eink-renderer.nix +├── dashboard/ # SvelteKit app rendered headlessly at 1304×984 +└── renderer/ # Python loop: screenshot → dither → push +``` + +## One-time bootstrap + +1. **Flash NixOS aarch64 SD image** onto a card and boot the Pi. + - Image: + - On first boot, set a `nixos` user password via the serial console or + monitor + keyboard. Enable SSH manually (`sudo systemctl start sshd`) + and note the Pi's IP. +2. **Add your SSH public key** to `hosts/dayplanner/default.nix` under + `users.users.ruben.openssh.authorizedKeys.keys`. +3. **Push this repo to a Git remote** the Pi can reach (e.g. your Gitea). + Update `services.comin.remotes[0].url` in `hosts/dayplanner/default.nix` + if the URL differs. +4. **First synchronous deploy from your laptop:** + ```sh + nix run nixpkgs#nixos-rebuild -- switch \ + --flake .#dayplanner \ + --target-host nixos@ \ + --build-host nixos@ \ + --use-remote-sudo + ``` + (Building on the Pi itself avoids needing a cross-compile setup on your + Mac. Slow but one-time.) +5. **Drop the CalDAV password** on the Pi: + ```sh + ssh ruben@ 'sudo install -d -m 700 /var/lib/eink/secrets' + ssh ruben@ 'sudo tee /var/lib/eink/secrets/caldav-password >/dev/null' \ + <<< 'your-caldav-app-password' + ssh ruben@ 'sudo chmod 600 /var/lib/eink/secrets/caldav-password' + ``` + (Long-term replace this with [sops-nix](https://github.com/Mic92/sops-nix) + so the secret lives encrypted in this repo.) +6. **Vendor the Waveshare driver** (see `renderer/waveshare/README.md`). + +After this, the steady-state workflow is just: + +```sh +git push origin main # comin picks it up within ~60s and switches the Pi +``` + +## Iterating on the dashboard locally + +You don't need the Pi to develop the UI — just open the SvelteKit dev +server in a browser sized to 1304×984: + +```sh +cd dashboard +npm install +CALDAV_PASSWORD=... npm run dev -- --host +# open http://localhost:5173 in a browser with viewport 1304×984 +``` + +Chrome devtools → "Toggle device toolbar" → set to 1304×984 to preview +exactly what the panel will see. + +## Refresh cadence & panel wear + +`eink.renderer.refreshSeconds = 300` (5 min) is the default. The 12.48" +panel has **no partial refresh** — every update is a full flash that +takes ~8s and consumes some panel life. Below 180 s is abusive; 300-600 +s is comfortable. Consider scheduling a nightly invert to spread wear. + +## Panel variant + +Set `eink.renderer.panel = "bwr"` in `hosts/dayplanner/default.nix` if +you have the **12.48inch e-Paper Module (B)** (B/W/Red) instead of the +plain B/W version. The dashboard's red channel is whatever CSS color +matches `rgb(>180, <100, <100)` — see `renderer/render.py:to_bwr`. + +## Troubleshooting + +- **Comin not pulling:** `journalctl -u comin.service -f` on the Pi. +- **Renderer dry-running:** check `/tmp/eink-last.png` — if it exists, + the screenshot+dither pipeline works and the Waveshare driver is the + remaining piece (see `renderer/waveshare/README.md`). +- **Dashboard 500:** `journalctl -u eink-dashboard.service -f`. Most + common cause: empty `/var/lib/eink/secrets/caldav-password`. diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..67377fc --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,1965 @@ +{ + "name": "eink-dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eink-dashboard", + "version": "0.1.0", + "dependencies": { + "ical.js": "^2.1.0", + "tsdav": "^2.1.0" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.8.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.5.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.3.tgz", + "integrity": "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", + "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.59.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.63.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.63.0.tgz", + "integrity": "sha512-1DrR7vQ9brXLrNE2sLtFXApwr7AUXPfpbIFYc+CQRf2+iURaZbXGU+7TG/RLr+9fdFkoRdyCAVUOHCChw11LFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.1.1.tgz", + "integrity": "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", + "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ical.js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz", + "integrity": "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==", + "license": "MPL-2.0" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.56.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.2.tgz", + "integrity": "sha512-1lDf8TLqpxyAt3xgybfytWPJQbaUD6TiDgpiCLH0BKrKEwzecB9pjuNVnEJMpzH018xUzo6oxheK2HT0oa2RoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.11", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.6.0.tgz", + "integrity": "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "0.1.1", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tsdav": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tsdav/-/tsdav-2.2.2.tgz", + "integrity": "sha512-/uC/RItcdYVxUj8b2gT4CorAkUmDFUuzhgS4XVT+OVzGRE80hH5ldubcWH13dHa2T9rNaXu6UIEWHlQ1UL7H1Q==", + "license": "MIT", + "dependencies": { + "base-64": "1.0.0", + "debug": "4.4.3", + "xml-js": "1.6.11" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..b672cba --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,25 @@ +{ + "name": "eink-dashboard", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.8.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.5.0", + "vite": "^6.0.0" + }, + "dependencies": { + "tsdav": "^2.1.0", + "ical.js": "^2.1.0" + } +} diff --git a/dashboard/src/app.css b/dashboard/src/app.css new file mode 100644 index 0000000..2e7febf --- /dev/null +++ b/dashboard/src/app.css @@ -0,0 +1,34 @@ +/* E-paper-friendly base styles. + Constraints: + - Hard 1-bit palette: pure black on pure white. No grays (they dither badly). + - No thin lines below 2px — Floyd-Steinberg dither breaks them up. + - No anti-aliased small fonts. Stick to bold weights at >= 18px. + - No soft shadows; use crisp 2-4px solid borders for separation. */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + width: 1304px; + height: 984px; + background: #ffffff; + color: #000000; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-size: 20px; + font-weight: 500; + -webkit-font-smoothing: none; + -moz-osx-font-smoothing: unset; + text-rendering: geometricPrecision; +} + +h1, h2, h3 { + font-weight: 800; + letter-spacing: -0.01em; +} + +.border-thick { + border: 3px solid #000000; +} diff --git a/dashboard/src/app.html b/dashboard/src/app.html new file mode 100644 index 0000000..428269f --- /dev/null +++ b/dashboard/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte new file mode 100644 index 0000000..34832d6 --- /dev/null +++ b/dashboard/src/routes/+layout.svelte @@ -0,0 +1,6 @@ + + +{@render children()} diff --git a/dashboard/src/routes/+page.server.ts b/dashboard/src/routes/+page.server.ts new file mode 100644 index 0000000..b0557a2 --- /dev/null +++ b/dashboard/src/routes/+page.server.ts @@ -0,0 +1,112 @@ +import { readFileSync } from 'node:fs'; +import { createDAVClient } from 'tsdav'; +import ICAL from 'ical.js'; +import type { PageServerLoad } from './$types'; + +const CALDAV_URL = process.env.CALDAV_URL ?? 'https://mail.rubenhensen.nl/dav/'; +const CALDAV_USER = process.env.CALDAV_USER ?? 'ruben@rubenhensen.nl'; + +// systemd LoadCredential drops the password at $CREDENTIALS_DIRECTORY/caldav-password. +// Falls back to env for dev. +function readCalDavPassword(): string { + const dir = process.env.CREDENTIALS_DIRECTORY; + if (dir) { + try { + return readFileSync(`${dir}/caldav-password`, 'utf-8').trim(); + } catch {} + } + return process.env.CALDAV_PASSWORD ?? ''; +} + +interface Event { + start: Date; + end: Date; + summary: string; + allDay: boolean; +} + +async function fetchTodayEvents(): Promise { + const password = readCalDavPassword(); + if (!password) return []; + + const client = await createDAVClient({ + serverUrl: CALDAV_URL, + credentials: { username: CALDAV_USER, password }, + authMethod: 'Basic', + defaultAccountType: 'caldav' + }); + + const calendars = await client.fetchCalendars(); + const start = new Date(); + start.setHours(0, 0, 0, 0); + const end = new Date(start); + end.setDate(end.getDate() + 1); + + // Stalwart returns "207 Multi-Status / 404 No resources found" for empty + // time-range queries, which tsdav rejects as an error. Fetch everything + // and filter client-side — the calendar is small enough that this is fine. + const windowStart = ICAL.Time.fromJSDate(start, true); + const windowEnd = ICAL.Time.fromJSDate(end, true); + + const events: Event[] = []; + for (const cal of calendars) { + const objects = await client.fetchCalendarObjects({ calendar: cal }); + for (const obj of objects) { + if (!obj.data) continue; + const jcal = ICAL.parse(obj.data); + const comp = new ICAL.Component(jcal); + + for (const vevent of comp.getAllSubcomponents('vevent')) { + const ev = new ICAL.Event(vevent); + if (ev.isRecurrenceException()) continue; + + const push = (s: ICAL.Time, e: ICAL.Time) => { + events.push({ + start: s.toJSDate(), + end: e.toJSDate(), + summary: ev.summary, + allDay: s.isDate + }); + }; + + if (!ev.isRecurring()) { + if (ev.endDate.compare(windowStart) <= 0) continue; + if (ev.startDate.compare(windowEnd) >= 0) continue; + push(ev.startDate, ev.endDate); + continue; + } + + const iter = ev.iterator(); + let next: ICAL.Time | null; + while ((next = iter.next())) { + if (next.compare(windowEnd) >= 0) break; + const occ = ev.getOccurrenceDetails(next); + if (occ.endDate.compare(windowStart) <= 0) continue; + push(occ.startDate, occ.endDate); + } + } + } + } + return events.sort((a, b) => a.start.getTime() - b.start.getTime()); +} + +export const load: PageServerLoad = async () => { + let events: Event[] = []; + let error: string | null = null; + try { + events = await fetchTodayEvents(); + } catch (e) { + if (e instanceof Error) { + error = e.message; + } else if (typeof e === 'object' && e !== null) { + try { error = JSON.stringify(e); } catch { error = String(e); } + } else { + error = String(e); + } + } + return { + events, + error, + renderedAt: new Date().toISOString() + }; +}; diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte new file mode 100644 index 0000000..6363487 --- /dev/null +++ b/dashboard/src/routes/+page.svelte @@ -0,0 +1,107 @@ + + +
+
+

{fmtDate(today)}

+ refreshed {fmtTime(data.renderedAt)} +
+ +
+

Today

+ {#if data.error} +

CalDAV error: {data.error}

+ {:else if data.events.length === 0} +

Nothing scheduled.

+ {:else} +
    + {#each data.events as ev (`${ev.start}-${ev.summary}`)} +
  • + + {#if ev.allDay}all day{:else}{fmtTime(ev.start)}–{fmtTime(ev.end)}{/if} + + {ev.summary} +
  • + {/each} +
+ {/if} +
+
+ + diff --git a/dashboard/svelte.config.js b/dashboard/svelte.config.js new file mode 100644 index 0000000..821e659 --- /dev/null +++ b/dashboard/svelte.config.js @@ -0,0 +1,10 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +export default { + preprocess: vitePreprocess(), + kit: { + adapter: adapter() + } +}; diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 0000000..4344710 --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 0000000..3406f32 --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b361485 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "E-paper dayplanner — NixOS config for Pi 4 driving Waveshare 12.48\" e-paper"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + + comin = { + url = "github:nlewo/comin"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, comin, ... }: { + nixosConfigurations.dayplanner = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; + specialArgs = { inherit self; }; + modules = [ + comin.nixosModules.comin + ./hosts/dayplanner + ]; + }; + }; +} diff --git a/hosts/dayplanner/default.nix b/hosts/dayplanner/default.nix new file mode 100644 index 0000000..0e3a831 --- /dev/null +++ b/hosts/dayplanner/default.nix @@ -0,0 +1,65 @@ +{ config, lib, pkgs, self, ... }: + +{ + imports = [ + ./hardware.nix + ../../modules/eink-dashboard.nix + ../../modules/eink-renderer.nix + ]; + + networking.hostName = "dayplanner"; + networking.domain = "local"; + + # WiFi or wired. Default to DHCP — set static IP via your router if you want predictability. + networking.networkmanager.enable = true; + networking.firewall.enable = true; + networking.firewall.allowedTCPPorts = [ 22 ]; + + time.timeZone = "Europe/Amsterdam"; + + users.users.ruben = { + isNormalUser = true; + extraGroups = [ "wheel" "gpio" "spi" "dialout" ]; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEiY+6AEuKQHyt4Mpg/IbyoIkhg76u3xFPdXGpTsmwxT ruben.hensen@protonmail.com" + ]; + }; + + services.openssh = { + enable = true; + settings.PasswordAuthentication = false; + settings.PermitRootLogin = "no"; + }; + + # Pull-deploy: every commit on `main` lands on the Pi within ~60 s. + # The remote URL must be reachable from the Pi (push to GitHub/Gitea). + services.comin = { + enable = true; + remotes = [{ + name = "origin"; + url = "https://git.rubenhensen.nl/ruben/eink.git"; + branches.main.name = "main"; + }]; + hostname = "dayplanner"; + }; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 14d"; + }; + + environment.systemPackages = with pkgs; [ + git + htop + vim + ]; + + eink.dashboard.enable = true; + eink.dashboard.caldavUser = "ruben"; + eink.renderer.enable = true; + eink.renderer.dashboardUrl = "http://127.0.0.1:3000"; + + system.stateVersion = "25.11"; +} diff --git a/hosts/dayplanner/hardware.nix b/hosts/dayplanner/hardware.nix new file mode 100644 index 0000000..e521509 --- /dev/null +++ b/hosts/dayplanner/hardware.nix @@ -0,0 +1,54 @@ +{ config, lib, pkgs, modulesPath, ... }: + +{ + # Pi 4 aarch64. Boot from the official NixOS sd-image; we don't generate + # the image from this flake — flash the upstream aarch64 sd-image once, + # then `nixos-rebuild switch --flake .#dayplanner --target-host` swaps + # the running system over and comin takes over from there. + imports = [ + "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" + ]; + + # Drop the default channel; we deploy from the flake. + sdImage.compressImage = false; + + boot.loader.grub.enable = false; + boot.loader.generic-extlinux-compatible.enable = true; + + # The Waveshare 12.48" HAT is driven entirely from userspace Python via + # /dev/spidev*. We only need SPI enabled in the device tree and the + # gpio/spi groups available to the renderer user. + boot.kernelParams = [ "console=serial0,115200" "console=tty1" ]; + + # Enable SPI0 (CE0/CE1) and the extra GPIOs the HAT needs. + # See: https://www.waveshare.com/wiki/12.48inch_e-Paper_Module + hardware.deviceTree = { + enable = true; + overlays = [ + { + name = "spi-on"; + dtsText = '' + /dts-v1/; + /plugin/; + / { + compatible = "brcm,bcm2711"; + fragment@0 { + target = <&spi0>; + __overlay__ { + status = "okay"; + }; + }; + }; + ''; + } + ]; + }; + + users.groups.spi = {}; + users.groups.gpio = {}; + + services.udev.extraRules = '' + SUBSYSTEM=="spidev", GROUP="spi", MODE="0660" + SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" + ''; +} diff --git a/modules/eink-dashboard.nix b/modules/eink-dashboard.nix new file mode 100644 index 0000000..ba88193 --- /dev/null +++ b/modules/eink-dashboard.nix @@ -0,0 +1,82 @@ +{ config, lib, pkgs, self, ... }: + +let + cfg = config.eink.dashboard; + + dashboardSrc = self + "/dashboard"; + + dashboardBuild = pkgs.buildNpmPackage { + pname = "eink-dashboard"; + version = "0.1.0"; + src = dashboardSrc; + + # Re-generate this hash with: + # nix run nixpkgs#prefetch-npm-deps -- dashboard/package-lock.json + npmDepsHash = "sha256-UticV62t7/CYZ1nVZPNxUZn38WhQ4mHmYT3V90D01OU="; + + buildPhase = '' + runHook preBuild + npm run build + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -r build package.json $out/ + cp -r node_modules $out/ + runHook postInstall + ''; + }; +in +{ + options.eink.dashboard = { + enable = lib.mkEnableOption "SvelteKit e-paper dashboard"; + + port = lib.mkOption { + type = lib.types.port; + default = 3000; + }; + + caldavUrl = lib.mkOption { + type = lib.types.str; + default = "https://mail.rubenhensen.nl/dav/cal/"; + description = "Base URL of the CalDAV server. Stalwart serves CalDAV at /dav/cal/, distinct from CardDAV at /dav/card/."; + }; + + caldavUser = lib.mkOption { + type = lib.types.str; + default = "ruben@rubenhensen.nl"; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.eink-dashboard = { + description = "E-paper dashboard (SvelteKit)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + HOST = "127.0.0.1"; + PORT = toString cfg.port; + CALDAV_URL = cfg.caldavUrl; + CALDAV_USER = cfg.caldavUser; + NODE_ENV = "production"; + }; + + serviceConfig = { + # CalDAV password loaded as a systemd credential. Provision the + # encrypted file under /var/lib/eink/secrets/caldav-password + # (or use sops-nix). See README. + LoadCredential = [ "caldav-password:/var/lib/eink/secrets/caldav-password" ]; + + DynamicUser = true; + StateDirectory = "eink-dashboard"; + ExecStart = "${pkgs.nodejs}/bin/node ${dashboardBuild}/build/index.js"; + Restart = "always"; + RestartSec = "5s"; + }; + }; + }; +} diff --git a/modules/eink-renderer.nix b/modules/eink-renderer.nix new file mode 100644 index 0000000..f4a43e2 --- /dev/null +++ b/modules/eink-renderer.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, self, ... }: + +let + cfg = config.eink.renderer; + + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + pillow + numpy + playwright + requests + # The Waveshare 12.48" lib is not packaged in nixpkgs; we vendor it + # under renderer/waveshare/ and import it as a local module. + spidev + rpi-gpio + ]); +in +{ + options.eink.renderer = { + enable = lib.mkEnableOption "E-paper renderer (Playwright + Pillow + Waveshare driver)"; + + dashboardUrl = lib.mkOption { + type = lib.types.str; + default = "http://127.0.0.1:3000"; + }; + + refreshSeconds = lib.mkOption { + type = lib.types.int; + default = 300; + description = "Seconds between full panel refreshes. Below 180 will wear the panel."; + }; + + panel = lib.mkOption { + type = lib.types.enum [ "bw" "bwr" ]; + default = "bw"; + description = "Panel variant: bw = 12.48\" B/W, bwr = 12.48\" Module (B) B/W/Red."; + }; + }; + + config = lib.mkIf cfg.enable { + # Playwright needs a Chromium and its system deps. Use the nixpkgs one + # so the binary path is stable across rebuilds (not the npm-downloaded + # browser, which won't find its sandbox libs on NixOS). + systemd.services.eink-renderer = { + description = "E-paper renderer loop"; + after = [ "eink-dashboard.service" "network-online.target" ]; + wants = [ "eink-dashboard.service" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + DASHBOARD_URL = cfg.dashboardUrl; + REFRESH_SECONDS = toString cfg.refreshSeconds; + PANEL_VARIANT = cfg.panel; + PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; + PYTHONPATH = "${self}/renderer"; + }; + + serviceConfig = { + Type = "simple"; + ExecStart = "${pythonEnv}/bin/python ${self}/renderer/render.py"; + Restart = "always"; + RestartSec = "10s"; + + # The Waveshare lib needs raw SPI + GPIO. Run as a dedicated user + # in the spi/gpio groups rather than root. + User = "eink"; + Group = "eink"; + SupplementaryGroups = [ "spi" "gpio" ]; + }; + }; + + users.users.eink = { + isSystemUser = true; + group = "eink"; + extraGroups = [ "spi" "gpio" ]; + }; + users.groups.eink = {}; + }; +} diff --git a/renderer/pyproject.toml b/renderer/pyproject.toml new file mode 100644 index 0000000..0f4a4e6 --- /dev/null +++ b/renderer/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "eink-renderer" +version = "0.1.0" +description = "Screenshot the dashboard, dither it, push it to the Waveshare 12.48\" panel." +requires-python = ">=3.11" +dependencies = [ + "playwright>=1.48", + "pillow>=10.4", + "numpy>=2.1", + "requests>=2.32", + # Hardware-only — installed by Nix, not pip on dev machines. + # "spidev>=3.6", + # "RPi.GPIO>=0.7", +] diff --git a/renderer/render.py b/renderer/render.py new file mode 100644 index 0000000..b13afdd --- /dev/null +++ b/renderer/render.py @@ -0,0 +1,144 @@ +"""E-paper renderer loop. + +Pipeline: + Playwright screenshot of $DASHBOARD_URL at 1304x984 + -> Pillow 1-bit dither (Floyd-Steinberg) + -> Waveshare 12.48" driver push + +Tunables come from env: DASHBOARD_URL, REFRESH_SECONDS, PANEL_VARIANT. +""" +from __future__ import annotations + +import io +import logging +import os +import signal +import sys +import time +from pathlib import Path + +from PIL import Image +from playwright.sync_api import sync_playwright + +log = logging.getLogger("eink.renderer") +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + +WIDTH, HEIGHT = 1304, 984 +DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "http://127.0.0.1:3000") +REFRESH_SECONDS = int(os.environ.get("REFRESH_SECONDS", "300")) +PANEL_VARIANT = os.environ.get("PANEL_VARIANT", "bw") # bw | bwr + + +def screenshot(url: str) -> Image.Image: + """Headless Chromium screenshot at the panel's native resolution.""" + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) + ctx = browser.new_context( + viewport={"width": WIDTH, "height": HEIGHT}, + device_scale_factor=1, + ) + page = ctx.new_page() + page.goto(url, wait_until="networkidle", timeout=30_000) + png = page.screenshot(full_page=False, type="png", omit_background=False) + browser.close() + return Image.open(io.BytesIO(png)).convert("RGB") + + +def to_bw(img: Image.Image) -> Image.Image: + """Floyd-Steinberg dither to 1-bit. Pillow's built-in is C-accelerated.""" + return img.convert("L").convert("1", dither=Image.Dither.FLOYDSTEINBERG) + + +def to_bwr(img: Image.Image) -> tuple[Image.Image, Image.Image]: + """Separate red plane from B/W plane for the Module (B) variant. + + Naive thresholding: pixels with R > 180 and G,B < 100 -> red plane. + Everything else -> dithered B/W. + """ + import numpy as np + + arr = np.array(img) + r, g, b = arr[..., 0], arr[..., 1], arr[..., 2] + red_mask = (r > 180) & (g < 100) & (b < 100) + + bw_arr = arr.copy() + bw_arr[red_mask] = 255 # remove red from BW plane + bw_img = to_bw(Image.fromarray(bw_arr)) + + red_img = Image.fromarray((red_mask * 255).astype("uint8"), mode="L").convert("1") + return bw_img, red_img + + +def push_to_panel_bw(bw: Image.Image) -> None: + """Push a 1-bit image to the B/W 12.48\" panel. + + NOTE: requires the Waveshare 12.48\" Python driver vendored under + renderer/waveshare/. See waveshare/README.md for install steps. + """ + try: + from waveshare import epd12in48 # type: ignore + except ImportError: + log.warning("Waveshare driver not vendored — dry-run mode (no panel push).") + bw.save("/tmp/eink-last.png") + return + + epd = epd12in48.EPD() + epd.Init() + epd.display(epd.getbuffer(bw)) + epd.EPD_Sleep() + + +def push_to_panel_bwr(bw: Image.Image, red: Image.Image) -> None: + try: + from waveshare import epd12in48b # type: ignore + except ImportError: + log.warning("Waveshare (B) driver not vendored — dry-run mode (no panel push).") + bw.save("/tmp/eink-last-bw.png") + red.save("/tmp/eink-last-red.png") + return + + epd = epd12in48b.EPD() + epd.Init() + epd.display(epd.getbuffer(bw), epd.getbuffer(red)) + epd.EPD_Sleep() + + +def render_once() -> None: + log.info("Screenshot %s", DASHBOARD_URL) + img = screenshot(DASHBOARD_URL) + if PANEL_VARIANT == "bwr": + bw, red = to_bwr(img) + push_to_panel_bwr(bw, red) + else: + push_to_panel_bw(to_bw(img)) + log.info("Frame pushed to panel.") + + +def main() -> None: + stop = False + + def handle(*_): + nonlocal stop + stop = True + log.info("Shutdown signal — exiting after current frame.") + + signal.signal(signal.SIGTERM, handle) + signal.signal(signal.SIGINT, handle) + + while not stop: + started = time.monotonic() + try: + render_once() + except Exception: + log.exception("Render cycle failed; will retry next interval.") + elapsed = time.monotonic() - started + sleep_for = max(0, REFRESH_SECONDS - elapsed) + log.info("Sleeping %.1fs", sleep_for) + for _ in range(int(sleep_for)): + if stop: + return + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/renderer/waveshare/README.md b/renderer/waveshare/README.md new file mode 100644 index 0000000..67849ab --- /dev/null +++ b/renderer/waveshare/README.md @@ -0,0 +1,40 @@ +# Waveshare 12.48" driver + +The 12.48" driver lives in a **separate repo** from the main Waveshare +e-Paper one — it's at [`waveshareteam/12.48inch-e-paper`](https://github.com/waveshareteam/12.48inch-e-paper). +The big `waveshareteam/e-Paper` repo does **not** contain it. + +## Install + +Run this from the project root (`~/Repos/eink/`): + +```sh +git clone --depth 1 https://github.com/waveshareteam/12.48inch-e-paper.git /tmp/waveshare-12in48 +cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epd12in48.py renderer/waveshare/ +cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epd12in48b.py renderer/waveshare/ +cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epd12in48b_V2.py renderer/waveshare/ +cp /tmp/waveshare-12in48/RaspberryPi/python/lib/epdconfig.py renderer/waveshare/ +rm -rf /tmp/waveshare-12in48 +``` + +After copying, `renderer/waveshare/` should look like: + +``` +renderer/waveshare/ +├── __init__.py # (already present) +├── README.md # (already present, this file) +├── epd12in48.py # B/W variant driver +├── epd12in48b.py # B/W/Red (Module B) original driver +├── epd12in48b_V2.py # B/W/Red (Module B) v2 driver — newer revision +└── epdconfig.py # SPI + GPIO abstraction (uses spidev + RPi.GPIO) +``` + +The renderer (`renderer/render.py`) imports `from waveshare import epd12in48` +(B/W) or `from waveshare import epd12in48b` (B/W/Red). For the v2 panel +revision, edit `render.py` to import `epd12in48b_V2` instead. + +## Why not vendor it from the start? + +It's vendor code with its own license headers — kept out of git until +the user opts in to keep the repo tidy and the license boundary clear. +Add it to `.gitignore` if you'd rather pull it fresh on each deploy. diff --git a/renderer/waveshare/__init__.py b/renderer/waveshare/__init__.py new file mode 100644 index 0000000..fedb056 --- /dev/null +++ b/renderer/waveshare/__init__.py @@ -0,0 +1,2 @@ +# Waveshare 12.48" driver — vendored from +# https://github.com/waveshareteam/e-Paper. See README.md in this dir. diff --git a/renderer/waveshare/epd12in48.py b/renderer/waveshare/epd12in48.py new file mode 100644 index 0000000..afb78ee --- /dev/null +++ b/renderer/waveshare/epd12in48.py @@ -0,0 +1,398 @@ +# /***************************************************************************** +# * | File : epd12in48.py +# * | Author : Waveshare electrices +# * | Function : Hardware underlying interface +# * | Info : +# *---------------- +# * | This version: V1.0 +# * | Date : 2019-11-01 +# * | Info : +# ******************************************************************************/ +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documnetation 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 +# furished 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 OR 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. +# +import time +import epdconfig + +EPD_WIDTH = 1304 +EPD_HEIGHT = 984 + +class EPD(object): + def __init__(self): + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + + self.EPD_M1_CS_PIN = epdconfig.EPD_M1_CS_PIN + self.EPD_S1_CS_PIN = epdconfig.EPD_S1_CS_PIN + self.EPD_M2_CS_PIN = epdconfig.EPD_M2_CS_PIN + self.EPD_S2_CS_PIN = epdconfig.EPD_S2_CS_PIN + + self.EPD_M1S1_DC_PIN = epdconfig.EPD_M1S1_DC_PIN + self.EPD_M2S2_DC_PIN = epdconfig.EPD_M2S2_DC_PIN + + self.EPD_M1S1_RST_PIN = epdconfig.EPD_M1S1_RST_PIN + self.EPD_M2S2_RST_PIN = epdconfig.EPD_M2S2_RST_PIN + + self.EPD_M1_BUSY_PIN = epdconfig.EPD_M1_BUSY_PIN + self.EPD_S1_BUSY_PIN = epdconfig.EPD_S1_BUSY_PIN + self.EPD_M2_BUSY_PIN = epdconfig.EPD_M2_BUSY_PIN + self.EPD_S2_BUSY_PIN = epdconfig.EPD_S2_BUSY_PIN + + def Init(self): + print("EPD init...") + epdconfig.module_init() + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + self.Reset() + + #panel setting + self.M1_SendCommand(0x00) + self.M1_SendData(0x1f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f + self.S1_SendCommand(0x00) + self.S1_SendData(0x1f) + self.M2_SendCommand(0x00) + self.M2_SendData(0x13) + self.S2_SendCommand(0x00) + self.S2_SendData(0x13) + + # booster soft start + self.M1_SendCommand(0x06) + self.M1_SendData(0x17) #A + self.M1_SendData(0x17) #B + self.M1_SendData(0x39) #C + self.M1_SendData(0x17) + self.M2_SendCommand(0x06) + self.M2_SendData(0x17) + self.M2_SendData(0x17) + self.M2_SendData(0x39) + self.M2_SendData(0x17) + + #resolution setting + self.M1_SendCommand(0x61) + self.M1_SendData(0x02) + self.M1_SendData(0x88) #source 648 + self.M1_SendData(0x01) #gate 492 + self.M1_SendData(0xEC) + self.S1_SendCommand(0x61) + self.S1_SendData(0x02) + self.S1_SendData(0x90) #source 656 + self.S1_SendData(0x01) #gate 492 + self.S1_SendData(0xEC) + self.M2_SendCommand(0x61) + self.M2_SendData(0x02) + self.M2_SendData(0x90) #source 656 + self.M2_SendData(0x01) #gate 492 + self.M2_SendData(0xEC) + self.S2_SendCommand(0x61) + self.S2_SendData(0x02) + self.S2_SendData(0x88) #source 648 + self.S2_SendData(0x01) #gate 492 + self.S2_SendData(0xEC) + + self.M1S1M2S2_SendCommand(0x15) #DUSPI + self.M1S1M2S2_SendData(0x20) + + self.M1S1M2S2_SendCommand(0x50) #Vcom and data interval setting + self.M1S1M2S2_SendData(0x21) #Border KW + self.M1S1M2S2_SendData(0x07) + + self.M1S1M2S2_SendCommand(0x60) #TCON + self.M1S1M2S2_SendData(0x22) + + self.M1S1M2S2_SendCommand(0xE3) + self.M1S1M2S2_SendData(0x00) + + #temperature + temp = self.M1_ReadTemperature() + + self.M1S1M2S2_SendCommand(0xe0) #Cascade setting + self.M1S1M2S2_SendData(0x03) + self.M1S1M2S2_SendCommand(0xe5) #Force temperature + self.M1S1M2S2_SendData(temp) + + def display(self, Image): + start = time.clock() + buf = [0x00] * int(self.width * self.height / 8) + image_monocolor = Image.convert('1') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + temp=0; + for y in range(0, imheight): + for x in range(0, imwidth): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] < 127: # black + buf[int((x + y*self.width)/8)] &= ~(0x80>>temp) + else: # white + buf[int((x + y*self.width)/8)] |= (0x80>>temp) + temp=temp+1 + if(temp==8): + temp=0 + + #M1 part 648*492 + self.M1_SendCommand(0x13) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(buf[y*163 + x]) + + #S1 part 656*492 + self.S1_SendCommand(0x13) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(buf[y*163 + x]) + + #M2 part 656*492 + self.M2_SendCommand(0x13) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(buf[y*163 + x]) + + #S2 part 648*492 + self.S2_SendCommand(0x13) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(buf[y*163 + x]) + + end = time.clock() + print("use time:%f"%(end - start)) + self.TurnOnDisplay() + + def clear(self): + """Clear contents of image buffer""" + start = time.clock() + self.M1_SendCommand(0x13) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(0xff) + + self.S1_SendCommand(0x13) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(0xff) + + self.M2_SendCommand(0x13) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(0xff) + + self.S2_SendCommand(0x13) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(0xff) + end = time.clock() + print("use time:%f"%(end - start)) + self.TurnOnDisplay() + + """ M1S1M2S2 Write register address and data """ + def M1S1M2S2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + def M1S1M2S2_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + """ M1M2 Write register address and data """ + def M1M2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + def M1S1M2S2_Senddata(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + """ S2 Write register address and data """ + def S2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + + def S2_SendData(self, val): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + """ M2 Write register address and data """ + def M2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + def M2_SendData(self, val): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + """ S1 Write register address and data """ + def S1_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + + def S1_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + + """ M1 Write register address and data """ + def M1_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + + def M1_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + + def Reset(self): + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1) + time.sleep(0.2) + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 0) + time.sleep(0.01) + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1) + time.sleep(0.2) + + def EPD_Sleep(self): + self.M1S1M2S2_SendCommand(0X02) + time.sleep(0.3) + + self.M1S1M2S2_SendCommand(0X07) + self.M1S1M2S2_SendData(0xA5) + time.sleep(0.3) + print("module_exit") + epdconfig.module_exit() + + + def TurnOnDisplay(self): + self.M1M2_SendCommand(0x04) + time.sleep(0.3) + self.M1S1M2S2_SendCommand(0x12) + self.M1_ReadBusy() + self.S1_ReadBusy() + self.M2_ReadBusy() + self.S2_ReadBusy() + + #Busy + def M1_ReadBusy(self): + self.M1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN) + busy = not(busy & 0x01) + print("M1_ReadBusy") + while(busy): + self.M1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + + def M2_ReadBusy(self): + self.M2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN) + busy = not(busy & 0x01) + print("M2_ReadBusy") + while(busy): + self.M2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN) + busy =not(busy & 0x01) + time.sleep(0.2) + + def S1_ReadBusy(self): + self.S1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN) + busy = not(busy & 0x01) + print("s1_ReadBusy") + while(busy): + self.S1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + + def S2_ReadBusy(self): + self.S2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN) + busy = not(busy & 0x01) + print("S2_ReadBusy") + while(busy): + self.S2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + + def M1_ReadTemperature(self): + self.M1_SendCommand(0x40) + self.M1_ReadBusy() + time.sleep(0.3) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + time.sleep(0.01) + + # temp = epdconfig.spi_readbyte(0x00) + temp = 25 + print("Read Temperature Reg:%d"%temp) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + # temp =0x29 + return temp diff --git a/renderer/waveshare/epd12in48b.py b/renderer/waveshare/epd12in48b.py new file mode 100644 index 0000000..662adb9 --- /dev/null +++ b/renderer/waveshare/epd12in48b.py @@ -0,0 +1,525 @@ +# /***************************************************************************** +# * | File : epd12in48.py +# * | Author : Waveshare electrices +# * | Function : Hardware underlying interface +# * | Info : +# *---------------- +# * | This version: V1.0 +# * | Date : 2019-11-01 +# * | Info : +# ******************************************************************************/ +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documnetation 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 +# furished 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 OR 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. +# +import time +import epdconfig + +EPD_WIDTH = 1304 +EPD_HEIGHT = 984 + +class EPD(object): + def __init__(self): + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + + self.EPD_M1_CS_PIN = epdconfig.EPD_M1_CS_PIN + self.EPD_S1_CS_PIN = epdconfig.EPD_S1_CS_PIN + self.EPD_M2_CS_PIN = epdconfig.EPD_M2_CS_PIN + self.EPD_S2_CS_PIN = epdconfig.EPD_S2_CS_PIN + + self.EPD_M1S1_DC_PIN = epdconfig.EPD_M1S1_DC_PIN + self.EPD_M2S2_DC_PIN = epdconfig.EPD_M2S2_DC_PIN + + self.EPD_M1S1_RST_PIN = epdconfig.EPD_M1S1_RST_PIN + self.EPD_M2S2_RST_PIN = epdconfig.EPD_M2S2_RST_PIN + + self.EPD_M1_BUSY_PIN = epdconfig.EPD_M1_BUSY_PIN + self.EPD_S1_BUSY_PIN = epdconfig.EPD_S1_BUSY_PIN + self.EPD_M2_BUSY_PIN = epdconfig.EPD_M2_BUSY_PIN + self.EPD_S2_BUSY_PIN = epdconfig.EPD_S2_BUSY_PIN + + def Init(self): + print("EPD init...") + epdconfig.module_init() + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + self.Reset() + + #panel setting + self.M1_SendCommand(0x00) + self.M1_SendData(0x2f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f + self.S1_SendCommand(0x00) + self.S1_SendData(0x2f) + self.M2_SendCommand(0x00) + self.M2_SendData(0x23) + self.S2_SendCommand(0x00) + self.S2_SendData(0x23) + + # POWER SETTING + self.M1_SendCommand(0x01) + self.M1_SendData(0x07) + self.M1_SendData(0x17) # VGH=20V,VGL=-20V + self.M1_SendData(0x3F) # VDH=15V + self.M1_SendData(0x3F) # VDL=-15V + self.M1_SendData(0x0d) + self.M2_SendCommand(0x01) + self.M2_SendData(0x07) + self.M2_SendData(0x17) # VGH=20V,VGL=-20V + self.M2_SendData(0x3F) # VDH=15V + self.M2_SendData(0x3F) # VDL=-15V + self.M2_SendData(0x0d) + + # booster soft start + self.M1_SendCommand(0x06) + self.M1_SendData(0x17) #A + self.M1_SendData(0x17) #B + self.M1_SendData(0x39) #C + self.M1_SendData(0x17) + self.M2_SendCommand(0x06) + self.M2_SendData(0x17) + self.M2_SendData(0x17) + self.M2_SendData(0x39) + self.M2_SendData(0x17) + + #resolution setting + self.M1_SendCommand(0x61) + self.M1_SendData(0x02) + self.M1_SendData(0x88) #source 648 + self.M1_SendData(0x01) #gate 492 + self.M1_SendData(0xEC) + self.S1_SendCommand(0x61) + self.S1_SendData(0x02) + self.S1_SendData(0x90) #source 656 + self.S1_SendData(0x01) #gate 492 + self.S1_SendData(0xEC) + self.M2_SendCommand(0x61) + self.M2_SendData(0x02) + self.M2_SendData(0x90) #source 656 + self.M2_SendData(0x01) #gate 492 + self.M2_SendData(0xEC) + self.S2_SendCommand(0x61) + self.S2_SendData(0x02) + self.S2_SendData(0x88) #source 648 + self.S2_SendData(0x01) #gate 492 + self.S2_SendData(0xEC) + + self.M1S1M2S2_SendCommand(0x15) #DUSPI + self.M1S1M2S2_SendData(0x20) + + self.M1S1M2S2_SendCommand(0x30) # PLL + self.M1S1M2S2_SendData(0x08) + + self.M1S1M2S2_SendCommand(0x50) #Vcom and data interval setting + self.M1S1M2S2_SendData(0x31) + self.M1S1M2S2_SendData(0x07) + + self.M1S1M2S2_SendCommand(0x60)#TCON + self.M1S1M2S2_SendData(0x22) + + self.M1_SendCommand(0xE0) #POWER SETTING + self.M1_SendData(0x01) + self.M2_SendCommand(0xE0) #POWER SETTING + self.M2_SendData(0x01) + + self.M1S1M2S2_SendCommand(0xE3) + self.M1S1M2S2_SendData(0x00) + + self.M1_SendCommand(0x82) + self.M1_SendData(0x1c) + self.M2_SendCommand(0x82) + self.M2_SendData(0x1c) + + self.SetLut() + + def display(self, BlackImage, RedImage): + start = time.clock() + + Blackbuf = [0x00] * int(self.width * self.height / 8) + blackconvert = BlackImage.convert('1') + bimwidth, bimheight = blackconvert.size + Blackpixles = blackconvert.load() + temp=0; + for y in range(0, bimheight): + for x in range(0, bimwidth): + if Blackpixles[x, y] < 127: # black + Blackbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp) + else: # white + Blackbuf[int((x + y*self.width)/8)] |= (0x80>>temp) + temp=temp+1 + if(temp==8): + temp=0 + + Redbuf = [0x00] * int(self.width * self.height / 8) + redconvert = RedImage.convert('1') + rimwidth, rimheight = redconvert.size + Redpixles = redconvert.load() + temp=0; + for y in range(0, rimheight): + for x in range(0, rimwidth): + if Redpixles[x, y] < 127: # black + Redbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp) + else: # white + Redbuf[int((x + y*self.width)/8)] |= (0x80>>temp) + temp=temp+1 + if(temp==8): + temp=0 + + #S2 part 648*492 + self.S2_SendCommand(0x10) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(Blackbuf[y*163 + x]) + self.S2_SendCommand(0x13) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(~Redbuf[y*163 + x]) + + #M2 part 656*492 + self.M2_SendCommand(0x10) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(Blackbuf[y*163 + x]) + self.M2_SendCommand(0x13) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(~Redbuf[y*163 + x]) + + #M1 part 648*492 + self.M1_SendCommand(0x10) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(Blackbuf[y*163 + x]) + self.M1_SendCommand(0x13) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(~Redbuf[y*163 + x]) + + #S1 part 656*492 + self.S1_SendCommand(0x10) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(Blackbuf[y*163 + x]) + self.S1_SendCommand(0x13) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(~Redbuf[y*163 + x]) + + end = time.clock() + print("use time: %f"%(end - start)) + self.TurnOnDisplay() + + def clear(self): + """Clear contents of image buffer""" + start = time.clock() + + self.S2_SendCommand(0x10) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(0xff) + self.S2_SendCommand(0x13) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(0x00) + + self.M2_SendCommand(0x10) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(0xff) + self.M2_SendCommand(0x13) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(0x00) + + self.M1_SendCommand(0x10) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(0xff) + self.M1_SendCommand(0x13) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(0x00) + + self.S1_SendCommand(0x10) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(0xff) + self.S1_SendCommand(0x13) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(0x00) + + end = time.clock() + print (end) + print (start) + print("use time: %f" %(end - start)) + + self.TurnOnDisplay() + + def Reset(self): + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1) + time.sleep(0.2) + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 0) + time.sleep(0.01) + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1) + time.sleep(0.2) + + def EPD_Sleep(self): + self.M1S1M2S2_SendCommand(0X02) + time.sleep(0.3) + + self.M1S1M2S2_SendCommand(0X07) + self.M1S1M2S2_SendData(0xA5) + time.sleep(0.3) + print("module_exit") + epdconfig.module_exit() + + def TurnOnDisplay(self): + self.M1M2_SendCommand(0x04) + time.sleep(0.3) + self.M1S1M2S2_SendCommand(0x12) + self.M1_ReadBusy() + self.S1_ReadBusy() + self.M2_ReadBusy() + self.S2_ReadBusy() + + """ M1S1M2S2 Write register address and data """ + def M1S1M2S2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + def M1S1M2S2_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + """ M1M2 Write register address and data """ + def M1M2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + def M1M2_Sendata(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + """ S2 Write register address and data """ + def S2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + def S2_SendData(self, val): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + """ M2 Write register address and data """ + def M2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + def M2_SendData(self, val): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + """ S1 Write register address and data """ + def S1_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + def S1_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + + """ M1 Write register address and data """ + def M1_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + def M1_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + + #Busy + def M1_ReadBusy(self): + self.M1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN) + busy = not(busy & 0x01) + while(busy): + self.M1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + def M2_ReadBusy(self): + self.M2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN) + busy = not(busy & 0x01) + self.M2_SendCommand(0x71) + while(busy): + self.M2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN) + busy =not(busy & 0x01) + time.sleep(0.2) + def S1_ReadBusy(self): + self.S1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN) + busy = not(busy & 0x01) + while(busy): + self.S1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + def S2_ReadBusy(self): + self.S2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN) + busy = not(busy & 0x01) + while(busy): + self.S2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + + lut_vcom1 = [ + 0x00, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x00, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x00, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x00, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_ww1 = [ + 0x91, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x04, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x80, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x08, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_bw1 = [ + 0xA8, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x84, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x86, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0xF0, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_wb1 = [ + 0x91, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x04, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x80, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x08, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_bb1 = [ + 0x92, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x80, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x04, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x01, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + + def SetLut(self): + self.M1S1M2S2_SendCommand(0x20) #vcom + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_vcom1[count]) + + self.M1S1M2S2_SendCommand(0x21) #red not use + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_ww1[count]) + + self.M1S1M2S2_SendCommand(0x22) #bw r + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_bw1[count]) # bw=r + + self.M1S1M2S2_SendCommand(0x23) #wb w + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_wb1[count]) # wb=w + + self.M1S1M2S2_SendCommand(0x24) #bb b + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_bb1[count]) # bb=b + + self.M1S1M2S2_SendCommand(0x25) #bb b + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_ww1[count]) # bb=b diff --git a/renderer/waveshare/epd12in48b_V2.py b/renderer/waveshare/epd12in48b_V2.py new file mode 100644 index 0000000..ecff7e7 --- /dev/null +++ b/renderer/waveshare/epd12in48b_V2.py @@ -0,0 +1,529 @@ +# /***************************************************************************** +# * | File : epd12in48b_V2.py +# * | Author : Waveshare electrices +# * | Function : Hardware underlying interface +# * | Info : +# *---------------- +# * | This version: V1.0 +# * | Date : 2022-09-14 +# * | Info : +# ******************************************************************************/ +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documnetation 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 +# furished 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 OR 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. +# +import time +import epdconfig + +EPD_WIDTH = 1304 +EPD_HEIGHT = 984 + +class EPD(object): + def __init__(self): + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + + self.EPD_M1_CS_PIN = epdconfig.EPD_M1_CS_PIN + self.EPD_S1_CS_PIN = epdconfig.EPD_S1_CS_PIN + self.EPD_M2_CS_PIN = epdconfig.EPD_M2_CS_PIN + self.EPD_S2_CS_PIN = epdconfig.EPD_S2_CS_PIN + + self.EPD_M1S1_DC_PIN = epdconfig.EPD_M1S1_DC_PIN + self.EPD_M2S2_DC_PIN = epdconfig.EPD_M2S2_DC_PIN + + self.EPD_M1S1_RST_PIN = epdconfig.EPD_M1S1_RST_PIN + self.EPD_M2S2_RST_PIN = epdconfig.EPD_M2S2_RST_PIN + + self.EPD_M1_BUSY_PIN = epdconfig.EPD_M1_BUSY_PIN + self.EPD_S1_BUSY_PIN = epdconfig.EPD_S1_BUSY_PIN + self.EPD_M2_BUSY_PIN = epdconfig.EPD_M2_BUSY_PIN + self.EPD_S2_BUSY_PIN = epdconfig.EPD_S2_BUSY_PIN + + def Init(self): + print("EPD init...") + epdconfig.module_init() + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + self.Reset() + + # panel setting for Clear + # self.M1_SendCommand(0x00) + # self.M1_SendData(0x07) #KW-3f KWR-2F BWROTP 0f BWOTP 1f + # self.S1_SendCommand(0x00) + # self.S1_SendData(0x07) + # self.M2_SendCommand(0x00) + # self.M2_SendData(0x07) + # self.S2_SendCommand(0x00) + # self.S2_SendData(0x07) + + # panel setting for Display + self.M1_SendCommand(0x00) + self.M1_SendData(0x0f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f + self.S1_SendCommand(0x00) + self.S1_SendData(0x0f) + self.M2_SendCommand(0x00) + self.M2_SendData(0x03) + self.S2_SendCommand(0x00) + self.S2_SendData(0x03) + + # booster soft start + self.M1_SendCommand(0x06) + self.M1_SendData(0x17) #A + self.M1_SendData(0x17) #B + self.M1_SendData(0x39) #C + self.M1_SendData(0x17) + self.M2_SendCommand(0x06) + self.M2_SendData(0x17) + self.M2_SendData(0x17) + self.M2_SendData(0x39) + self.M2_SendData(0x17) + + #resolution setting + self.M1_SendCommand(0x61) + self.M1_SendData(0x02) + self.M1_SendData(0x88) #source 648 + self.M1_SendData(0x01) #gate 492 + self.M1_SendData(0xEC) + self.S1_SendCommand(0x61) + self.S1_SendData(0x02) + self.S1_SendData(0x90) #source 656 + self.S1_SendData(0x01) #gate 492 + self.S1_SendData(0xEC) + self.M2_SendCommand(0x61) + self.M2_SendData(0x02) + self.M2_SendData(0x90) #source 656 + self.M2_SendData(0x01) #gate 492 + self.M2_SendData(0xEC) + self.S2_SendCommand(0x61) + self.S2_SendData(0x02) + self.S2_SendData(0x88) #source 648 + self.S2_SendData(0x01) #gate 492 + self.S2_SendData(0xEC) + + self.M1S1M2S2_SendCommand(0x15) #DUSPI + self.M1S1M2S2_SendData(0x20) + + self.M1S1M2S2_SendCommand(0x50) #Vcom and data interval setting + self.M1S1M2S2_SendData(0x11) + self.M1S1M2S2_SendData(0x07) + + self.M1S1M2S2_SendCommand(0x60)#TCON + self.M1S1M2S2_SendData(0x22) + + self.M1S1M2S2_SendCommand(0xE3) + self.M1S1M2S2_SendData(0x00) + + self.M1_ReadTemperature() + + def display(self, BlackImage, RedImage): + start = time.perf_counter() + + Blackbuf = [0x00] * int(self.width * self.height / 8) + blackconvert = BlackImage.convert('1') + bimwidth, bimheight = blackconvert.size + Blackpixles = blackconvert.load() + temp=0 + for y in range(0, bimheight): + for x in range(0, bimwidth): + if Blackpixles[x, y] < 127: # black + Blackbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp) + else: # white + Blackbuf[int((x + y*self.width)/8)] |= (0x80>>temp) + temp=temp+1 + if(temp==8): + temp=0 + + Redbuf = [0x00] * int(self.width * self.height / 8) + redconvert = RedImage.convert('1') + rimwidth, rimheight = redconvert.size + Redpixles = redconvert.load() + temp=0 + for y in range(0, rimheight): + for x in range(0, rimwidth): + if Redpixles[x, y] < 127: # black + Redbuf[int((x + y*self.width)/8)] &= ~(0x80>>temp) + else: # white + Redbuf[int((x + y*self.width)/8)] |= (0x80>>temp) + temp=temp+1 + if(temp==8): + temp=0 + + #S2 part 648*492 + self.S2_SendCommand(0x10) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(Blackbuf[y*163 + x]) + self.S2_SendCommand(0x13) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(~Redbuf[y*163 + x]) + + #M2 part 656*492 + self.M2_SendCommand(0x10) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(Blackbuf[y*163 + x]) + self.M2_SendCommand(0x13) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(~Redbuf[y*163 + x]) + + #M1 part 648*492 + self.M1_SendCommand(0x10) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(Blackbuf[y*163 + x]) + self.M1_SendCommand(0x13) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(~Redbuf[y*163 + x]) + + #S1 part 656*492 + self.S1_SendCommand(0x10) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(Blackbuf[y*163 + x]) + self.S1_SendCommand(0x13) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(~Redbuf[y*163 + x]) + + end = time.perf_counter() + print("use time: %f"%(end - start)) + self.TurnOnDisplay() + + def clear(self): + """Clear contents of image buffer""" + start = time.perf_counter() + + self.S2_SendCommand(0x10) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(0xff) + self.S2_SendCommand(0x13) + for y in range(0, 492): + for x in range(0, 81): + self.S2_SendData(0x00) + + self.M2_SendCommand(0x10) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(0xff) + self.M2_SendCommand(0x13) + for y in range(0, 492): + for x in range(81, 163): + self.M2_SendData(0x00) + + self.M1_SendCommand(0x10) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(0xff) + self.M1_SendCommand(0x13) + for y in range(492, 984): + for x in range(0, 81): + self.M1_SendData(0x00) + + self.S1_SendCommand(0x10) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(0xff) + self.S1_SendCommand(0x13) + for y in range(492, 984): + for x in range(81, 163): + self.S1_SendData(0x00) + + end = time.perf_counter() + print (end) + print (start) + print("use time: %f" %(end - start)) + + self.TurnOnDisplay() + + def Reset(self): + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1) + time.sleep(0.2) + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 0) + time.sleep(0.01) + epdconfig.digital_write(self.EPD_M1S1_RST_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_RST_PIN, 1) + time.sleep(0.2) + + def EPD_Sleep(self): + self.M1S1M2S2_SendCommand(0X02) + time.sleep(0.3) + + self.M1S1M2S2_SendCommand(0X07) + self.M1S1M2S2_SendData(0xA5) + time.sleep(0.3) + print("module_exit") + epdconfig.module_exit() + + def TurnOnDisplay(self): + self.M1M2_SendCommand(0x04) + time.sleep(0.3) + self.M1S1M2S2_SendCommand(0x12) + self.M1_ReadBusy() + self.S1_ReadBusy() + self.M2_ReadBusy() + self.S2_ReadBusy() + + """ M1S1M2S2 Write register address and data """ + def M1S1M2S2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + def M1S1M2S2_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + """ M1M2 Write register address and data """ + def M1M2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + def M1M2_Sendata(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + """ S2 Write register address and data """ + def S2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + def S2_SendData(self, val): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + """ M2 Write register address and data """ + def M2_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + def M2_SendData(self, val): + epdconfig.digital_write(self.EPD_M2S2_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + + """ S1 Write register address and data """ + def S1_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + def S1_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + + """ M1 Write register address and data """ + def M1_SendCommand(self, cmd): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 0) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.spi_writebyte(cmd) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + def M1_SendData(self, val): + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.spi_writebyte(val) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + + #Busy + def M1_ReadBusy(self): + self.M1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN) + busy = not(busy & 0x01) + while(busy): + self.M1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M1_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + def M2_ReadBusy(self): + self.M2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN) + busy = not(busy & 0x01) + self.M2_SendCommand(0x71) + while(busy): + self.M2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_M2_BUSY_PIN) + busy =not(busy & 0x01) + time.sleep(0.2) + def S1_ReadBusy(self): + self.S1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN) + busy = not(busy & 0x01) + while(busy): + self.S1_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S1_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + def S2_ReadBusy(self): + self.S2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN) + busy = not(busy & 0x01) + while(busy): + self.S2_SendCommand(0x71) + busy = epdconfig.digital_read(self.EPD_S2_BUSY_PIN) + busy = not(busy & 0x01) + time.sleep(0.2) + + lut_vcom1 = [ + 0x00, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x00, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x00, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x00, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_ww1 = [ + 0x91, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x04, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x80, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x08, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_bw1 = [ + 0xA8, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x84, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x86, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x8C, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0xF0, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_wb1 = [ + 0x91, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x04, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x80, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x08, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + lut_bb1 = [ + 0x92, 0x10, 0x10, 0x01, 0x08, 0x01, + 0x80, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x84, 0x08, 0x01, 0x08, 0x01, 0x06, + 0x04, 0x06, 0x01, 0x06, 0x01, 0x05, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x06, + 0x00, 0x05, 0x01, 0x1E, 0x0F, 0x01, + 0x01, 0x04, 0x05, 0x08, 0x08, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + + def SetLut(self): + self.M1S1M2S2_SendCommand(0x20) #vcom + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_vcom1[count]) + + self.M1S1M2S2_SendCommand(0x21) #red not use + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_ww1[count]) + + self.M1S1M2S2_SendCommand(0x22) #bw r + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_bw1[count]) # bw=r + + self.M1S1M2S2_SendCommand(0x23) #wb w + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_wb1[count]) # wb=w + + self.M1S1M2S2_SendCommand(0x24) #bb b + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_bb1[count]) # bb=b + + self.M1S1M2S2_SendCommand(0x25) #bb b + for count in range(0, 60): + self.M1S1M2S2_SendData(self.lut_ww1[count]) # bb=b + + def M1_ReadTemperature(self): + self.M1_SendCommand(0x40) + self.M1_ReadBusy() + time.sleep(0.3) + + epdconfig.digital_write(self.EPD_M1_CS_PIN, 0) + epdconfig.digital_write(self.EPD_S1_CS_PIN, 1) + epdconfig.digital_write(self.EPD_M2_CS_PIN, 1) + epdconfig.digital_write(self.EPD_S2_CS_PIN, 1) + + epdconfig.digital_write(self.EPD_M1S1_DC_PIN, 1) + time.sleep(0.05) + + temp = epdconfig.spi_readbyte(0x00) + epdconfig.digital_write(self.EPD_M1_CS_PIN, 1) + + self.M1S1M2S2_SendCommand(0xE0) + self.M1S1M2S2_SendData(0x03) + self.M1S1M2S2_SendCommand(0xE5) + self.M1S1M2S2_SendData(temp) diff --git a/renderer/waveshare/epdconfig.py b/renderer/waveshare/epdconfig.py new file mode 100644 index 0000000..cfa6e4d --- /dev/null +++ b/renderer/waveshare/epdconfig.py @@ -0,0 +1,99 @@ +# /***************************************************************************** +# * | File : epdconfig.py +# * | Author : Waveshare electrices +# * | Function : Hardware underlying interface +# * | Info : +# *---------------- +# * | This version: V1.0 +# * | Date : 2019-11-01 +# * | Info : +# ******************************************************************************/ +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documnetation 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 +# furished 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 OR 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. +# +import time +import os +import logging +import sys + +from ctypes import * + +EPD_SCK_PIN =11 +EPD_MOSI_PIN =10 + +EPD_M1_CS_PIN =8 +EPD_S1_CS_PIN =7 +EPD_M2_CS_PIN =17 +EPD_S2_CS_PIN =18 + +EPD_M1S1_DC_PIN =13 +EPD_M2S2_DC_PIN =22 + +EPD_M1S1_RST_PIN =6 +EPD_M2S2_RST_PIN =23 + +EPD_M1_BUSY_PIN =5 +EPD_S1_BUSY_PIN =19 +EPD_M2_BUSY_PIN =27 +EPD_S2_BUSY_PIN =24 + +find_dirs = [ + os.path.dirname(os.path.realpath(__file__)), + '/usr/local/lib', + '/usr/lib', +] +spi = None +for find_dir in find_dirs: + val = int(os.popen('getconf LONG_BIT').read()) + logging.debug("System is %d bit"%val) + if val == 64: + so_filename = os.path.join(find_dir, 'DEV_Config_64.so') + else: + so_filename = os.path.join(find_dir, 'DEV_Config_32.so') + if os.path.exists(so_filename): + spi = CDLL(so_filename) + break +if spi is None: + RuntimeError('Cannot find DEV_Config.so') + +def digital_write(pin, value): + spi.DEV_Digital_Write(pin, value) + +def digital_read(pin): + return spi.DEV_Digital_Read(pin) + +def spi_writebyte(value): + spi.DEV_SPI_WriteByte(value) + +def delay_ms(delaytime): + time.sleep(delaytime / 1000.0) + +def module_init(): + spi.DEV_ModuleInit() + +def module_exit(): + spi.DEV_ModuleExit() + + +def spi_readbyte(Reg): + return spi.DEV_SPI_ReadByte(Reg) + +def delay_ms(delaytime): + time.sleep(delaytime / 1000.0) + + \ No newline at end of file