Initial working version
This commit is contained in:
parent
b12de4545f
commit
3ae7ac1b1d
5 changed files with 396 additions and 1 deletions
120
.gitignore
vendored
Normal file
120
.gitignore
vendored
Normal 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
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
# 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
14
package-lock.json
generated
Normal 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
29
package.json
Normal 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
232
repl.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue