Skip to content

Commit ceae91b

Browse files
committed
Filesystem and REPL improvements
1 parent ec6f067 commit ceae91b

8 files changed

Lines changed: 2363 additions & 2 deletions

File tree

ports/wasm/FILESYSTEM_README.md

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
# CircuitPython WASM Persistent Filesystem
2+
3+
This implementation provides **Option 3: Hybrid** from the WASM_PORT_ROADMAP.md - combining frozen modules with IndexedDB for persistent user files.
4+
5+
## Features
6+
7+
### ✅ Implemented
8+
9+
1. **Persistent Storage**: Files are saved to IndexedDB and survive page reloads
10+
2. **Auto-Run Workflow**: Automatic execution of `boot.py` and `code.py`
11+
3. **File Management**: Create, read, update, delete files
12+
4. **Project Export/Import**: Save and restore entire projects as JSON
13+
5. **VFS Synchronization**: Seamless integration with Emscripten's virtual filesystem
14+
15+
## Quick Start
16+
17+
### Basic Usage (Memory Only)
18+
19+
```javascript
20+
import { loadCircuitPython } from './circuitpython.mjs';
21+
22+
const ctpy = await loadCircuitPython({
23+
autoRun: true, // Auto-run boot.py and code.py
24+
verbose: true
25+
});
26+
27+
// Write code
28+
ctpy.FS.writeFile('/code.py', 'print("Hello WASM!")');
29+
30+
// Run it
31+
ctpy.runFile('/code.py');
32+
```
33+
34+
### Persistent Storage with IndexedDB
35+
36+
```javascript
37+
import { loadCircuitPython } from './circuitpython.mjs';
38+
39+
const ctpy = await loadCircuitPython({
40+
filesystem: 'indexeddb', // Enable persistent storage
41+
autoRun: true, // Auto-run boot.py and code.py
42+
verbose: true,
43+
stdout: (text) => console.log(text)
44+
});
45+
46+
// Save a file (persists across page reloads!)
47+
await ctpy.saveFile('/code.py', `
48+
import board
49+
import time
50+
51+
print("This code persists!")
52+
time.sleep(1)
53+
`);
54+
55+
// Save a library
56+
await ctpy.saveFile('/lib/mymodule.py', `
57+
def greet(name):
58+
return f"Hello, {name}!"
59+
`);
60+
61+
// Run the workflow
62+
ctpy.runWorkflow(); // Runs boot.py then code.py
63+
```
64+
65+
## API Reference
66+
67+
### loadCircuitPython(options)
68+
69+
Main initialization function with new options:
70+
71+
```typescript
72+
interface LoadOptions {
73+
// Existing options
74+
pystack?: number; // Python stack size in words (default: 2048)
75+
heapsize?: number; // Heap size in bytes (default: 1MB)
76+
url?: string; // URL to load circuitpython.mjs
77+
stdin?: Function; // Input handler
78+
stdout?: Function; // Output handler
79+
stderr?: Function; // Error output handler
80+
linebuffer?: boolean; // Buffer output by lines (default: true)
81+
verbose?: boolean; // Log initialization (default: false)
82+
83+
// NEW: Filesystem options
84+
filesystem?: 'memory' | 'indexeddb'; // Storage type (default: 'memory')
85+
autoRun?: boolean; // Auto-run boot.py and code.py (default: false)
86+
}
87+
```
88+
89+
### Returned Object Properties
90+
91+
```typescript
92+
interface CircuitPython {
93+
// Existing properties
94+
_module: EmscriptenModule;
95+
virtualClock: VirtualClock;
96+
PyProxy: typeof PyProxy;
97+
FS: EmscriptenFS;
98+
globals: {
99+
__dict__: any;
100+
get(key: string): any;
101+
set(key: string, value: any): void;
102+
delete(key: string): void;
103+
};
104+
registerJsModule(name: string, module: any): void;
105+
pyimport(name: string): any;
106+
runPython(code: string): any;
107+
runPythonAsync(code: string): Promise<any>;
108+
replInit(): void;
109+
replProcessChar(chr: number): number;
110+
replProcessCharWithAsyncify(chr: number): Promise<number>;
111+
112+
// NEW: Filesystem methods
113+
filesystem: CircuitPythonFilesystem | null; // IndexedDB filesystem (null if memory mode)
114+
runFile(filepath: string): any; // Run a Python file
115+
runWorkflow(): void; // Run boot.py then code.py
116+
saveFile(filepath: string, content: string | Uint8Array): Promise<void>; // Save to VFS + IndexedDB
117+
}
118+
```
119+
120+
### CircuitPythonFilesystem Class
121+
122+
When `filesystem: 'indexeddb'` is enabled:
123+
124+
```typescript
125+
class CircuitPythonFilesystem {
126+
// Initialize the database
127+
async init(): Promise<void>;
128+
129+
// File operations
130+
async writeFile(path: string, content: string | Uint8Array): Promise<void>;
131+
async readFile(path: string): Promise<Uint8Array>;
132+
async deleteFile(path: string): Promise<void>;
133+
async exists(path: string): Promise<boolean>;
134+
async listFiles(dirPath?: string): Promise<FileEntry[]>;
135+
136+
// VFS synchronization
137+
async syncToVFS(Module): Promise<void>; // Load files from IndexedDB to VFS
138+
async syncFromVFS(Module, paths: string[]): Promise<void>; // Save files from VFS to IndexedDB
139+
140+
// Project management
141+
async exportProject(): Promise<Blob>; // Export all files as JSON
142+
async importProject(blob: Blob): Promise<void>; // Import files from JSON
143+
async clear(): Promise<void>; // Delete all files
144+
}
145+
```
146+
147+
## File Structure in IndexedDB
148+
149+
```javascript
150+
// Database: 'circuitpython'
151+
// Object Store: 'files'
152+
// Key Path: 'path'
153+
154+
// File record:
155+
{
156+
path: '/code.py', // File path
157+
content: Uint8Array, // File content as bytes
158+
modified: 1705932845000, // Last modified timestamp
159+
size: 1024, // Size in bytes
160+
isDirectory: false // Directory flag
161+
}
162+
```
163+
164+
## Usage Examples
165+
166+
### Example 1: Simple Blink with Persistence
167+
168+
```javascript
169+
const ctpy = await loadCircuitPython({
170+
filesystem: 'indexeddb',
171+
autoRun: true,
172+
verbose: true
173+
});
174+
175+
// Save code that will run on every page load
176+
await ctpy.saveFile('/code.py', `
177+
import board
178+
import digitalio
179+
import time
180+
181+
led = digitalio.DigitalInOut(board.LED)
182+
led.direction = digitalio.Direction.OUTPUT
183+
184+
for i in range(10):
185+
led.value = not led.value
186+
time.sleep(0.5)
187+
`);
188+
189+
// Reload the page - code.py runs automatically!
190+
```
191+
192+
### Example 2: Library Management
193+
194+
```javascript
195+
const ctpy = await loadCircuitPython({
196+
filesystem: 'indexeddb',
197+
verbose: true
198+
});
199+
200+
// Install a library (persists!)
201+
await ctpy.saveFile('/lib/neopixel.py', libraryCode);
202+
203+
// Use it in your code
204+
await ctpy.saveFile('/code.py', `
205+
import neopixel
206+
import board
207+
208+
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10)
209+
pixels[0] = (255, 0, 0)
210+
pixels.show()
211+
`);
212+
213+
ctpy.runWorkflow();
214+
```
215+
216+
### Example 3: Project Export/Import
217+
218+
```javascript
219+
const ctpy = await loadCircuitPython({
220+
filesystem: 'indexeddb'
221+
});
222+
223+
// Create multiple files
224+
await ctpy.saveFile('/code.py', mainCode);
225+
await ctpy.saveFile('/boot.py', bootCode);
226+
await ctpy.saveFile('/lib/helpers.py', helperCode);
227+
228+
// Export entire project
229+
const projectBlob = await ctpy.filesystem.exportProject();
230+
231+
// Save to disk
232+
const url = URL.createObjectURL(projectBlob);
233+
const a = document.createElement('a');
234+
a.href = url;
235+
a.download = 'my-project.json';
236+
a.click();
237+
238+
// Later, import it back
239+
const fileInput = document.querySelector('input[type="file"]');
240+
fileInput.addEventListener('change', async (e) => {
241+
const file = e.target.files[0];
242+
await ctpy.filesystem.importProject(file);
243+
await ctpy.filesystem.syncToVFS(ctpy._module);
244+
ctpy.runWorkflow();
245+
});
246+
```
247+
248+
### Example 4: File Browser
249+
250+
```javascript
251+
const ctpy = await loadCircuitPython({
252+
filesystem: 'indexeddb'
253+
});
254+
255+
// List all files
256+
const files = await ctpy.filesystem.listFiles();
257+
console.table(files.map(f => ({
258+
path: f.path,
259+
size: `${(f.size / 1024).toFixed(1)} KB`,
260+
modified: new Date(f.modified).toLocaleString()
261+
})));
262+
263+
// List files in /lib
264+
const libFiles = await ctpy.filesystem.listFiles('/lib');
265+
console.log('Libraries:', libFiles.map(f => f.path));
266+
```
267+
268+
## Demo
269+
270+
See [demo_filesystem.html](./demo_filesystem.html) for a complete interactive demo with:
271+
272+
- Code editor for `code.py`
273+
- File list browser
274+
- Save/Load functionality
275+
- Run button
276+
- Project export/import
277+
- Console output
278+
279+
## Architecture
280+
281+
```
282+
┌─────────────────────────────────────────────────────┐
283+
│ Web Browser │
284+
├─────────────────────────────────────────────────────┤
285+
│ │
286+
│ ┌───────────────┐ ┌────────────────────┐ │
287+
│ │ Your App │←───────→│ CircuitPython │ │
288+
│ │ (HTML/JS) │ │ WASM Runtime │ │
289+
│ └───────────────┘ └────────────────────┘ │
290+
│ ↓ ↑ │
291+
│ ┌─────────────────────────────┐ │ │
292+
│ │ CircuitPythonFilesystem │ │ │
293+
│ │ (filesystem.js) │ │ │
294+
│ └─────────────────────────────┘ │ │
295+
│ ↓ │ │
296+
│ ┌─────────────────────────────┐ │ │
297+
│ │ IndexedDB │ │ │
298+
│ │ (Persistent Storage) │ │ │
299+
│ └─────────────────────────────┘ │ │
300+
│ │ │
301+
│ Sync on init │ │
302+
│ ─────────────────────────────┘ │
303+
│ ↓ │
304+
│ ┌────────────────────┐ │
305+
│ │ Emscripten VFS │ │
306+
│ │ (Memory Only) │ │
307+
│ └────────────────────┘ │
308+
│ │
309+
└─────────────────────────────────────────────────────┘
310+
```
311+
312+
## Workflow
313+
314+
```
315+
1. Page Load
316+
317+
2. loadCircuitPython({ filesystem: 'indexeddb' })
318+
319+
3. Initialize IndexedDB
320+
321+
4. Load files from IndexedDB → Emscripten VFS
322+
323+
5. If autoRun: true
324+
325+
6. Run boot.py (if exists)
326+
327+
7. Run code.py (if exists)
328+
329+
8. User can edit/save files
330+
331+
9. Files saved to BOTH VFS and IndexedDB
332+
333+
10. Page reload → Start from step 1 (files persist!)
334+
```
335+
336+
## Browser Compatibility
337+
338+
- ✅ Chrome/Edge (all features)
339+
- ✅ Firefox (all features)
340+
- ✅ Safari (all features)
341+
- ✅ All modern browsers with IndexedDB support
342+
343+
## Storage Limits
344+
345+
Typical browser IndexedDB limits:
346+
- **Chrome**: ~60% of free disk space
347+
- **Firefox**: ~50% of free disk space
348+
- **Safari**: ~1GB
349+
350+
Practical limits for CircuitPython projects: **50-100MB** is safe across all browsers.
351+
352+
## Future Enhancements
353+
354+
1. **Frozen Modules**: Pre-compile Adafruit libraries into WASM for instant loading
355+
2. **Auto-save**: Automatically save code on file writes from Python
356+
3. **File Watching**: Auto-reload on file changes
357+
4. **Library Manager**: Install libraries from Adafruit bundle with one click
358+
5. **Multi-tab Sync**: Share filesystem across browser tabs
359+
360+
## Related Files
361+
362+
- `api.js` - Main API with filesystem integration
363+
- `filesystem.js` - IndexedDB filesystem implementation
364+
- `demo_filesystem.html` - Interactive demo
365+
- `WASM_PORT_ROADMAP.md` - Overall roadmap and plans

0 commit comments

Comments
 (0)