From fa01bc6afc4d917aff8c0eb4d6cdb0f99cc23a6f Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:53:17 -0600 Subject: [PATCH] Package manager: put omip.py on the connected device from the Tools menu A board that can reach the network installs OmniPython packages with omip.py, and this gets that one file onto it without typing a URL. It goes through the same install as every other package, so it reaches a USB, Bluetooth or network board and the simulator alike. It stays source, because CircuitPython can't run the .mpy this app would compile, and it needs no package index. test/browser/omip.spec.js fetches the real file in Chromium, writes it to the simulator and imports it. WebSerial can't be opened in a headless browser, so a real board wasn't run. --- docs/reference/features.md | 1 + src/ViperIDE.html | 1 + src/app.js | 16 ++++++++-- src/pydevices/omip.js | 24 ++++++++++++++ test/browser/omip.spec.js | 64 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/pydevices/omip.js create mode 100644 test/browser/omip.spec.js diff --git a/docs/reference/features.md b/docs/reference/features.md index c008b9e..94cdfc9 100644 --- a/docs/reference/features.md +++ b/docs/reference/features.md @@ -28,6 +28,7 @@ - Root FS stats display - **Package Manager** - Official `micropython-lib` index + - **Tools > Install omip.py on the device** puts `omip.py` on the connected board or the simulator, so it can run `omip.install("package")` itself, for OmniPython packages and what they need from `micropython-lib` - **MicroPython compiler** - Runs in-browser using WebAssembly - Instant syntax check in the editor diff --git a/src/ViperIDE.html b/src/ViperIDE.html index 0900c1a..effb4a1 100644 --- a/src/ViperIDE.html +++ b/src/ViperIDE.html @@ -88,6 +88,7 @@
📦 Install package via link
+
📦 Install omip.py on the device
🎤 Enable microphone
diff --git a/src/app.js b/src/app.js index 673d3d0..b982189 100644 --- a/src/app.js +++ b/src/app.js @@ -32,6 +32,7 @@ import { ConnectionUID } from './connection_uid.js' import translations from '../build/translations.json' import { parseStackTrace, validatePython, disassembleMPY, minifyPython, prettifyPython, compilePython, reprStr } from './python_utils.js' import { SYSTEM_DIRS, visibleTree } from './emulator.js' +import { rawInstallOmip } from './pydevices/omip.js' import { createPyDevicesVM, enableSimulatorAudio, armSimulatorAudio, disarmSimulatorAudio } from './pydevices/runtime.js' import { showDeviceStage, hideDeviceStage, initDeviceStage, onDisplayResize, getDisplayConfig } from './pydevices/stage.js' import { resolveGalleryExample, rawStageGalleryExample } from './pydevices/gallery.js' @@ -2026,14 +2027,25 @@ async function _raw_installPkg(raw, pkg, { version=null } = {}) { } } -export async function installPkg(pkg, { version=null } = {}) { +async function _raw_installOmip(raw) { + analytics.track('Package Install', { name: 'omip' }) + toastr.info('Installing omip...') + await rawInstallOmip(raw, await _raw_ensureDevInfo(raw)) + toastr.success('Installed omip.py. Try: import omip; omip.install("package")') +} + +export async function installOmip() { + return await installPkg('omip', { rawInstall: _raw_installOmip }) +} + +export async function installPkg(pkg, { version=null, rawInstall=_raw_installPkg } = {}) { if (!portReady()) { toastr.info('Connect yout device first') return false } const raw = await MpRawMode.begin(port) try { - await _raw_installPkg(raw, pkg, { version }) + await rawInstall(raw, pkg, { version }) await _raw_updateFileTree(raw) return true } catch (err) { diff --git a/src/pydevices/omip.js b/src/pydevices/omip.js new file mode 100644 index 0000000..49ee4ff --- /dev/null +++ b/src/pydevices/omip.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 PyDevices + * SPDX-License-Identifier: MIT + * + * Putting omip.py on the connected device, so it can install OmniPython + * packages for itself. The file comes straight from its repository and needs + * no package index. It stays source: CircuitPython can't run the .mpy this app + * would compile, and the same file then works on every board. + */ + +import { rawInstallPkg } from '../package_mgr.js' + +const OMIP_PACKAGE = { + name: 'omip', + urls: [['omip.py', 'github:PyDevices/pydevices/lib/omip.py']], +} + +export async function rawInstallOmip(raw, dev) { + return rawInstallPkg(raw, OMIP_PACKAGE.name, { + dev, + pkg_info: OMIP_PACKAGE, + prefer_source: true, + }) +} diff --git a/test/browser/omip.spec.js b/test/browser/omip.spec.js new file mode 100644 index 0000000..f215f3d --- /dev/null +++ b/test/browser/omip.spec.js @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// +// The Tools menu puts omip.py on the device: the real file, fetched from its +// repository in this browser, written over the raw REPL, as source, and +// importable. A WebSerial board goes through the same install over a different +// transport, which a headless browser can't open. +import { test, expect } from '@playwright/test' +import { connectSimulator, focusTerminal, terminalText } from './helpers.js' + +const INDEXES = [/vsh\.pp\.ua/i, /micropython\.org\/pi\//i, /omnipython\.org/i] + +async function openTools(page) { + await page.click('.tab[data-target="menu-tools"]') + await expect(page.locator('#install-omip')).toBeVisible() +} + +test('the Tools menu installs omip.py as source, with no index, and it imports', async ({ page }) => { + await connectSimulator(page) + + const indexRequests = [] + page.on('request', (request) => { + if (INDEXES.some((pattern) => pattern.test(request.url()))) { + indexRequests.push(request.url()) + } + }) + + await openTools(page) + await page.click('#install-omip') + await expect(page.locator('#toast-container')).toContainText('Installed omip.py') + + await focusTerminal(page) + const before = await terminalText(page) + await page.keyboard.type('import omip, os; print("omip-ok", omip.__file__, omip.INDEX, "omip.mpy" in os.listdir("/lib"))') + await page.keyboard.press('Enter') + await expect + .poll(async () => (await terminalText(page)).slice(before.length)) + .toContain('omip-ok /lib/omip.py https://lib.omnipython.org False') + + expect(indexRequests).toEqual([]) +}) + +test('a failed download says so and writes nothing', async ({ page }) => { + await connectSimulator(page) + await page.route('**/raw.githubusercontent.com/**', (route) => route.fulfill({ status: 404, body: 'nope' })) + + await openTools(page) + await page.click('#install-omip') + await expect(page.locator('#toast-container')).toContainText('Installing failed') + + await focusTerminal(page) + const before = await terminalText(page) + await page.keyboard.type('import os; print("has-omip", "omip.py" in os.listdir("/lib"))') + await page.keyboard.press('Enter') + await expect + .poll(async () => (await terminalText(page)).slice(before.length)) + .toContain('has-omip False') +}) + +test('with nothing connected it asks for a device', async ({ page }) => { + await page.goto('/') + await openTools(page) + await page.click('#install-omip') + await expect(page.locator('#toast-container')).toContainText('Connect') +})