mirror of
https://github.com/We-Dont-Byte/Mind_Reader.git
synced 2024-11-15 03:35:59 +00:00
Implement HubController
This commit is contained in:
parent
e3e9a218df
commit
8ac7e2c5ea
4415
package-lock.json
generated
4415
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
143
package.json
143
package.json
@ -47,48 +47,71 @@
|
|||||||
"command": "mind-reader.selectTheme",
|
"command": "mind-reader.selectTheme",
|
||||||
"title": "Select Theme"
|
"title": "Select Theme"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"command": "mind-reader.runLineContext",
|
"command": "mind-reader.runLineContext",
|
||||||
"title": "Run Line Context"
|
"title": "Run Line Context"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"command": "mind-reader.runCursorContext",
|
"command": "mind-reader.runCursorContext",
|
||||||
"title": "Run Cursor Context"
|
"title": "Run Cursor Context"
|
||||||
}
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "mind-reader.connectHub",
|
||||||
|
"title": "Connect LEGO Hub"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "mind-reader.disconnectHub",
|
||||||
|
"title": "Disconnect LEGO Hub"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "mind-reader.uploadCurrentFile",
|
||||||
|
"title": "Upload current file to LEGO Hub"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "mind-reader.runProgram",
|
||||||
|
"title": "Run a program from the LEGO Hub"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "mind-reader.stopExecution",
|
||||||
|
"title": "Stop running program on the LEGO Hub"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "mind-reader.deleteProgram",
|
||||||
|
"title": "Delete a program from the LEGO Hub"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keybindings": [
|
||||||
|
{
|
||||||
|
"command": "mind-reader.decreaseFontScale",
|
||||||
|
"key": "numpad_subtract",
|
||||||
|
"mac": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "mind-reader.increaseFontScale",
|
||||||
|
"key": "numpad_add",
|
||||||
|
"mac": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "mind-reader.increaseEditorScale",
|
||||||
|
"key": "shift+numpad_add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "mind-reader.decreaseEditorScale",
|
||||||
|
"key": "shift+numpad_subtract",
|
||||||
|
"mac": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "mind-reader.resetEditorScale",
|
||||||
|
"key": "shift+enter",
|
||||||
|
"mac": ""
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"keybindings": [
|
|
||||||
{
|
|
||||||
"command": "mind-reader.decreaseFontScale",
|
|
||||||
"key": "numpad_subtract",
|
|
||||||
"mac": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"command": "mind-reader.increaseFontScale",
|
|
||||||
"key": "numpad_add",
|
|
||||||
"mac": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"command": "mind-reader.increaseEditorScale",
|
|
||||||
"key": "shift+numpad_add"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"command": "mind-reader.decreaseEditorScale",
|
|
||||||
"key": "shift+numpad_subtract",
|
|
||||||
"mac": ""
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"command": "mind-reader.resetEditorScale",
|
|
||||||
"key": "shift+enter",
|
|
||||||
"mac": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"menus": {
|
"menus": {
|
||||||
"editor/context": [
|
"editor/context": [
|
||||||
{
|
{
|
||||||
@ -187,25 +210,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"accessActions": [
|
"accessActions": [
|
||||||
{
|
{
|
||||||
"id": "accessActions",
|
"id": "accessActions",
|
||||||
"name": "Access Actions",
|
"name": "Access Actions",
|
||||||
"icon": "media/dep.svg",
|
"icon": "media/dep.svg",
|
||||||
"contextualTitle": "Accessibility Menu Actions"
|
"contextualTitle": "Accessibility Menu Actions"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"viewsContainers": {
|
"viewsContainers": {
|
||||||
"activitybar": [
|
"activitybar": [
|
||||||
{
|
{
|
||||||
"id": "accessActions",
|
"id": "accessActions",
|
||||||
"title": "Access Actions",
|
"title": "Access Actions",
|
||||||
"icon": "media/dep.svg"
|
"icon": "media/dep.svg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vscode:prepublish": "npm run compile",
|
"vscode:prepublish": "npm run compile",
|
||||||
@ -216,17 +239,23 @@
|
|||||||
"test": "node ./out/test/runTest.js"
|
"test": "node ./out/test/runTest.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.60.0",
|
|
||||||
"@types/glob": "^7.1.3",
|
"@types/glob": "^7.1.3",
|
||||||
"@types/mocha": "^8.2.2",
|
"@types/mocha": "^8.2.2",
|
||||||
"@types/node": "14.x",
|
"@types/node": "16.x",
|
||||||
"eslint": "^7.27.0",
|
"@types/serialport": "^8.0.2",
|
||||||
|
"@types/vscode": "^1.60.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||||
"@typescript-eslint/parser": "^4.26.0",
|
"@typescript-eslint/parser": "^4.26.0",
|
||||||
|
"@vscode/test-electron": "^1.6.2",
|
||||||
|
"electron": "^13.5.2",
|
||||||
|
"electron-rebuild": "^3.2.5",
|
||||||
|
"eslint": "^7.27.0",
|
||||||
"glob": "^7.1.7",
|
"glob": "^7.1.7",
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^8.4.0",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.3.2",
|
||||||
"vscode-test": "^1.5.2",
|
"vscode-test": "^1.5.2"
|
||||||
"@vscode/test-electron": "^1.6.2"
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"serialport": "^9.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
178
src/commands.ts
178
src/commands.ts
@ -1,5 +1,8 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as pl from './pylex';
|
import * as pl from './pylex';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import HubManager from './hubManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Object} Command // Command to register with the VS Code Extension API
|
* @type {Object} Command // Command to register with the VS Code Extension API
|
||||||
@ -54,10 +57,40 @@ const commands: Command[] = [
|
|||||||
name: 'mind-reader.runLineContext',
|
name: 'mind-reader.runLineContext',
|
||||||
callback: runLineContext,
|
callback: runLineContext,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'mind-reader.runCursorContext',
|
name: 'mind-reader.runCursorContext',
|
||||||
|
|
||||||
callback: runCursorContext
|
callback: runCursorContext
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mind-reader.connectHub',
|
||||||
|
callback: connectHub
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mind-reader.disconnectHub',
|
||||||
|
callback: disconnectHub
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mind-reader.uploadCurrentFile',
|
||||||
|
callback: uploadCurrentFile
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mind-reader.runProgram',
|
||||||
|
callback: runProgram
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mind-reader.stopExecution',
|
||||||
|
callback: stopExecution
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'mind-reader.deleteProgram',
|
||||||
|
callback: deleteProgram
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -145,7 +178,7 @@ function createContextString(context: pl.LexNode[], line: number): string {
|
|||||||
function runCursorContext(): void {
|
function runCursorContext(): void {
|
||||||
let editor = vscode.window.activeTextEditor;
|
let editor = vscode.window.activeTextEditor;
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
vscode.window.showErrorMessage("RunCursorContext: No Active Editor");
|
vscode.window.showErrorMessage('RunCursorContext: No Active Editor');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,4 +239,145 @@ function runCursorContext(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Current connected hub
|
||||||
|
let hub: HubManager | null;
|
||||||
|
|
||||||
|
// TODO: port option
|
||||||
|
async function connectHub(): Promise<void> {
|
||||||
|
if (hub) {
|
||||||
|
vscode.window.showWarningMessage('LEGO Hub is already connected, reconnecting...');
|
||||||
|
hub.close();
|
||||||
|
}
|
||||||
|
const config = vscode.workspace.getConfiguration();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (config.get('mindreader.connection.connectAutomatically')) {
|
||||||
|
hub = await HubManager.create();
|
||||||
|
vscode.window.showInformationMessage('LEGO Hub connected');
|
||||||
|
} else {
|
||||||
|
const ports = await HubManager.queryPorts();
|
||||||
|
|
||||||
|
if (ports.length === 0) {
|
||||||
|
vscode.window.showErrorMessage('No ports found. Is the LEGO Hub connected?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let slots: vscode.QuickPickItem[] = [];
|
||||||
|
for (const port of ports) {
|
||||||
|
slots.push({ label: port.path });
|
||||||
|
}
|
||||||
|
|
||||||
|
let picked = await vscode.window.showQuickPick(slots);
|
||||||
|
|
||||||
|
if (!picked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hub = await HubManager.create({ port: picked.label });
|
||||||
|
vscode.window.showInformationMessage('LEGO Hub connected');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: better handling
|
||||||
|
vscode.window.showErrorMessage('Could not connect to LEGO Hub');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disconnectHub(): Promise<void> {
|
||||||
|
if (!hub) {
|
||||||
|
vscode.window.showErrorMessage('LEGO Hub is not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hub.close();
|
||||||
|
hub = null;
|
||||||
|
vscode.window.showInformationMessage('LEGO Hub disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadCurrentFile(): Promise<void> {
|
||||||
|
if (!hub) {
|
||||||
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vscode.window.activeTextEditor) {
|
||||||
|
vscode.window.showErrorMessage('No active text editor');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFilePath = vscode.window.activeTextEditor.document.fileName;
|
||||||
|
|
||||||
|
if (currentFilePath) {
|
||||||
|
// construct quickpick
|
||||||
|
const slots: vscode.QuickPickItem[] = [];
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
slots.push({ label: i.toString() });
|
||||||
|
}
|
||||||
|
const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false });
|
||||||
|
|
||||||
|
if (!slotID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: progress bar?
|
||||||
|
vscode.window.showInformationMessage('Uploading current file');
|
||||||
|
await hub.uploadFile(currentFilePath, parseInt(slotID.label), path.basename(currentFilePath));
|
||||||
|
vscode.window.showInformationMessage(path.basename(currentFilePath) + ' uploaded to slot ' + slotID.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: find empty slots
|
||||||
|
async function runProgram(): Promise<void> {
|
||||||
|
if (!hub) {
|
||||||
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots: vscode.QuickPickItem[] = [];
|
||||||
|
// construct quickpick
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
slots.push({ label: i.toString() });
|
||||||
|
}
|
||||||
|
const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false });
|
||||||
|
|
||||||
|
if (!slotID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vscode.window.showInformationMessage('Running program ' + slotID.label);
|
||||||
|
await hub.programExecute(parseInt(slotID.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopExecution(): Promise<void> {
|
||||||
|
if (!hub) {
|
||||||
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hub.programTerminate();
|
||||||
|
vscode.window.showInformationMessage('Execution stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: find slots from status
|
||||||
|
async function deleteProgram(): Promise<void> {
|
||||||
|
if (!hub) {
|
||||||
|
vscode.window.showErrorMessage('LEGO Hub is not connected!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots: vscode.QuickPickItem[] = [];
|
||||||
|
// construct quickpick
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
slots.push({ label: i.toString() });
|
||||||
|
}
|
||||||
|
const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false });
|
||||||
|
|
||||||
|
if (!slotID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hub.deleteProgram(parseInt(slotID.label));
|
||||||
|
vscode.window.showInformationMessage('Deleted program ' + slotID.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default commands;
|
export default commands;
|
||||||
|
@ -3,7 +3,7 @@ import * as vscode from 'vscode';
|
|||||||
import * as pl from './pylex';
|
import * as pl from './pylex';
|
||||||
import commands from './commands';
|
import commands from './commands';
|
||||||
|
|
||||||
import AccessNodeProvider from './accessNodeProvider'
|
import AccessNodeProvider from './accessNodeProvider';
|
||||||
|
|
||||||
let parser: pl.Parser = new pl.Parser();
|
let parser: pl.Parser = new pl.Parser();
|
||||||
|
|
||||||
@ -24,7 +24,6 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
let provider = new AccessNodeProvider();
|
let provider = new AccessNodeProvider();
|
||||||
vscode.window.registerTreeDataProvider('accessActions', provider);
|
vscode.window.registerTreeDataProvider('accessActions', provider);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {}
|
export function deactivate() {}
|
||||||
|
455
src/hubManager.ts
Normal file
455
src/hubManager.ts
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as SerialPort from 'serialport';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type RPCRequest an RPC request message
|
||||||
|
*
|
||||||
|
* @prop {string} `'m'` method to invoke.
|
||||||
|
* @prop {Object?} `'p'` optional parameters for method.
|
||||||
|
* @prop {string|null?} `'i'` optional request ID.
|
||||||
|
*/
|
||||||
|
type RPCRequest = {
|
||||||
|
'm': string;
|
||||||
|
'p'?: Object;
|
||||||
|
'i'?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type RPCResponse an RPC response message
|
||||||
|
*
|
||||||
|
* @prop {any?} `'r'` RPC response body
|
||||||
|
* @prop {Object?} `'e'` RPC error body
|
||||||
|
* @prop {string|null} `'i'` required response ID.
|
||||||
|
*/
|
||||||
|
type RPCResponse = {
|
||||||
|
'r'?: any;
|
||||||
|
'e'?: Object;
|
||||||
|
'i': string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type HubOptions Connection options
|
||||||
|
*
|
||||||
|
* @prop {boolean=} `magic` automatically try and find a suitable port to connect. Defaults to `true`
|
||||||
|
* @prop {string=} `port` port to use if `magic` is disabled. Defaults to `'/dev/ttyACM0'`.
|
||||||
|
* @prop {number|null=} `timeout` how long to wait for responses from the Hub.
|
||||||
|
*/
|
||||||
|
type HubOptions = {
|
||||||
|
magic?: boolean;
|
||||||
|
port?: string;
|
||||||
|
timeout?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages sending and receiving of JSON Remote Procedure Call (JSON-RPC) protocol messages
|
||||||
|
* to the Hub.
|
||||||
|
*/
|
||||||
|
export default class HubManager {
|
||||||
|
private port: SerialPort;
|
||||||
|
private receiveQueue: string[] = []; // queue of received messages to handle
|
||||||
|
public responses: RPCResponse[] = []; // list of messages returned to the user
|
||||||
|
|
||||||
|
// ======================== INSTANCE METHODS ========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor, use static `create` + `init`
|
||||||
|
*/
|
||||||
|
private constructor(public options: HubOptions) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a created HubManager with the current option settings
|
||||||
|
*/
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.port = new SerialPort(
|
||||||
|
this.options.port!,
|
||||||
|
{
|
||||||
|
autoOpen: true,
|
||||||
|
baudRate: 112500,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// error during port opening
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.port.setEncoding('utf-8');
|
||||||
|
|
||||||
|
// push lines received to data queue
|
||||||
|
|
||||||
|
let rl = this.port.pipe(new SerialPort.parsers.Readline({delimiter: '\r'}));
|
||||||
|
rl.on('data', data => this.receiveQueue.push(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close(): Promise<void> {
|
||||||
|
this.port.close(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an RPC message and get the corresponding response.
|
||||||
|
*
|
||||||
|
* @param `proc` Procedure to execute
|
||||||
|
* @param `params` Optional parameters for the procedure
|
||||||
|
* @param `id` The ID to use for the RPC message. Use null to indicate no ID/notification message.
|
||||||
|
* If neither a string or `null` is passed, an ID is automatically generated.
|
||||||
|
*/
|
||||||
|
// TODO: make send take a single RPCRequest argument, made inline in each function
|
||||||
|
public async send(request: RPCRequest): Promise<RPCResponse> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (request['i'] === undefined) {
|
||||||
|
// generate an ID
|
||||||
|
request['i'] = 'mind-reader-' + HubManager.randomID();
|
||||||
|
}
|
||||||
|
|
||||||
|
// write JSON to port
|
||||||
|
this.port.write(JSON.stringify(request));
|
||||||
|
this.port.write('\r', async () => {
|
||||||
|
if (request['i']) {
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async uploadFile(file: string, slotid: number, name?: string, autoStart: boolean = false) {
|
||||||
|
const data = fs.readFileSync(file, 'utf8');
|
||||||
|
const size = data.length;
|
||||||
|
name = name || file;
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
const ack = await this.startWriteProgram(name, size, slotid, now, now);
|
||||||
|
const blockSize = ack['r'].blocksize;
|
||||||
|
const transferid = ack['r'].transferid;
|
||||||
|
|
||||||
|
const numBlocks = Math.ceil(size / blockSize);
|
||||||
|
|
||||||
|
for (let i = 0; i < numBlocks; i++) {
|
||||||
|
const dataChunk = data.substring(i*blockSize, i*blockSize + blockSize);
|
||||||
|
await this.writePackage(dataChunk, transferid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoStart) {
|
||||||
|
return Promise.resolve(await this.programExecute(slotid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= Hub Methods =========================
|
||||||
|
// TODO: only spike is supported rn -> m_strType
|
||||||
|
//
|
||||||
|
// These methods each handle a single RPC method's communication.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a program that is saved on the hub
|
||||||
|
*
|
||||||
|
* @param `slotid` Slot ID of the program to run
|
||||||
|
*/
|
||||||
|
public async programExecute(slotid: number): Promise<RPCResponse> {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({
|
||||||
|
'm': 'program_execute',
|
||||||
|
'p': {
|
||||||
|
'slotid': slotid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminate a currently running program
|
||||||
|
*/
|
||||||
|
public async programTerminate(): Promise<RPCResponse> {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({ 'm': 'program_terminate' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the storage status of the hub
|
||||||
|
*
|
||||||
|
* TODO: fill with actual example
|
||||||
|
* slot
|
||||||
|
* decoded name
|
||||||
|
* size
|
||||||
|
* last modified
|
||||||
|
* project_id
|
||||||
|
* type
|
||||||
|
*/
|
||||||
|
public async getStorageStatus(): Promise<RPCResponse> {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({ 'm': 'get_storage_status' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the Hub that a program write is about to occur.
|
||||||
|
*
|
||||||
|
* @param `name` Name of the program.
|
||||||
|
* @param `name` Size of the program TODO: in bytes?
|
||||||
|
* @param `slotID` Slot ID to write the program to
|
||||||
|
* @param `created` Creation timestamp
|
||||||
|
* @param `modified` Modified timestamp
|
||||||
|
*/
|
||||||
|
public async startWriteProgram(
|
||||||
|
name: string,
|
||||||
|
size: number,
|
||||||
|
slotID: number,
|
||||||
|
created: number,
|
||||||
|
modified: number
|
||||||
|
): Promise<RPCResponse> {
|
||||||
|
// file meta data
|
||||||
|
let stat = {
|
||||||
|
'created': created,
|
||||||
|
'modified': modified,
|
||||||
|
'name': name,
|
||||||
|
'type': 'python', // always python
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'project_id': '50uN1ZaRpHj2', // TODO: check this
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({
|
||||||
|
'm': 'start_write_program',
|
||||||
|
'p': {
|
||||||
|
'slotid': slotID,
|
||||||
|
'size': size,
|
||||||
|
'meta': stat
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a package chunk of a file
|
||||||
|
*
|
||||||
|
* @param `data` Chunk of data to write
|
||||||
|
* @param `transferID` The transfer id for the current transfer
|
||||||
|
*/
|
||||||
|
public async writePackage(data: string, transferID: string): Promise<RPCResponse> {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({
|
||||||
|
'm': 'write_package',
|
||||||
|
'p': {
|
||||||
|
'data': Buffer.from(data, 'utf-8').toString('base64'),
|
||||||
|
'transferid': transferID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a program from slot `fromSlot` to slot `toSlot`.
|
||||||
|
* TODO: verify
|
||||||
|
* If the destination already has a program stored in it, the two programs
|
||||||
|
* are swapped
|
||||||
|
*
|
||||||
|
* @param `fromSlot` slot to move from
|
||||||
|
* @param `toSlot` slot to move to
|
||||||
|
*/
|
||||||
|
public async moveProgram(oldSlotID: number, newSlotID: number) {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({
|
||||||
|
'm': 'move_project',
|
||||||
|
'p': {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'old_slotid': oldSlotID,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'new_slotid': newSlotID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a program from the slot on the Hub
|
||||||
|
*
|
||||||
|
* @param `slotID` the slot to delete
|
||||||
|
*/
|
||||||
|
public async deleteProgram(slotID: number) {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({
|
||||||
|
'm': 'remove_project',
|
||||||
|
'p': {
|
||||||
|
'slotid': slotID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get firmware info for the connected device
|
||||||
|
*/
|
||||||
|
public async getHubInfo() {
|
||||||
|
return Promise.resolve(
|
||||||
|
await this.send({
|
||||||
|
'm': 'get_hub_info'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================== INSTANCE METHODS ========================
|
||||||
|
|
||||||
|
public static async create(options?: HubOptions): Promise<HubManager> {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
// merge passed options into default options
|
||||||
|
options = {
|
||||||
|
port: '/dev/ttyACM0',
|
||||||
|
magic: true,
|
||||||
|
timeout: 2500,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
let mgr: HubManager;
|
||||||
|
|
||||||
|
// try to detect port automatically
|
||||||
|
if (options.magic) {
|
||||||
|
const availablePorts = await HubManager.queryPorts();
|
||||||
|
|
||||||
|
// get paths from port information
|
||||||
|
const portPaths = availablePorts.map(x => x.path);
|
||||||
|
|
||||||
|
if (portPaths.length > 0) {
|
||||||
|
// try to establish connections to found ports
|
||||||
|
for (const port of portPaths) {
|
||||||
|
try {
|
||||||
|
mgr = new HubManager({ ...options, port });
|
||||||
|
await mgr.init();
|
||||||
|
|
||||||
|
return resolve(mgr);
|
||||||
|
} catch (err) {
|
||||||
|
// could not connect to port, try next port
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: better error, this will do for now
|
||||||
|
vscode.window.showErrorMessage('Mind_Reader: Magic is enabled but no ports were found. Is the Hub plugged in and turned on');
|
||||||
|
}
|
||||||
|
|
||||||
|
// magic disabled or failed, try normally
|
||||||
|
try {
|
||||||
|
mgr = new HubManager(options);
|
||||||
|
await mgr.init();
|
||||||
|
|
||||||
|
return resolve(mgr);
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
// could not connect to port
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of serial devices
|
||||||
|
* advertising their manufacturer
|
||||||
|
* as `LEGO System A/S`
|
||||||
|
*/
|
||||||
|
public static async queryPorts() {
|
||||||
|
// get all ports
|
||||||
|
let ports = await SerialPort.list();
|
||||||
|
|
||||||
|
// filter by manufacturer
|
||||||
|
ports = ports.filter(x => x.manufacturer === 'LEGO System A/S');
|
||||||
|
return Promise.resolve(ports);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random identifier consisting of lowercase
|
||||||
|
* letters, numbers, and underscores
|
||||||
|
*
|
||||||
|
* @param `len` The length of the string to generate
|
||||||
|
*/
|
||||||
|
static randomID(len: number = 4): string {
|
||||||
|
let result = '';
|
||||||
|
let characters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
result += characters[Math.floor(Math.random() * characters.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user