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
1 change: 1 addition & 0 deletions docs/reference/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/ViperIDE.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@

<div class="title-lines" id="menu-line-pkg-mgr">package manager</div>
<div><a href="#" onclick="app.installPkgFromUrl()" id="install-via-url">📦 Install package via link</a></div>
<div><a href="#" onclick="app.installOmip();return false;" id="install-omip">📦 Install omip.py on the device</a></div>

<div class="title-lines" id="menu-tools-simulator">simulator</div>
<div><a href="#" onclick="app.enableMicrophone();return false;" id="sim-enable-mic">🎤 Enable microphone</a></div>
Expand Down
16 changes: 14 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 24 additions & 0 deletions src/pydevices/omip.js
Original file line number Diff line number Diff line change
@@ -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,
})
}
64 changes: 64 additions & 0 deletions test/browser/omip.spec.js
Original file line number Diff line number Diff line change
@@ -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')
})
Loading