diff --git a/demo/emulator-run.ts b/demo/emulator-run.ts index d1eb08a82c..04c6e7ab9b 100644 --- a/demo/emulator-run.ts +++ b/demo/emulator-run.ts @@ -1,46 +1,70 @@ import * as fs from 'fs'; +import packageJson from '../package.json'; +import sade from 'sade'; import { GDBTCPServer } from '../src/gdb/gdb-tcp-server.js'; +import { RP2040 } from '../src/index.js'; import { Simulator } from '../src/simulator.js'; import { bootromB1 } from './bootrom.js'; import { loadHex } from './intelhex.js'; import { loadUF2 } from './load-flash.js'; -import minimist from 'minimist'; - -const args = minimist(process.argv.slice(2), { - string: [ - 'image', // An image to load, hex and UF2 are supported - ], -}); - -const simulator = new Simulator(); -const mcu = simulator.rp2040; -mcu.loadBootrom(bootromB1); - -const imageName = args.image ?? 'hello_uart.hex'; - -// Check the extension of the file -const extension = imageName.split('.').pop(); -if (extension === 'hex') { - // Create an array with the compiled code of blink - // Execute the instructions from this array, one by one. - const hex = fs.readFileSync(imageName, 'utf-8'); - - console.log(`Loading hex image ${imageName}`); - loadHex(hex, mcu.flash, 0x10000000); -} else if (extension === 'uf2') { - console.log(`Loading uf2 image ${imageName}`); - loadUF2(imageName, mcu); -} else { - console.log(`Unsupported file type: ${extension}`); - process.exit(1); + +type CliOptions = { + image: string; + gdb: boolean; + 'gdb-port': number; +}; + +function loadImage(imageName: string, mcu: RP2040) { + const extension = imageName.split('.').pop(); + + if (extension === 'hex') { + // Create an array with the compiled code of blink + // Execute the instructions from this array, one by one. + const hex = fs.readFileSync(imageName, 'utf-8'); + console.log(`Loading hex image ${imageName}`); + loadHex(hex, mcu.flash, 0x10000000); + } else if (extension === 'uf2') { + console.log(`Loading uf2 image ${imageName}`); + loadUF2(imageName, mcu); + } else { + console.log(`Unsupported file type: ${extension}`); + process.exit(1); + } } -const gdbServer = new GDBTCPServer(simulator, 3333); -console.log(`RP2040 GDB Server ready! Listening on port ${gdbServer.port}`); +function simulateImage(opts: CliOptions) { + const simulator = new Simulator(); + const mcu = simulator.rp2040; + mcu.loadBootrom(bootromB1); -mcu.uart[0].onByte = (value) => { - process.stdout.write(new Uint8Array([value])); -}; + try { + loadImage(opts.image, mcu); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + + console.log(`Error: Failed to load image file: "${message}"`); + process.exit(1); + } + + if (opts.gdb) { + const gdbServer = new GDBTCPServer(simulator, opts['gdb-port']); + console.log(`RP2040 GDB Server ready! Listening on port ${gdbServer.port}`); + } + + mcu.uart[0].onByte = (value) => { + process.stdout.write(new Uint8Array([value])); + }; + + simulator.rp2040.core.PC = 0x10000000; + simulator.execute(); +} -simulator.rp2040.core.PC = 0x10000000; -simulator.execute(); +sade('rp2040js', true) + .version(packageJson.version) + .describe(packageJson.description) + .option('-i, --image', 'Provide an image to run (.uf2, .hex)', 'hello_uart.hex') + .option('-g, --gdb', 'If a GDB server should be started or not', true) + .option('-p, --gdb-port', 'The port to start the gdb server on', 3333) + .example('--image ./hello_world.uf2') + .action(simulateImage) + .parse(process.argv); diff --git a/demo/micropython-run.ts b/demo/micropython-run.ts index 2eed8686a4..7de8f0f1ee 100644 --- a/demo/micropython-run.ts +++ b/demo/micropython-run.ts @@ -1,94 +1,126 @@ import fs from 'fs'; -import minimist from 'minimist'; +import packageJson from '../package.json'; +import sade from 'sade'; import { GDBTCPServer } from '../src/gdb/gdb-tcp-server.js'; +import { RP2040 } from '../src/index.js'; import { Simulator } from '../src/simulator.js'; import { USBCDC } from '../src/usb/cdc.js'; import { ConsoleLogger, LogLevel } from '../src/utils/logging.js'; import { bootromB1 } from './bootrom.js'; import { loadCircuitpythonFlashImage, loadMicropythonFlashImage, loadUF2 } from './load-flash.js'; -const args = minimist(process.argv.slice(2), { - string: [ - 'image', // UF2 image to load; defaults to "RPI_PICO-20230426-v1.20.0.uf2" - 'expect-text', // Text to expect on the serial console, process will exit with code 0 if found - ], - boolean: [ - 'gdb', // start GDB server on 3333 - 'circuitpython', // use CircuitPython instead of MicroPython - ], -}); -const expectText = args['expect-text']; - -const simulator = new Simulator(); -const mcu = simulator.rp2040; -mcu.loadBootrom(bootromB1); -mcu.logger = new ConsoleLogger(LogLevel.Error); - -let imageName: string; -if (!args.circuitpython) { - imageName = args.image ?? 'RPI_PICO-20230426-v1.20.0.uf2'; -} else { - imageName = args.image ?? 'adafruit-circuitpython-raspberry_pi_pico-en_US-8.0.2.uf2'; -} -console.log(`Loading uf2 image ${imageName}`); -loadUF2(imageName, mcu); - -if (fs.existsSync('littlefs.img') && !args.circuitpython) { - console.log(`Loading uf2 image littlefs.img`); - loadMicropythonFlashImage('littlefs.img', mcu); -} else if (fs.existsSync('fat12.img') && args.circuitpython) { - loadCircuitpythonFlashImage('fat12.img', mcu); - // Instead of reading from file, it would also be possible to generate the LittleFS image on-the-fly here, e.g. using - // https://github.com/wokwi/littlefs-wasm or https://github.com/littlefs-project/littlefs-js -} +type CliOptions = { + image: string | null; + 'expect-text': string | null; + gdb: boolean; + 'gdb-port': number; + 'circuit-python': boolean; +}; + +function loadImage(mcu: RP2040, image: string | null, useCircuitPython: boolean) { + let selectedImage: string; + if (image) selectedImage = image; + else if (useCircuitPython) + selectedImage = 'adafruit-circuitpython-raspberry_pi_pico-en_US-8.0.2.uf2'; + else selectedImage = 'RPI_PICO-20230426-v1.20.0.uf2'; + + console.log(`Loading uf2 image ${selectedImage}`); + + try { + loadUF2(selectedImage, mcu); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + + console.log(`Error: Failed to load image file: "${message}"`); + process.exit(1); + } -if (args.gdb) { - const gdbServer = new GDBTCPServer(simulator, 3333); - console.log(`RP2040 GDB Server ready! Listening on port ${gdbServer.port}`); + if (fs.existsSync('littlefs.img') && !useCircuitPython) { + console.log(`Loading uf2 image littlefs.img`); + loadMicropythonFlashImage('littlefs.img', mcu); + } else if (fs.existsSync('fat12.img') && useCircuitPython) { + loadCircuitpythonFlashImage('fat12.img', mcu); + // Instead of reading from file, it would also be possible to generate the LittleFS image on-the-fly here, e.g. using + // https://github.com/wokwi/littlefs-wasm or https://github.com/littlefs-project/littlefs-js + } } -const cdc = new USBCDC(mcu.usbCtrl); -cdc.onDeviceConnected = () => { - if (!args.circuitpython) { - // We send a newline so the user sees the MicroPython prompt - cdc.sendSerialByte('\r'.charCodeAt(0)); - cdc.sendSerialByte('\n'.charCodeAt(0)); - } else { +function handleDeviceConnected(cdc: USBCDC, useCircuitPython: boolean) { + if (useCircuitPython) { cdc.sendSerialByte(3); + return; } -}; -let currentLine = ''; -cdc.onSerialData = (value) => { + // We send a newline so the user sees the MicroPython prompt + cdc.sendSerialByte('\r'.charCodeAt(0)); + cdc.sendSerialByte('\n'.charCodeAt(0)); +} + +function testWriteSerialData(value: Uint8Array, expectText: string, decoder: TextDecoder) { process.stdout.write(value); - for (const byte of value) { - const char = String.fromCharCode(byte); - if (char === '\n') { - if (expectText && currentLine.includes(expectText)) { - console.log(`Expected text found: "${expectText}"`); - console.log('TEST PASSED.'); - process.exit(0); - } - currentLine = ''; - } else { - currentLine += char; - } - } -}; + const current = decoder.decode(value); -if (process.stdin.isTTY) { - process.stdin.setRawMode(true); -} -process.stdin.on('data', (chunk) => { - // 24 is Ctrl+X - if (chunk[0] === 24) { + if (current.includes(expectText)) { + console.log(`\nExpected text found: "${expectText}"`); + console.log('TEST PASSED.'); process.exit(0); } - for (const byte of chunk) { - cdc.sendSerialByte(byte); +} + +function installSerialDataWriter(cdc: USBCDC, expectText: string | null) { + if (expectText) { + const decoder = new TextDecoder(); + cdc.onSerialData = (value) => testWriteSerialData(value, expectText, decoder); + } else { + cdc.onSerialData = (value) => process.stdout.write(value); + } +} + +function simulateMicropythonImage(opts: CliOptions) { + const simulator = new Simulator(); + const mcu = simulator.rp2040; + mcu.loadBootrom(bootromB1); + mcu.logger = new ConsoleLogger(LogLevel.Error); + + loadImage(mcu, opts.image, opts['circuit-python']); + + if (opts.gdb) { + const gdbServer = new GDBTCPServer(simulator, opts['gdb-port']); + console.log(`RP2040 GDB Server ready! Listening on port ${gdbServer.port}`); } -}); -simulator.rp2040.core.PC = 0x10000000; -simulator.execute(); + const cdc = new USBCDC(mcu.usbCtrl); + cdc.onDeviceConnected = () => handleDeviceConnected(cdc, opts['circuit-python']); + installSerialDataWriter(cdc, opts['expect-text']); + + if (process.stdin.isTTY) process.stdin.setRawMode(true); + + process.stdin.on('data', (chunk) => { + // 24 is Ctrl+X + if (chunk[0] === 24) { + process.exit(0); + } + for (const byte of chunk) { + cdc.sendSerialByte(byte); + } + }); + + simulator.rp2040.core.PC = 0x10000000; + simulator.execute(); +} + +sade('rp2040js-micropython', true) + .version(packageJson.version) + .describe(packageJson.description) + .option('-i, --image', 'UF2 image to load') + .option( + '-e, --expect-text', + 'Text to expect on the serial console, process will exit with code 0 if found', + ) + .option('-g, --gdb', 'If a GDB server should be started on 3333 or not', false) + .option('-p, --gdb-port', 'The port to start the gdb server on', 3333) + .option('-c, --circuit-python', 'If CircuitPython should be used instead of MicroPython', false) + .example('--image ./my-image.uf2') + .action(simulateMicropythonImage) + .parse(process.argv); diff --git a/package-lock.json b/package-lock.json index c5abda1923..9e5a920934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,15 @@ "version": "1.1.1", "license": "MIT", "devDependencies": { - "@types/minimist": "^1.2.2", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "eslint": "^8.50.0", "husky": "^8.0.3", "lint-staged": "^15.4.3", - "minimist": "^1.2.7", "prettier": "^3.0.3", "rimraf": "^5.0.5", + "sade": "^1.8.1", "tsx": "^4.19.3", "typescript": "^5.7.3", "uf2": "^1.0.0", @@ -949,12 +948,6 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, - "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, "node_modules/@types/node": { "version": "18.19.76", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", @@ -3283,15 +3276,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", @@ -3301,6 +3285,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3888,6 +3882,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5341,12 +5348,6 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, - "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, "@types/node": { "version": "18.19.76", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", @@ -6978,18 +6979,18 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true - }, "minipass": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7368,6 +7369,15 @@ "queue-microtask": "^1.2.2" } }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "requires": { + "mri": "^1.1.0" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/package.json b/package.json index 387a115d87..30305b7ea2 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "scripts": { "build": "rimraf dist && tsc && tsc -p tsconfig.cjs.json && node build-scripts/dist-package-json", + "build:demo": "rimraf dist && tsc -p tsconfig.demo.json", "prepublish": "npm run build", "prepare": "husky install", "format:check": "prettier --check **/*.{ts,js} !**/dist/** !**/node_modules/**", @@ -57,16 +58,15 @@ "test:micropython-spi": "tsx test/micropython-spi-test.ts" }, "devDependencies": { - "@types/minimist": "^1.2.2", "@types/node": "^18", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "eslint": "^8.50.0", "husky": "^8.0.3", "lint-staged": "^15.4.3", - "minimist": "^1.2.7", "prettier": "^3.0.3", "rimraf": "^5.0.5", + "sade": "^1.8.1", "tsx": "^4.19.3", "typescript": "^5.7.3", "uf2": "^1.0.0", diff --git a/test/micropython-spi-test.ts b/test/micropython-spi-test.ts index 8285f55d0d..50bc433a44 100644 --- a/test/micropython-spi-test.ts +++ b/test/micropython-spi-test.ts @@ -1,36 +1,38 @@ import fs from 'fs'; -import minimist from 'minimist'; +import sade from 'sade'; +import packageJson from '../package.json'; +import { ConsoleLogger, LogLevel } from '../src/utils/logging.js'; +import { GPIOPinState, Simulator } from '../src/index.js'; import { bootromB1 } from '../demo/bootrom.js'; import { loadMicropythonFlashImage, loadUF2 } from '../demo/load-flash.js'; -import { GPIOPinState, Simulator } from '../src/index.js'; -import { ConsoleLogger, LogLevel } from '../src/utils/logging.js'; -const args = minimist(process.argv.slice(2)); - -const simulator = new Simulator(); -const mcu = simulator.rp2040; -mcu.loadBootrom(bootromB1); -mcu.logger = new ConsoleLogger(LogLevel.Error); +type CliOpts = { + _: string[]; +}; -const imageName = 'micropython.uf2'; -console.log(`Loading uf2 image ${imageName}`); -loadUF2(imageName, mcu); +function runTest(opts: CliOpts) { + const simulator = new Simulator(); + const mcu = simulator.rp2040; + mcu.loadBootrom(bootromB1); + mcu.logger = new ConsoleLogger(LogLevel.Error); -const littlefs = 'littlefs-spi.img'; + const imageName = 'micropython.uf2'; + console.log(`Loading uf2 image ${imageName}`); + loadUF2(imageName, mcu); -if (fs.existsSync(littlefs)) { - console.log(`Loading littlefs image ${littlefs}`); - loadMicropythonFlashImage(littlefs, mcu); -} + const littlefs = 'littlefs-spi.img'; -let spiBuf = ''; -mcu.gpio[5].addListener((state: GPIOPinState, oldState: GPIOPinState) => { - if (!spiBuf) { - return; + if (fs.existsSync(littlefs)) { + console.log(`Loading littlefs image ${littlefs}`); + loadMicropythonFlashImage(littlefs, mcu); } - if (state === GPIOPinState.High && oldState === GPIOPinState.Low) { - if (spiBuf !== args._?.shift()) { + let spiBuf = ''; + mcu.gpio[5].addListener((state: GPIOPinState, oldState: GPIOPinState) => { + if (!spiBuf) return; + if (state !== GPIOPinState.High || oldState !== GPIOPinState.Low) return; + + if (spiBuf !== opts._?.shift()) { console.log('SPI TEST FAILED.'); process.exit(1); } else { @@ -38,20 +40,28 @@ mcu.gpio[5].addListener((state: GPIOPinState, oldState: GPIOPinState) => { spiBuf = ''; } - if (args._.length === 0) { + if (opts._.length === 0) { console.log('SPI TEST PASSED.'); process.exit(0); } - } -}); - -const transmitAlarm = mcu.clock.createAlarm(() => { - mcu.spi[0].completeTransmit(0); -}); -mcu.spi[0].onTransmit = (char) => { - spiBuf += String.fromCharCode(char); - transmitAlarm.schedule(2000); // 2us per byte, so 4 MHz SPI -}; + }); + + const transmitAlarm = mcu.clock.createAlarm(() => { + mcu.spi[0].completeTransmit(0); + }); + + mcu.spi[0].onTransmit = (char) => { + spiBuf += String.fromCharCode(char); + transmitAlarm.schedule(2000); // 2us per byte, so 4 MHz SPI + }; + + mcu.core.PC = 0x10000000; + simulator.execute(); +} -mcu.core.PC = 0x10000000; -simulator.execute(); +sade('test', true) + .version(packageJson.version) + .describe(packageJson.description) + .example('test "hello world" "h" "0123456789abcdef0123456789abcdef0123456789abcdef"') + .action(runTest) + .parse(process.argv); diff --git a/tsconfig.demo.json b/tsconfig.demo.json new file mode 100644 index 0000000000..e3e056adf7 --- /dev/null +++ b/tsconfig.demo.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "resolveJsonModule": true + }, + "include": ["src/**/*.ts", "demo/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "demo/**/*.spec.ts"] +}