mirror of
https://github.com/We-Dont-Byte/Mind_Reader.git
synced 2024-11-15 03:35:59 +00:00
Parse messages on arrival
Rather than queuing inbound messages, the HubController now saves pending promises/rejects for each pending request. Each inbound packet is checked at the time of arrival, and if the ID matches a pending response, the corresponding promise is called. This fixes a problem where the longer the time between reads, the more garbage responses queue up that are guaranteed to get thrown away the next time the next response was gathered.
This commit is contained in:
parent
8ac7e2c5ea
commit
92d42edaff
@ -246,7 +246,7 @@ let hub: HubManager | null;
|
|||||||
async function connectHub(): Promise<void> {
|
async function connectHub(): Promise<void> {
|
||||||
if (hub) {
|
if (hub) {
|
||||||
vscode.window.showWarningMessage('LEGO Hub is already connected, reconnecting...');
|
vscode.window.showWarningMessage('LEGO Hub is already connected, reconnecting...');
|
||||||
hub.close();
|
disconnectHub();
|
||||||
}
|
}
|
||||||
const config = vscode.workspace.getConfiguration();
|
const config = vscode.workspace.getConfiguration();
|
||||||
|
|
||||||
@ -283,7 +283,7 @@ async function connectHub(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function disconnectHub(): Promise<void> {
|
async function disconnectHub(): Promise<void> {
|
||||||
if (!hub) {
|
if (!hub || !hub.isOpen()) {
|
||||||
vscode.window.showErrorMessage('LEGO Hub is not connected');
|
vscode.window.showErrorMessage('LEGO Hub is not connected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -294,7 +294,7 @@ async function disconnectHub(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadCurrentFile(): Promise<void> {
|
async function uploadCurrentFile(): Promise<void> {
|
||||||
if (!hub) {
|
if (!hub || !hub.isOpen()) {
|
||||||
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -327,7 +327,7 @@ async function uploadCurrentFile(): Promise<void> {
|
|||||||
|
|
||||||
// TODO: find empty slots
|
// TODO: find empty slots
|
||||||
async function runProgram(): Promise<void> {
|
async function runProgram(): Promise<void> {
|
||||||
if (!hub) {
|
if (!hub || !hub.isOpen()) {
|
||||||
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -348,7 +348,7 @@ async function runProgram(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function stopExecution(): Promise<void> {
|
async function stopExecution(): Promise<void> {
|
||||||
if (!hub) {
|
if (!hub || !hub.isOpen()) {
|
||||||
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -359,7 +359,7 @@ async function stopExecution(): Promise<void> {
|
|||||||
|
|
||||||
// TODO: find slots from status
|
// TODO: find slots from status
|
||||||
async function deleteProgram(): Promise<void> {
|
async function deleteProgram(): Promise<void> {
|
||||||
if (!hub) {
|
if (!hub || !hub.isOpen()) {
|
||||||
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,14 @@ import commands from './commands';
|
|||||||
|
|
||||||
import AccessNodeProvider from './accessNodeProvider';
|
import AccessNodeProvider from './accessNodeProvider';
|
||||||
|
|
||||||
|
// create output channel
|
||||||
|
const outputChannel = vscode.window.createOutputChannel("SPIKE Prime Output");
|
||||||
|
|
||||||
|
export function MindReaderOutput(line: string) {
|
||||||
|
outputChannel.show();
|
||||||
|
outputChannel.appendLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
let parser: pl.Parser = new pl.Parser();
|
let parser: pl.Parser = new pl.Parser();
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
@ -2,6 +2,7 @@ import * as vscode from 'vscode';
|
|||||||
import * as SerialPort from 'serialport';
|
import * as SerialPort from 'serialport';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
|
import { MindReaderOutput } from './extension';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type RPCRequest an RPC request message
|
* @type RPCRequest an RPC request message
|
||||||
@ -48,8 +49,8 @@ type HubOptions = {
|
|||||||
*/
|
*/
|
||||||
export default class HubManager {
|
export default class HubManager {
|
||||||
private port: SerialPort;
|
private port: SerialPort;
|
||||||
private receiveQueue: string[] = []; // queue of received messages to handle
|
private receiveBuffer: string = ''; // buffer for in-flight messages
|
||||||
public responses: RPCResponse[] = []; // list of messages returned to the user
|
private pendingRequests = new Map<string, [(result: any) => void, (error: string) => void]>();
|
||||||
|
|
||||||
// ======================== INSTANCE METHODS ========================
|
// ======================== INSTANCE METHODS ========================
|
||||||
|
|
||||||
@ -58,6 +59,65 @@ export default class HubManager {
|
|||||||
*/
|
*/
|
||||||
private constructor(public options: HubOptions) { }
|
private constructor(public options: HubOptions) { }
|
||||||
|
|
||||||
|
public isOpen(): boolean {
|
||||||
|
return this.port.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a received data chunk from the serial port
|
||||||
|
*
|
||||||
|
* @param `data` Data received from serial port
|
||||||
|
*/
|
||||||
|
private async receiveData(data: string) {
|
||||||
|
// add data to buffer
|
||||||
|
this.receiveBuffer += data;
|
||||||
|
|
||||||
|
// get full lines in buffer
|
||||||
|
|
||||||
|
let msgs = this.receiveBuffer.split(/\r/); // split by newline
|
||||||
|
this.receiveBuffer = msgs.pop()!; // store unhandled data
|
||||||
|
|
||||||
|
msgs = msgs.filter(x => !x.startsWith('{"m":0,"p":')); // drop sensor broadcast response spam
|
||||||
|
|
||||||
|
for (const msg of msgs) {
|
||||||
|
// check if this msg is a response to a pending request
|
||||||
|
try {
|
||||||
|
let json: { [key: string]: any };
|
||||||
|
|
||||||
|
json = JSON.parse(msg);
|
||||||
|
|
||||||
|
let id = json['i'];
|
||||||
|
if (id && this.pendingRequests.has(id)) {
|
||||||
|
// a request is waiting on this response
|
||||||
|
let [resolve, reject] = this.pendingRequests.get(id) ?? [];
|
||||||
|
|
||||||
|
if (json['e'] && reject) {
|
||||||
|
// error
|
||||||
|
reject(Buffer.from(json['e'], 'base64').toString());
|
||||||
|
} else if (resolve) {
|
||||||
|
resolve(json['r']);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingRequests.delete(id);
|
||||||
|
} else if (json['m']) {
|
||||||
|
// Print errors
|
||||||
|
const params = json['p'];
|
||||||
|
switch (json['m']) {
|
||||||
|
case 'user_program_error':
|
||||||
|
MindReaderOutput(Buffer.from(params[3], 'base64').toString());
|
||||||
|
MindReaderOutput(Buffer.from(params[4], 'base64').toString());
|
||||||
|
break;
|
||||||
|
case 'runtime_error':
|
||||||
|
MindReaderOutput(Buffer.from(params[3], 'base64').toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Could not parse JSON:', msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a created HubManager with the current option settings
|
* Initializes a created HubManager with the current option settings
|
||||||
*/
|
*/
|
||||||
@ -79,8 +139,10 @@ export default class HubManager {
|
|||||||
|
|
||||||
// push lines received to data queue
|
// push lines received to data queue
|
||||||
|
|
||||||
let rl = this.port.pipe(new SerialPort.parsers.Readline({delimiter: '\r'}));
|
let mgr = this;
|
||||||
rl.on('data', data => this.receiveQueue.push(data));
|
this.port.on('data', data => {
|
||||||
|
mgr.receiveData(data)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async close(): Promise<void> {
|
public async close(): Promise<void> {
|
||||||
@ -97,99 +159,18 @@ export default class HubManager {
|
|||||||
*/
|
*/
|
||||||
// TODO: make send take a single RPCRequest argument, made inline in each function
|
// TODO: make send take a single RPCRequest argument, made inline in each function
|
||||||
public async send(request: RPCRequest): Promise<RPCResponse> {
|
public async send(request: RPCRequest): Promise<RPCResponse> {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve, reject) => {
|
||||||
if (request['i'] === undefined) {
|
if (request['i'] === undefined) {
|
||||||
// generate an ID
|
// generate an ID
|
||||||
request['i'] = 'mind-reader-' + HubManager.randomID();
|
request['i'] = 'mind-reader-' + HubManager.randomID();
|
||||||
}
|
}
|
||||||
|
|
||||||
// write JSON to port
|
// write JSON to port
|
||||||
|
|
||||||
|
this.pendingRequests.set(request['i'], [resolve, reject]);
|
||||||
this.port.write(JSON.stringify(request));
|
this.port.write(JSON.stringify(request));
|
||||||
this.port.write('\r', async () => {
|
this.port.write('\r');
|
||||||
if (request['i']) {
|
this.port.drain();
|
||||||
// expecting a response
|
|
||||||
let response = await this.recv(request['i']);
|
|
||||||
resolve(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receive an RPC message.
|
|
||||||
*
|
|
||||||
* @param `id` The id to match received messages against. Use `null` to match *all* messages
|
|
||||||
*/
|
|
||||||
public async recv(id: string | null): Promise<RPCResponse> {
|
|
||||||
let index = 0; // index into receive qeueue
|
|
||||||
let startTime = performance.now();
|
|
||||||
|
|
||||||
return new Promise(async (resolve) => {
|
|
||||||
// used for non-blocking "wait-until" behavior
|
|
||||||
let rcv = () => {
|
|
||||||
let elapsedTime = performance.now() - startTime;
|
|
||||||
if (this.options.timeout !== null && elapsedTime >= this.options.timeout!) {
|
|
||||||
return resolve({
|
|
||||||
'e': 'Timed out while receiving message',
|
|
||||||
'i': id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that there is more data in the queue
|
|
||||||
if (index < this.receiveQueue.length) {
|
|
||||||
// get next message in queue
|
|
||||||
let r = this.receiveQueue[index];
|
|
||||||
index++;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let j = JSON.parse(r);
|
|
||||||
|
|
||||||
|
|
||||||
// check for matching id
|
|
||||||
if (id === null || 'i' in j && j['i'] === id) {
|
|
||||||
let response: RPCResponse;
|
|
||||||
// is response an error?
|
|
||||||
if ('e' in j) {
|
|
||||||
// decode error from base64
|
|
||||||
let error = JSON.parse(Buffer.from(j['e'], 'base64').toString('ascii'));
|
|
||||||
|
|
||||||
response = {
|
|
||||||
'i': id,
|
|
||||||
'e': error
|
|
||||||
};
|
|
||||||
|
|
||||||
this.responses.push(response);
|
|
||||||
return resolve(response);
|
|
||||||
} else {
|
|
||||||
response = j;
|
|
||||||
}
|
|
||||||
|
|
||||||
// trim start of queue (just processed)
|
|
||||||
this.receiveQueue = this.receiveQueue.slice(index);
|
|
||||||
|
|
||||||
// return response object
|
|
||||||
this.responses.push(response);
|
|
||||||
return resolve(response);
|
|
||||||
} else {
|
|
||||||
// not at end of queue, eager retry
|
|
||||||
setTimeout(rcv, 0);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// TODO: parse print statements somehow
|
|
||||||
//console.debug('Could not parse json response: "' + r + '"');
|
|
||||||
|
|
||||||
// not at end of queue, eager retry
|
|
||||||
setTimeout(rcv, 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no more data in queue, wait
|
|
||||||
// before attempting again
|
|
||||||
setTimeout(rcv, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
rcv();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,9 +180,9 @@ export default class HubManager {
|
|||||||
name = name || file;
|
name = name || file;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
||||||
const ack = await this.startWriteProgram(name, size, slotid, now, now);
|
const ack: {[key: string]: any} = await this.startWriteProgram(name, size, slotid, now, now);
|
||||||
const blockSize = ack['r'].blocksize;
|
const blockSize = ack.blocksize;
|
||||||
const transferid = ack['r'].transferid;
|
const transferid = ack.transferid;
|
||||||
|
|
||||||
const numBlocks = Math.ceil(size / blockSize);
|
const numBlocks = Math.ceil(size / blockSize);
|
||||||
|
|
||||||
@ -226,14 +207,17 @@ export default class HubManager {
|
|||||||
* @param `slotid` Slot ID of the program to run
|
* @param `slotid` Slot ID of the program to run
|
||||||
*/
|
*/
|
||||||
public async programExecute(slotid: number): Promise<RPCResponse> {
|
public async programExecute(slotid: number): Promise<RPCResponse> {
|
||||||
return Promise.resolve(
|
return new Promise(async (resolve) => {
|
||||||
await this.send({
|
let response = await this.send({
|
||||||
'm': 'program_execute',
|
'm': 'program_execute',
|
||||||
'p': {
|
'p': {
|
||||||
'slotid': slotid
|
'slotid': slotid
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,7 +269,7 @@ export default class HubManager {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'type': 'python', // always python
|
'type': 'python', // always python
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
'project_id': '50uN1ZaRpHj2', // TODO: check this
|
'project_id': HubManager.randomID(16),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
|
Loading…
Reference in New Issue
Block a user