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",
|
||||
"title": "Select Theme"
|
||||
},
|
||||
|
||||
{
|
||||
"command": "mind-reader.runLineContext",
|
||||
"title": "Run Line Context"
|
||||
},
|
||||
|
||||
{
|
||||
"command": "mind-reader.runCursorContext",
|
||||
"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": {
|
||||
"editor/context": [
|
||||
{
|
||||
@ -187,25 +210,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"accessActions": [
|
||||
{
|
||||
"id": "accessActions",
|
||||
"name": "Access Actions",
|
||||
"icon": "media/dep.svg",
|
||||
"contextualTitle": "Accessibility Menu Actions"
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "accessActions",
|
||||
"title": "Access Actions",
|
||||
"icon": "media/dep.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
"views": {
|
||||
"accessActions": [
|
||||
{
|
||||
"id": "accessActions",
|
||||
"name": "Access Actions",
|
||||
"icon": "media/dep.svg",
|
||||
"contextualTitle": "Accessibility Menu Actions"
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "accessActions",
|
||||
"title": "Access Actions",
|
||||
"icon": "media/dep.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
@ -216,17 +239,23 @@
|
||||
"test": "node ./out/test/runTest.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.60.0",
|
||||
"@types/glob": "^7.1.3",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/node": "14.x",
|
||||
"eslint": "^7.27.0",
|
||||
"@types/node": "16.x",
|
||||
"@types/serialport": "^8.0.2",
|
||||
"@types/vscode": "^1.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "^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",
|
||||
"mocha": "^8.4.0",
|
||||
"typescript": "^4.3.2",
|
||||
"vscode-test": "^1.5.2",
|
||||
"@vscode/test-electron": "^1.6.2"
|
||||
"vscode-test": "^1.5.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 pl from './pylex';
|
||||
import * as path from 'path';
|
||||
|
||||
import HubManager from './hubManager';
|
||||
|
||||
/**
|
||||
* @type {Object} Command // Command to register with the VS Code Extension API
|
||||
@ -54,10 +57,40 @@ const commands: Command[] = [
|
||||
name: 'mind-reader.runLineContext',
|
||||
callback: runLineContext,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'mind-reader.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 {
|
||||
let editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showErrorMessage("RunCursorContext: No Active Editor");
|
||||
vscode.window.showErrorMessage('RunCursorContext: No Active Editor');
|
||||
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;
|
||||
|
@ -3,7 +3,7 @@ import * as vscode from 'vscode';
|
||||
import * as pl from './pylex';
|
||||
import commands from './commands';
|
||||
|
||||
import AccessNodeProvider from './accessNodeProvider'
|
||||
import AccessNodeProvider from './accessNodeProvider';
|
||||
|
||||
let parser: pl.Parser = new pl.Parser();
|
||||
|
||||
@ -24,7 +24,6 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
let provider = new AccessNodeProvider();
|
||||
vscode.window.registerTreeDataProvider('accessActions', provider);
|
||||
|
||||
}
|
||||
|
||||
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