Skip to content

Commit c2d9903

Browse files
committed
feat(simulator): enable offline PWA support with manifest and coi service worker
1 parent 1225e16 commit c2d9903

7 files changed

Lines changed: 151 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ What follows is the mechanics.
1313
| Path | Role |
1414
|---|---|
1515
| `index.html` | The portal page. The regions between `<!-- PYDEVICES-…: START/END -->` markers are generated — edit the database, not the markup. |
16+
| `simulator/` | Interactive browser-based Python playground PWA with Monaco editor, canvas emulation, and offline caching. |
1617
| `assets/img/` | Brand marks, synced from `dotgithub/assets/img/`: `logo.svg`, `logo-512.png`, `logo-avatar.png`. |
1718
| `assets/chrome/` | Shared first-party chrome, synced from `dotgithub/assets/`: `site.css`, `site-chrome.js`, `theme-toggle.js`, `hero-runtime.js`. |
1819
| `assets/apps/` | Interactive pure-Python apps, synced from `dotgithub/assets/apps/`. |
19-
| `vendor/` | True third-party dependencies (`micropython`, `xterm`). |
20+
| `vendor/` | Centralized third-party runtimes and WebAssembly interpreters (`/vendor/micropython/`, `xterm`). |
2021
| `.nojekyll` | Bypasses Jekyll on GitHub Pages. |
2122

2223
## Regenerating

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ <h3>lvgl-circuitpython</h3>
178178

179179
<!-- Tier 4 Section -->
180180
<div class="section-head" style="margin-top: 40px;">
181-
<h2>4: Target App Hosts & PWA Templates</h2>
181+
<h2>4: App Hosts & Mobile</h2>
182182
<span class="hint">Deploy PyDevices apps directly to desktop browsers or mobile APKs.</span>
183183
</div>
184184
<div class="grid">

simulator/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# PyDevices Simulator
2+
3+
Interactive browser-based Python device playground for LVGL, pdwidgets, pygraphics, and displaydev.
4+
5+
Live at <https://pydevices.github.io/simulator/>.
6+
7+
## Architecture
8+
9+
- **Editor**: Monaco Editor with Python syntax highlighting and LZ-String compressed URL hash sharing.
10+
- **Runtimes**:
11+
- **MicroPython WebAssembly**: Pre-compiled binary (`/vendor/micropython/micropython.mjs`) containing built-in `displaydev`, `multimer`, `appdev`, `board_config`, `lvgl`, `pydevices-lvgl`, `display_driver`, `palettes`, `pdwidgets`, `pygraphics`, and `usdl2`.
12+
- **Pyodide (CPython WASM)**: Python 3.12+ in the browser with dynamic wheel loading for companion packages.
13+
- **Display Emulation**: Direct HTML5 Canvas binding with configurable resolution and shape presets (round watches, rectangular displays, landscape panels).
14+
- **Progressive Web App (PWA)**: Includes `manifest.json` and `sw.js` with Cross-Origin-Isolation (`COOP`/`COEP`/`CORP`) headers, enabling offline installation and execution on desktop and mobile browsers.

simulator/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<title>PyDevices Simulator — Interactive Python Device Playground</title>
88
<meta name="description" content="Interactive browser-based Python playground for LVGL, pdwidgets, pygraphics, and displaydev on MicroPython and Pyodide.">
99
<link rel="icon" type="image/svg+xml" href="/assets/img/logo.svg">
10+
<link rel="manifest" href="manifest.json">
11+
<meta name="theme-color" content="#100e0b">
12+
<link rel="apple-touch-icon" href="/assets/img/logo-512.png">
1013
<link rel="preconnect" href="https://fonts.googleapis.com">
1114
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1215
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap">

simulator/manifest.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "PyDevices Simulator",
3+
"short_name": "Simulator",
4+
"description": "Interactive Python Device Playground for LVGL, pdwidgets, pygraphics, and displaydev",
5+
"start_url": "./",
6+
"scope": "./",
7+
"display": "standalone",
8+
"orientation": "any",
9+
"theme_color": "#100e0b",
10+
"background_color": "#100e0b",
11+
"icons": [
12+
{
13+
"src": "/assets/img/logo-512.png",
14+
"sizes": "512x512",
15+
"type": "image/png",
16+
"purpose": "any maskable"
17+
},
18+
{
19+
"src": "/assets/img/logo-avatar.png",
20+
"sizes": "192x192",
21+
"type": "image/png",
22+
"purpose": "any maskable"
23+
},
24+
{
25+
"src": "/assets/img/logo.svg",
26+
"sizes": "any",
27+
"type": "image/svg+xml",
28+
"purpose": "any"
29+
}
30+
]
31+
}

simulator/simulator.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,14 @@
498498
const { width, height, shape } = currentResolution;
499499
setCanvasResolution(width, height, shape);
500500
}
501-
syncResolutionSelect();
501+
// Register PWA Service Worker
502+
if ("serviceWorker" in navigator) {
503+
window.addEventListener("load", () => {
504+
navigator.serviceWorker.register("./sw.js").catch((err) => {
505+
console.warn("[Simulator] ServiceWorker registration failed:", err);
506+
});
507+
});
508+
}
502509

503510
// Refresh the environment, run the editor's code, then open the REPL.
504511
mountRuntime(currentRuntime());

simulator/sw.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: MIT
2+
// PyDevices Simulator PWA Service Worker with Cross-Origin Isolation (COI) support.
3+
4+
const CACHE_NAME = "pydevices-simulator-v1";
5+
6+
const PRECACHE_ASSETS = [
7+
"./",
8+
"./index.html",
9+
"./simulator.css",
10+
"./simulator.js",
11+
"./templates.js",
12+
"./micropython.json",
13+
"./pyodide.json",
14+
"./manifest.json",
15+
"/assets/img/logo.svg",
16+
"/assets/img/logo-512.png",
17+
"/assets/chrome/site.css",
18+
"/vendor/micropython/micropython.mjs",
19+
"/vendor/micropython/micropython.wasm",
20+
"https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js",
21+
"https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.js",
22+
"https://pyscript.net/releases/2024.11.1/core.css",
23+
"https://pyscript.net/releases/2024.11.1/core.js",
24+
];
25+
26+
self.addEventListener("install", (event) => {
27+
event.waitUntil(
28+
caches.open(CACHE_NAME).then((cache) => {
29+
return cache.addAll(PRECACHE_ASSETS).catch((err) => {
30+
console.warn("[Simulator SW] Precache warning:", err);
31+
});
32+
}).then(() => self.skipWaiting())
33+
);
34+
});
35+
36+
self.addEventListener("activate", (event) => {
37+
event.waitUntil(
38+
caches.keys().then((keys) => {
39+
return Promise.all(
40+
keys.map((key) => {
41+
if (key !== CACHE_NAME) {
42+
return caches.delete(key);
43+
}
44+
})
45+
);
46+
}).then(() => self.clients.claim())
47+
);
48+
});
49+
50+
function withCoiHeaders(response) {
51+
if (!response || response.status === 0) return response;
52+
const newHeaders = new Headers(response.headers);
53+
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
54+
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
55+
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
56+
57+
return new Response(response.body, {
58+
status: response.status,
59+
statusText: response.statusText,
60+
headers: newHeaders,
61+
});
62+
}
63+
64+
self.addEventListener("fetch", (event) => {
65+
const { request } = event;
66+
if (request.method !== "GET") return;
67+
68+
event.respondWith(
69+
caches.match(request).then((cached) => {
70+
if (cached) {
71+
return withCoiHeaders(cached);
72+
}
73+
74+
return fetch(request)
75+
.then((response) => {
76+
// Cache successful responses for same-origin or known CDNs
77+
if (response && response.status === 200) {
78+
const copy = response.clone();
79+
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
80+
}
81+
return withCoiHeaders(response);
82+
})
83+
.catch(() => {
84+
// If offline and request is for page, fallback to cached index.html
85+
if (request.mode === "navigate") {
86+
return caches.match("./index.html").then(withCoiHeaders);
87+
}
88+
throw new Error("Network unavailable");
89+
});
90+
})
91+
);
92+
});

0 commit comments

Comments
 (0)