Initial working version

This commit is contained in:
Melissa LeBlanc-Williams 2023-01-27 14:28:43 -08:00
parent b12de4545f
commit 3ae7ac1b1d
5 changed files with 396 additions and 1 deletions

120
.gitignore vendored Normal file
View file

@ -0,0 +1,120 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
*.DS_Store

View file

@ -1,2 +1,2 @@
# circuitpython-repl-js # circuitpython-repl-js
A JavaScript Module to help with interfacing to CircuitPython Devices A JavaScript Module to help with interfacing to the REPL on CircuitPython Devices over serial. This has been tested with CircuitPython 8.0.0-beta.6.

14
package-lock.json generated Normal file
View file

@ -0,0 +1,14 @@
{
"name": "@adafruit/circuitpython-repl-js",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@adafruit/circuitpython-repl-js",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {}
}
}
}

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "@adafruit/circuitpython-repl-js",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
"version": "1.0.0",
"description": "A JavaScript Module to help with interfacing to the REPL on CircuitPython Devices over serial",
"main": "repl.js",
"dependencies": {
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git://github.com/adafruit/circuitpython-repl-js.git"
},
"keywords": [
"BLE",
"WebBluetooth"
],
"author": "Melissa LeBlanc-Williams",
"license": "MIT",
"bugs": {
"url": "https://github.com/adafruit/circuitpython-repl-js/issues"
},
"homepage": "https://github.com/adafruit/circuitpython-repl-js#readme"
}

232
repl.js Normal file
View file

@ -0,0 +1,232 @@
const CHAR_CTRL_C = '\x03';
const CHAR_CTRL_D = '\x04';
const CHAR_CRLF = '\x0a\x0d';
const CHAR_BKSP = '\x08';
const CHAR_TITLE_START = "\x1b]0;";
const CHAR_TITLE_END = "\x1b\\";
const LINE_ENDING = "\r\n";
// Default timeouts in milliseconds (can be overridden with properties)
const PROMPT_TIMEOUT = 10000;
const PROMPT_CHECK_INTERVAL = 50;
export class REPL {
constructor() {
this._pythonCodeRunning = false;
this._codeOutput = '';
this._currentSerialReceiveLine = '';
this._checkingPrompt = false;
this._titleMode = false;
this.promptTimeout = PROMPT_TIMEOUT;
this.promptCheckInterval = PROMPT_CHECK_INTERVAL;
this.withholdTitle = false;
this.serialTransmit = null;
this._tokenQueue = [];
}
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
_timeout(callback, ms) {
return Promise.race([callback(), this._sleep(ms).then(() => {throw Error("Timed Out");})]);
}
_currentLineIsPrompt() {
return this._currentSerialReceiveLine.match(/>>> $/);
}
_regexEscape(regexString) {
return regexString.replace(/\\/, "\\\\");
}
// This should help detect lines like ">>> ", but not ">>> 1+1"
async checkPrompt() {
// Only allow one instance of this function to run at a time (unless this could cause it to miss a prompt)
if (!this._currentLineIsPrompt()) {
return;
}
// Check again after a short delay to see if it's still a prompt
await this._sleep(this.promptCheckInterval);
if (!this._currentLineIsPrompt()) {
return;
}
this._pythonCodeRunning = false;
}
async waitForPrompt() {
this._pythonCodeRunning = true;
await this.getToPrompt();
// Wait for a prompt
try {
await this._timeout(
async () => {
while (this._pythonCodeRunning) {
await this._sleep(100);
}
}, this.promptTimeout
);
} catch (error) {
console.log("Awaiting prompt timed out.");
return false;
}
return true;
}
async softRestart() {
await this.serialTransmit(CHAR_CTRL_D);
}
async getToPrompt() {
await this.serialTransmit(CHAR_CTRL_C);
}
async onSerialReceive(e) {
// Prepend a partial token if it exists
if (this._partialToken) {
e.data = this._partialToken + e.data;
this._partialToken = null;
}
// Tokenize the larger string and send to the parent
let tokens = this._tokenize(e.data);
// Remove any partial tokens and store for the next serial data receive
if (tokens.length && this._hasPartialToken(tokens.slice(-1))) {
this._partialToken = tokens.pop();
}
// Send only full tokens to the token queue
for (let token of tokens) {
this._tokenQueue.push(token);
}
await this._processQueuedTokens();
}
async _processQueuedTokens() {
if (this._processing) {
return;
}
this._processing = true;
while (this._tokenQueue.length) {
await this._processToken(this._tokenQueue.shift());
}
this._processing = false;
}
async _processToken(token) {
if (token == CHAR_TITLE_START) {
this._titleMode = true;
this.setTitle("");
} else if (token == CHAR_TITLE_END) {
this._titleMode = false;
} else if (this._titleMode) {
this.setTitle(token, true);
}
let codeline = '';
if (this._pythonCodeRunning) {
this._currentSerialReceiveLine += token;
// Run asynchronously to avoid blocking the serial receive
this.checkPrompt();
if (this._currentSerialReceiveLine.includes(LINE_ENDING)) {
[codeline, this._currentSerialReceiveLine] = this._currentSerialReceiveLine.split(LINE_ENDING, 2);
}
}
// Is it still running? Then we add to code output
if (this._pythonCodeRunning && codeline.length > 0) {
if (!codeline.match(/^\... /) && !codeline.match(/^>>> /)) {
this._codeOutput += codeline + LINE_ENDING;
}
}
}
// Placeholder Function
setTitle(title, append=false) {
if (append) {
title = this.title + title;
}
this.title = title;
}
async _serialTransmit(msg) {
if (!this.serialTransmit) {
console.log("Default serial transmit function called. Message: " + msg);
throw new Error("REPL serialTransmit must be connected to an external transmit function");
} else {
return await this.serialTransmit(msg);
}
}
// Allows for supplied python code to be run on the device via the REPL
async runCode(code, codeTimeoutMs=15000) {
// Wait for the prompt to appear
if (!(await this.waitForPrompt())) {
return null;
}
// Slice the code up into block and lines and run it
this._pythonCodeRunning = true;
this._codeOutput = '';
const codeBlocks = code.split(/(?:\r?\n)+(?!\s)/);
let indentLevel = 0;
for (const block of codeBlocks) {
for (const line of block.split(/\r?\n/)) {
const codeIndent = Math.floor(line.match(/^\s*/)[0].length / 4);
// Send code line with indents removed
await this._serialTransmit(line.slice(codeIndent * 4) + CHAR_CRLF);
if (codeIndent < indentLevel) {
// Remove indents to match the code
await this._serialTransmit(CHAR_BKSP.repeat(indentLevel - codeIndent) + CHAR_CRLF);
}
indentLevel = codeIndent;
}
}
// Wait for the code to finish running, so we can capture the output
if (codeTimeoutMs) {
try {
await this._timeout(
async () => {
while (this._pythonCodeRunning) {
await this._sleep(100);
}
}, codeTimeoutMs
);
} catch (error) {
console.log("Code timed out.");
}
} else {
// Run without timeout
while (this._pythonCodeRunning) {
await this._sleep(100);
}
}
return this._codeOutput;
}
// Split a string up by full title start and end character sequences
_tokenize(string) {
const tokenRegex = new RegExp("(" + this._regexEscape(CHAR_TITLE_START) + "|" + this._regexEscape(CHAR_TITLE_END) + ")", "gi");
return string.split(tokenRegex);
}
// Check if a chunk of data has a partial title start/end character sequence at the end
_hasPartialToken(chunk) {
const partialToken = /\\x1b(?:\](?:0"?)?)?$/gi;
return partialToken.test(chunk);
}
}