Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

The `/simulator` page uses CodeMirror for editing and a locally hosted MicroPython WASM worker for simulation. It loads the saved project on startup and runs it automatically. With Auto-run enabled, source changes restart the simulation after a 650 ms pause in typing. Stop disables automatic simulation; Run, F5, or Command/Ctrl+Enter runs the current project manually.

The page shows a browser-only guide above the workspace and a physical-badge guide above the connection tools. Run and Auto-run never write to a connected badge; Upload or Install sends changes explicitly. The guides stay visible without expanding a help panel. Full screen hides the setup text to preserve editing space while keeping the connection and upload tools available.

Projects contain an `__init__.py` entry point, companion Python and text files, and binary assets. The file sidebar lets users switch between files, edit text, preview images, and add files. Each simulation stages the complete project in the virtual filesystem, sets its working directory and import path, and executes the entry point using its actual filename. A fresh worker terminates the previous program and resets its virtual state. The display receives RGBA framebuffer messages. Pointer and keyboard controls release held buttons when focus changes, and execution pauses in hidden tabs.

The Apps collection uses the same installable-app selection rules as `/apps`. `public/editor-apps/catalog.json` records the bundled public `badger/home` revision and lists every project file. Run `npm run sync:editor-apps` to refresh these apps, including modules and assets. The sync also bundles the `menu` launcher as the catalog's `launcher` entry; it is not listed as an editable app.

## About page simulator

On wide screens, the working badge sits beside the hardware specifications. Narrow screens put the badge first. `src/lib/badge-controls.ts` supplies the approved control names and SVG icons to both the About page buttons and `badge-control-symbols.astro`. The shared 3D view draws these same SVGs beside the five physical buttons and the existing Home touchpad; clicking a symbol uses its existing input bit. Both model hosts include the SVG source component, so the editor's 3D model gets the same markings without changing its separate page controls. No case-model rebuild is required.
On wide screens, the working badge sits beside the hardware specifications. Narrow screens put the badge first. `src/lib/badge-controls.ts` supplies the approved control names and SVG icons to `badge-controls.astro` and `badge-control-symbols.astro`. Both About and the Developer Simulator use the same 52-pixel page buttons, 24-pixel icons, and responsive control groups. The shared 3D view draws these same SVGs beside the five physical buttons and the existing Home touchpad; clicking a symbol uses its existing input bit. Both model hosts include the SVG source component. No case-model rebuild is required.

`/about-badge/` boots the Mona OS 4.0 menu in the same worker and renders it on the editor's 3D badge model (`initBadgeModel`). Zooming moves the orbit target toward the badge screen as the camera closes in, and back to the model centre when zoomed out. `npm run sync:editor-apps` bundles the menu from `badger/home` `badge/apps/menu` as the catalog `launcher`. Each device `.mpy` module is replaced by its `src/<module>.py` source, because the simulator cannot load `.mpy` files. `launcher.stock` lists the stock apps, taken from the menu's `anim/<app>.bin` icons. The page stages only the menu at `/system/apps/menu` and the stock apps at `/system/apps/<name>`. `src/lib/badge-firmware-compat.ts` registers stand-ins for firmware-frozen modules that are missing from the vendored runtime (`aye_arr` and `badgeware.ir`) in `sys.modules`. `public/simulator/filesystem/sparkle.py` replaces the firmware's Sparkle supervisor: `request_local_show()` wraps the next `run` call to draw the local show's bouncing Invertocat and case lights, with the Bluetooth mesh shimmed out. Three SELECT presses or the 10-minute timeout end the show and call `machine.reset()`. It also patches newer firmware APIs: `display.set_async`, `machine.reset_cause`, and the three-argument `missing_details.show`. Both this page and editor simulations use it; no extra files are staged. IR does nothing in the simulator. A Python launcher in `src/lib/badge-os-simulator.ts` replicates `badge/main.py`: it runs the menu, launches the selected app, and returns to the menu when HOME is pressed or the app calls `reset()` or fails. This loop is needed because `machine.reset()` and pin IRQs are inert in the simulator. The page shows the badge photo until the 3D model has loaded and the first frame renders. If the browser lacks module workers, WebAssembly, WebGL, or `ImageData`, or the simulator or model errors or times out, the worker stops and the photo remains.
The Examples collection includes all 36 examples from the simulator's categorized manifest at revision `eb3b3fbf27e6e61635aef5de11ab17e6303e8c20`. `public/editor-examples/` includes their original source, manifest categories and descriptions, required images and fonts, and MIT notice. Run `npm run sync:editor-examples` to restore them from that pinned revision. Update `simulatorRevision` in `scripts/editor-example-source.mjs` only when updating the bundled runtime and its provenance notices together. Opening an example creates an editable `example-<name>` app with an `__init__.py` entry point. References to bundled `/system/assets/` files become app-local asset paths, so first installs include those dependencies and avoid collisions with existing badge app names.
Expand Down
30 changes: 30 additions & 0 deletions src/components/badge-controls.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
import { badgeControls } from '@/lib/badge-controls'

interface Props {
disabled?: boolean
}

const { disabled = false } = Astro.props
const groups = [badgeControls.slice(0, 3), badgeControls.slice(3)]
---

<div class="mx-auto flex max-w-[420px] flex-wrap items-center justify-center gap-4 sm:justify-between" role="group" aria-label="Badge controls">
{groups.map(group => (
<div class="flex gap-2">
{group.map(({ icon: Icon, name, bit }) => (
<button type="button" class="badge-control" data-button={bit} aria-label={`Badge ${name} button`} title={name} disabled={disabled}>
<Icon className="h-6 w-6" aria-hidden="true" />
</button>
))}
</div>
))}
</div>

<style>
.badge-control { display: grid; width: 3.25rem; height: 3.25rem; place-items: center; border: 1px solid hsl(var(--border) / 0.4); border-radius: 0.375rem; background: transparent; color: hsl(var(--foreground)); touch-action: none; user-select: none; transition: border-color 150ms, background-color 150ms; }
.badge-control:hover:not(:disabled) { border-color: hsl(var(--primary) / 0.5); background: hsl(var(--primary) / 0.05); }
.badge-control:disabled { opacity: 0.4; }
.badge-control:focus-visible { outline: 2px solid hsl(var(--primary)); outline-offset: 2px; }
.badge-control[data-pressed='true'] { color: hsl(var(--primary)); border-color: hsl(var(--primary)); background: hsl(var(--primary) / 0.1); transform: translateY(1px); box-shadow: inset 0 1px 3px hsl(var(--border) / 0.6); }
</style>
23 changes: 3 additions & 20 deletions src/components/badge-os-simulator.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
---
import { Maximize, Minus, Plus, RotateCw, ScanSearch } from 'lucide-react'
import BadgeControlSymbols from '@/components/badge-control-symbols.astro'
import BadgeControls from '@/components/badge-controls.astro'
import BadgeImage from '@/components/badge-image.astro'
import { badgeControls } from '@/lib/badge-controls'

const badgeButtons = badgeControls.slice(0, 3)
const sideButtons = badgeControls.slice(3)
---

<div id="badge-os-simulator" data-state="loading" class="badge-os-stack">
Expand Down Expand Up @@ -35,21 +32,8 @@ const sideButtons = badgeControls.slice(3)
</div>
</div>
</div>
<div data-interactive class="mx-auto flex max-w-[420px] flex-wrap items-center justify-center gap-4 sm:justify-between" role="group" aria-label="Badge controls">
<div class="flex gap-2">
{badgeButtons.map(({ icon: Icon, name, bit }) => (
<button type="button" class="badge-os-key badge-os-control text-foreground" data-button={bit} aria-label={`Badge ${name} button`} title={name} disabled>
<Icon className="h-6 w-6" aria-hidden="true" />
</button>
))}
</div>
<div class="flex gap-2">
{sideButtons.map(({ icon: Icon, name, bit }) => (
<button type="button" class="badge-os-key badge-os-control text-foreground" data-button={bit} aria-label={`Badge ${name} button`} title={name} disabled>
<Icon className="h-6 w-6" aria-hidden="true" />
</button>
))}
</div>
<div data-interactive>
<BadgeControls disabled />
</div>
<figcaption data-interactive class="mx-auto max-w-3xl pt-4 text-center text-sm leading-6 text-muted-foreground">
Use the controls above, or use the arrow keys to move, Enter to select, and Esc for Home. Drag the badge to rotate it; scroll or use +/− to zoom; use Rotate to flip it; device tilt controls motion. MonaOS 4.0 and stock apps run in the browser. HTTP requests depend on CORS, and socket apps such as Camera and Copilot require a physical badge.
Expand Down Expand Up @@ -81,7 +65,6 @@ const sideButtons = badgeControls.slice(3)
.badge-os-key:disabled { opacity: 0.4; }
.badge-os-key:focus-visible { outline: 2px solid hsl(var(--primary)); outline-offset: 2px; }
.badge-os-key[data-pressed='true'] { color: hsl(var(--primary)); border-color: hsl(var(--primary)); background: hsl(var(--primary) / 0.1); transform: translateY(1px); box-shadow: inset 0 1px 3px rgb(0 0 0 / 0.6); }
.badge-os-control { display: grid; width: 3.25rem; height: 3.25rem; place-items: center; }
.badge-os-tool { display: inline-flex; width: auto; align-items: center; gap: 0.5rem; padding: 0 0.75rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.12em; }
.badge-os-tool[aria-pressed='true'] { color: hsl(var(--primary)); border-color: hsl(var(--primary) / 0.6); }
@media (max-width: 480px) { .badge-os-tool { padding: 0 0.5rem; letter-spacing: 0.08em; } }
Expand Down
28 changes: 20 additions & 8 deletions src/pages/simulator.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
---
import Layout from '@/layouts/Layout.astro'
import BadgeControlSymbols from '@/components/badge-control-symbols.astro'
import BadgeControls from '@/components/badge-controls.astro'
import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb, RefreshCw, ArrowRight, Undo2, Redo2, Grid2X2, Maximize, Minimize, X, Search, Trash2, Plus, RotateCw } from 'lucide-react'
---

<Layout title="Developer Simulator" description="Write Python, run it in the browser, and upload your app to a Badger badge.">
<section class="container py-10 sm:py-14">
<h1 class="mb-6 text-4xl sm:text-5xl">Developer Simulator</h1>

<div class="mb-6 max-w-[75ch]">
<h2 class="mb-2 text-2xl sm:text-3xl">Use the browser</h2>
<p class="text-sm leading-6 text-muted-foreground">No badge is needed. Select <strong class="font-mono font-medium text-foreground">Apps</strong> or <strong class="font-mono font-medium text-foreground">Examples</strong>, or select <strong class="font-mono font-medium text-foreground">New app</strong>. Edit the code, then select <strong class="font-mono font-medium text-foreground">Run</strong> to test it in the browser. With <strong class="font-mono font-medium text-foreground">Auto-run</strong> selected, the preview updates as you edit. Run does not change files on a connected badge.</p>
</div>

<div id="badge-editor" class="font-sans">
<div id="editor-workspace" class="surface overflow-hidden">
<div class="editor-toolbar flex flex-wrap items-center gap-3 border-b border-border/70 px-4 py-3">
Expand Down Expand Up @@ -75,9 +81,8 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
<div id="screen-placeholder" class="pointer-events-none absolute inset-0 grid place-content-center gap-2 text-center"><Code2 size={28} className="mx-auto text-primary" /><span class="text-xs text-[#8b949e]">Your code starts here.</span></div>
<p id="model-status" role="status" class="pointer-events-none absolute inset-x-4 bottom-3 text-center text-xs text-muted-foreground">Loading 3D view… You can use the simulator now.</p>
</div>
<div class="mx-auto mt-2 flex max-w-[380px] items-center justify-between gap-2" role="group" aria-label="Badge controls">
<div class="flex gap-2">{[['A',16],['B',8],['C',4]].map(([label,bit]) => <button class="badge-key" data-button={bit} aria-label={`Badge button ${label}`}>{label}</button>)}</div>
<div class="flex gap-2">{[['↑',2],['↓',1],['⌂',32]].map(([label,bit]) => <button class="badge-key" data-button={bit} aria-label={bit === 32 ? 'Badge Home button' : bit === 2 ? 'Badge Up button' : 'Badge Down button'}>{label}</button>)}</div>
<div class="mt-2">
<BadgeControls />
</div>
<div class="mt-3 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-[0.65rem] text-muted-foreground"><span>Drag to rotate · Scroll to zoom</span><a href="https://leereilly.net/h4x0r3d-badge/#hardware" target="_blank" rel="noreferrer" class="hover:text-foreground">Model source ↗</a></div>
<details class="mx-auto mt-3 max-w-[380px] text-xs text-muted-foreground">
Expand All @@ -86,7 +91,7 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
{[['←',256],['↑',2048],['↓',4096],['→',512],['Select',1024],['Back',8192]].map(([label,bit]) => <button class="badge-key !w-auto min-w-8 px-2" data-button={bit} aria-label={`Gamepad ${label}`}>{label}</button>)}
</div>
<label class="mt-3 flex items-center gap-2"><input id="simulator-flipped" type="checkbox" /> Upside down</label>
<p class="mt-2 leading-relaxed">Badge: arrow keys, Space or Enter to select, Escape for Home. Gamepad: WASD to move, J to select, K to go back, U for Menu. Keyboard controls work while the simulator is focused. In full screen, Escape exits full screen; use the ⌂ button for Home.</p>
<p class="mt-2 leading-relaxed">Badge: arrow keys, Space or Enter to select, Escape for Home. Gamepad: WASD to move, J to select, K to go back, U for Menu. Keyboard controls work while the simulator is focused. In full screen, Escape exits full screen; use the Home button for Home.</p>
</details>
</div>
</div>
Expand All @@ -106,6 +111,16 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
<h2 class="flex items-center gap-2 text-2xl sm:text-3xl"><Usb size={22} className="text-primary" /> Edit apps on your badge</h2>
<span id="disk-status" role="status" class="font-mono text-[0.65rem] text-muted-foreground">No badge connected</span>
</div>
<div id="usb-help" class="mb-4 max-w-[75ch] text-sm leading-6 text-muted-foreground">
<p class="mb-2">Use desktop Chrome or Edge.</p>
<ol class="list-decimal space-y-2 pl-5">
<li>Connect the badge to your computer with a USB data cable. Quickly press RESET twice. Wait for USB Disk Mode on the screen and the BADGER drive on your computer.</li>
<li>Select <strong class="font-mono font-medium text-foreground">Connect badge</strong>. Choose the BADGER drive itself and allow access.</li>
<li>To edit an installed app, select <strong class="font-mono font-medium text-foreground">Choose badge app</strong>. To install your current project, leave it open in the editor.</li>
<li>Edit the code, then select <strong class="font-mono font-medium text-foreground">Upload</strong> or <strong class="font-mono font-medium text-foreground">Install</strong>. Wait for confirmation.</li>
<li>Select <strong class="font-mono font-medium text-foreground">Disconnect</strong>, then eject BADGER from your computer. Wait for the drive to disappear before unplugging. If the badge does not restart on its own, press RESET. Open the app on the badge to test it.</li>
</ol>
</div>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex min-w-0 max-w-full flex-wrap items-center gap-3">
<button id="connect-disk" class="editor-button"><FolderOpen size={15} /> Connect badge</button>
Expand All @@ -121,10 +136,6 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
<button id="upload-code" class="editor-button editor-button-primary" disabled><Upload size={15} /> <span id="upload-label">Upload</span></button>
</div>
</div>
<details id="usb-help" class="mt-2 text-xs text-muted-foreground">
<summary class="font-mono w-fit cursor-pointer text-xs hover:text-foreground">USB setup</summary>
<p class="mt-2 max-w-2xl leading-relaxed">Connect over USB, double tap the reset button to open disk mode, then choose the BADGER volume with Connect badge. Select an app to edit its files.</p>
</details>
<label id="overwrite-option" class="font-mono mt-3 flex items-center gap-2 text-xs" hidden><input id="overwrite" type="checkbox" /><span>Allow updating this existing app.</span></label>
<p id="upload-message" role="status" class="mt-3 text-sm" hidden></p>
</div>
Expand Down Expand Up @@ -215,6 +226,7 @@ import { Code2, Play, Square, Upload, Download, FolderOpen, Terminal, Cpu, Usb,
.fullscreen-exit { display: none; }
#badge-editor[data-fullscreen='true'] { position: fixed; inset: 0; z-index: 100; display: flex; flex-direction: column; height: 100dvh; width: 100%; max-width: none; margin: 0; border: 0; border-radius: 0; box-shadow: none; background: hsl(var(--background)); }
#badge-editor[data-fullscreen='true'] #badge-connection { flex-shrink: 0; margin: 0 0 0.75rem; border: 0; border-radius: 0; box-shadow: none; }
#badge-editor[data-fullscreen='true'] #usb-help { display: none; }
#badge-editor[data-fullscreen='true'] #editor-workspace { display: flex; flex-direction: column; flex: 1; min-height: 0; border: 0; border-radius: 0; box-shadow: none; }
#badge-editor[data-fullscreen='true'] .editor-toolbar { flex-shrink: 0; }
#badge-editor[data-fullscreen='true'] .editor-split { flex: 1; min-height: 0; grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }
Expand Down
Loading