Improved reliability in certain edge cases

This commit is contained in:
Melissa LeBlanc-Williams 2024-07-17 11:11:44 -07:00
parent 451b4bcdfc
commit 35402aa686

95
repl.js
View file

@ -4,7 +4,7 @@ const CHAR_CTRL_C = '\x03';
const CHAR_CTRL_D = '\x04'; const CHAR_CTRL_D = '\x04';
const CHAR_TITLE_START = "\x1b]0;"; const CHAR_TITLE_START = "\x1b]0;";
const CHAR_TITLE_END = "\x1b\\"; const CHAR_TITLE_END = "\x1b\\";
const REGEX_RAW_PASTE_RESPONSE = /R[\x00\x01]..\x01/; const CHAR_SNAKE = "🐍";
const MODE_NORMAL = 1; const MODE_NORMAL = 1;
const MODE_RAW = 2; const MODE_RAW = 2;
@ -16,13 +16,6 @@ const DEBUG = true;
export const LINE_ENDING_CRLF = "\r\n"; export const LINE_ENDING_CRLF = "\r\n";
export const LINE_ENDING_LF = "\n"; export const LINE_ENDING_LF = "\n";
const CONTROL_SEQUENCES = [
REGEX_RAW_PASTE_RESPONSE
];
// Mostly needed when the terminal echos back the input
const IGNORE_OUTPUT_LINE_PREFIXES = [/^\... /, /^>>> /];
// Default timeouts in milliseconds (can be overridden with properties) // Default timeouts in milliseconds (can be overridden with properties)
const PROMPT_TIMEOUT = 20000; const PROMPT_TIMEOUT = 20000;
const CODE_EXECUTION_TIMEOUT = 15000; const CODE_EXECUTION_TIMEOUT = 15000;
@ -30,6 +23,7 @@ const CODE_INTERRUPT_TIMEOUT = 5000;
const PROMPT_CHECK_INTERVAL = 50; const PROMPT_CHECK_INTERVAL = 50;
const REGEX_PROMPT_RAW_MODE = /raw REPL; CTRL-B to exit/; const REGEX_PROMPT_RAW_MODE = /raw REPL; CTRL-B to exit/;
const REGEX_PROMPT_NORMAL_MODE = />>> /;
const modes = [ const modes = [
"Unknown", "Unknown",
@ -403,7 +397,6 @@ export class REPL {
this._checkpointCount = 0; this._checkpointCount = 0;
this._rawByteCount = 0; this._rawByteCount = 0;
this.terminalOutput = true; this.terminalOutput = true;
this._codeCheckInProgress = false;
} }
//// Placeholder Functions //// //// Placeholder Functions ////
@ -437,7 +430,7 @@ export class REPL {
// Split a string up by full title start and end character sequences // Split a string up by full title start and end character sequences
_tokenize(string) { _tokenize(string) {
const tokenRegex = new RegExp("(" + this._regexEscape(CHAR_TITLE_START) + "|" + this._regexEscape(CHAR_TITLE_END) + "|" + this._regexEscape(CHAR_CTRL_D) + ")", "gi"); const tokenRegex = new RegExp("(" + this._regexEscape(CHAR_TITLE_START) + "|" + this._regexEscape(CHAR_TITLE_END) + ")", "gi");
return string.split(tokenRegex); return string.split(tokenRegex);
} }
@ -463,7 +456,7 @@ export class REPL {
let buffer = this._serialInputBuffer.get(); let buffer = this._serialInputBuffer.get();
const rawModRegex = new RegExp(REGEX_PROMPT_RAW_MODE, 'g'); const rawModRegex = new RegExp(REGEX_PROMPT_RAW_MODE, 'g');
const normalModRegex = new RegExp(/>>> /g); const normalModRegex = new RegExp(REGEX_PROMPT_NORMAL_MODE, 'g');
let lastRawPosition = this._findLastRegexPosition(rawModRegex, buffer); let lastRawPosition = this._findLastRegexPosition(rawModRegex, buffer);
let lastNormalPosition = this._findLastRegexPosition(normalModRegex, buffer); let lastNormalPosition = this._findLastRegexPosition(normalModRegex, buffer);
@ -512,26 +505,9 @@ export class REPL {
return this._lineIsPrompt(/>>> $/); return this._lineIsPrompt(/>>> $/);
} }
_currentLineIsPrompt() {
if (this._mode == MODE_NORMAL) {
return this._currentLineIsNormalPrompt();
}
return false;
}
async _processSerialData(data) {
// This should:
// check for mode change
// look for code output (and Ctrl + D)
// write to terminal
// This should not require a new line to be received to process the data
}
async _checkCodeRunning() { async _checkCodeRunning() {
await this._detectCurrentMode(); await this._detectCurrentMode();
console.log("Checking code running"); console.log("Checking code running in " + modes[this._mode]);
if (this._mode == MODE_RAW) { if (this._mode == MODE_RAW) {
// In raw mode, we simply need to look for OK // In raw mode, we simply need to look for OK
// Then we should store the results in the code output // Then we should store the results in the code output
@ -548,8 +524,13 @@ export class REPL {
if (bytes.slice(0, 2).match("OK")) { if (bytes.slice(0, 2).match("OK")) {
this._checkpointCount++; this._checkpointCount++;
bytes = bytes.slice(2); bytes = bytes.slice(2);
} else if (bytes.slice(0, 2).match("ra")) {
console.log("Unexpected bytes encountered. Assuming code is not running.");
this._pythonCodeRunning = false;
return;
} else { } else {
console.error("Error: " + bytes); console.error("Unexpected output in raw mode: " + bytes);
return;
} }
} else { } else {
if (bytes.slice(0, 1).match(CHAR_CTRL_D)) { if (bytes.slice(0, 1).match(CHAR_CTRL_D)) {
@ -583,7 +564,6 @@ export class REPL {
console.log("REPL at Normal Mode prompt"); console.log("REPL at Normal Mode prompt");
this._pythonCodeRunning = false; this._pythonCodeRunning = false;
} }
this._codeCheckInProgress = false;
} }
_decodeError(rawError) { _decodeError(rawError) {
@ -651,16 +631,23 @@ export class REPL {
} }
} }
async _waitForModeChange(mode) { async _waitForModeChange(mode, keySequence=null) {
console.log("Waiting for mode change from " + modes[this._mode] + " to " + modes[mode]); console.log("Waiting for mode change from " + modes[this._mode] + " to " + modes[mode]);
try {
await this._timeout( await this._timeout(
async () => { async () => {
while (this._mode != mode) { while (this._mode != mode) {
if (keySequence) {
await this.serialTransmit(keySequence);
}
await this._detectCurrentMode(); await this._detectCurrentMode();
await this._sleep(100); await this._sleep(100);
} }
}, this.promptTimeout }, 1000
); );
} catch (error) {
console.log("Awaiting mode change timed out.");
}
} }
// Raw mode allows code execution without echoing back to the terminal // Raw mode allows code execution without echoing back to the terminal
@ -668,8 +655,7 @@ export class REPL {
if (this._mode == MODE_RAW) { if (this._mode == MODE_RAW) {
await this._exitRawMode(); await this._exitRawMode();
} }
await this.serialTransmit(CHAR_CTRL_A); await this._waitForModeChange(MODE_RAW, CHAR_CTRL_A);
await this._waitForModeChange(MODE_RAW);
} }
async _exitRawMode() { async _exitRawMode() {
@ -677,34 +663,8 @@ export class REPL {
return; return;
} }
await this.serialTransmit(CHAR_CTRL_B);
// Wait for >>> to be displayed // Wait for >>> to be displayed
await this._waitForModeChange(MODE_NORMAL); await this._waitForModeChange(MODE_NORMAL, CHAR_CTRL_B);
}
_getSerialCodeOutput() {
let bufferLines, codeline = '';
// Get the remaining buffer from _codeCheckPointer to the next line ending and update the position of the pointer
let remainingBuffer = this._serialInputBuffer.slice(this._codeCheckPointer);
if (remainingBuffer.includes(this._inputLineEnding)) {
[codeline, ...bufferLines] = remainingBuffer.split(this._inputLineEnding);
this._codeCheckPointer += codeline.length + this._inputLineEnding.length;
}
return this._formatCodeOutput(codeline);
}
_formatCodeOutput(codeline) {
// Remove lines that are prompts or control characters and strip control sequences
// Return the result
for (let prefix of IGNORE_OUTPUT_LINE_PREFIXES) {
if (codeline.match(prefix)) {
return '';
}
}
for (let sequence of CONTROL_SEQUENCES) {
codeline = codeline.replace(sequence, '');
}
return codeline;
} }
async _processQueuedTokens() { async _processQueuedTokens() {
@ -720,7 +680,6 @@ export class REPL {
// Handle Title setting and add to the serial input buffer // Handle Title setting and add to the serial input buffer
async _processToken(token) { async _processToken(token) {
//console.log(token);
if (token == CHAR_TITLE_START) { if (token == CHAR_TITLE_START) {
this._titleMode = true; this._titleMode = true;
this._setTitle(""); this._setTitle("");
@ -728,6 +687,12 @@ export class REPL {
this._titleMode = false; this._titleMode = false;
} else if (this._titleMode) { } else if (this._titleMode) {
this._setTitle(token, true); this._setTitle(token, true);
// Fix duplicate Title charactes
let snakeIndex = this.title.indexOf(CHAR_SNAKE);
if (snakeIndex > -1) {
this._setTitle(this.title.slice(snakeIndex));
}
} }
this._serialInputBuffer.append(token); this._serialInputBuffer.append(token);
@ -800,7 +765,7 @@ export class REPL {
async interruptCode() { async interruptCode() {
console.log("Interrupting code"); console.log("Interrupting code");
this._pythonCodeRunning = true; this._pythonCodeRunning = true;
// Wait for a prompt // Wait for code to be interrupted
try { try {
await this._timeout( await this._timeout(
async () => { async () => {
@ -812,7 +777,7 @@ export class REPL {
}, CODE_INTERRUPT_TIMEOUT }, CODE_INTERRUPT_TIMEOUT
); );
} catch (error) { } catch (error) {
console.error("Awaiting code interruption timed out. Restarting device."); console.log("Awaiting code interruption timed out. Restarting device.");
// Can't determine the state, so restart device // Can't determine the state, so restart device
await this.softRestart(); await this.softRestart();
await this.serialTransmit(CHAR_CTRL_C); await this.serialTransmit(CHAR_CTRL_C);