Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic implementation to support pybricks hubs #165

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions examples/pybricks_inventorhub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
*
* This demonstrates connecting a Spike Prime / Mindstorms Inventor Hub with pybricks firmware.
*
*/

const PoweredUP = require("..");

const poweredUP = new PoweredUP.PoweredUP();
poweredUP.scan(); // Start scanning for hubs

console.log("Looking for Hubs...");

poweredUP.on("discover", async (hub) => { // Wait to discover hubs
if(hub.type === PoweredUP.Consts.HubType.PYBRICKS_HUB) {
await hub.connect(); // If we found a hub with Pybricks firmware, connect to it
console.log(`Connected to ${hub.name}!`);

// If the hub transmits something, show it in the console
hub.on("recieve", (data) => { console.log(data.toString()) });

// Stop any running user program
await hub.stopUserProgram();

// Compiles the python code and uploads it as __main__
await hub.uploadUserProgram(`
from pybricks.hubs import InventorHub
hub = InventorHub() # We assume the connected hub is an Inventor hub
hub.display.text("Hello node-poweredup!") # Show on the led matrix of the hub
print("finished") # Transmit via bluetooth to the laptop
`);

// Run the user program that was uploaded on the hub
// Alternatively the user program can be started by pressing the button on the hub
await hub.startUserProgram();
}
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"dependencies": {
"@abandonware/noble": "1.9.2-15",
"compare-versions": "^4.1.3",
"debug": "^4.3.3"
"debug": "^4.3.3",
"@pybricks/mpy-cross-v6": "^2.0.0"
},
"devDependencies": {
"@types/debug": "4.1.7",
Expand Down
18 changes: 13 additions & 5 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* @property {number} TECHNIC_MEDIUM_HUB 6
* @property {number} MARIO 7
* @property {number} TECHNIC_SMALL_HUB 8
* @property {number} PYBRICKS_HUB 100
*/
export enum HubType {
UNKNOWN = 0,
Expand All @@ -20,6 +21,7 @@ export enum HubType {
TECHNIC_MEDIUM_HUB = 6,
MARIO = 7,
TECHNIC_SMALL_HUB = 8,
PYBRICKS_HUB = 100,
}


Expand Down Expand Up @@ -214,14 +216,16 @@ export enum BLEService {
WEDO2_SMART_HUB_2 = "00004f0e-1212-efde-1523-785feabcd123",
WEDO2_SMART_HUB_3 = "2a19",
WEDO2_SMART_HUB_4 = "180f",
WEDO2_SMART_HUB_5 = "180a",
LPF2_HUB = "00001623-1212-efde-1623-785feabcd123"
STANDARD_DEVICE_INFORMATION = "180a",
LPF2_HUB = "00001623-1212-efde-1623-785feabcd123",
PYBRICKS_HUB = "c5f50001-8280-46da-89f4-6d8051e4aeef",
PYBRICKS_NUS = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
}


export enum BLECharacteristic {
WEDO2_BATTERY = "2a19",
WEDO2_FIRMWARE_REVISION = "2a26",
STANDARD_BATTERY = "2a19",
STANDARD_FIRMWARE_REVISION = "2a26",
WEDO2_BUTTON = "00001526-1212-efde-1523-785feabcd123", // "1526"
WEDO2_PORT_TYPE = "00001527-1212-efde-1523-785feabcd123", // "1527" // Handles plugging and unplugging of devices on WeDo 2.0 Smart Hub
WEDO2_LOW_VOLTAGE_ALERT = "00001528-1212-efde-1523-785feabcd123", // "1528"
Expand All @@ -233,7 +237,11 @@ export enum BLECharacteristic {
WEDO2_PORT_TYPE_WRITE = "00001563-1212-efde-1523-785feabcd123", // "1563"
WEDO2_MOTOR_VALUE_WRITE = "00001565-1212-efde-1523-785feabcd123", // "1565"
WEDO2_NAME_ID = "00001524-1212-efde-1523-785feabcd123", // "1524"
LPF2_ALL = "00001624-1212-efde-1623-785feabcd123"
LPF2_ALL = "00001624-1212-efde-1623-785feabcd123",
PYBRICKS_COMMAND_EVENT = "c5f50002-8280-46da-89f4-6d8051e4aeef",
PYBRICKS_CAPABILITIES = "c5f50003-8280-46da-89f4-6d8051e4aeef",
PYBRICKS_NUS_RX = "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
PYBRICKS_NUS_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
}


Expand Down
131 changes: 131 additions & 0 deletions src/hubs/pybrickshub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Peripheral } from "@abandonware/noble";
import compareVersion from "compare-versions";
import { compile } from "@pybricks/mpy-cross-v6";
import { IBLEAbstraction } from "../interfaces";
import { BaseHub } from "./basehub";
import * as Consts from "../consts";
import Debug = require("debug");
const debug = Debug("pybrickshub");


/**
* The PybricksHub is emitted if the discovered device is a hub with Pybricks firmware installed.
* To flash your hub with Pybricks firmware, follow the instructions from https://pybricks.com.
* The class supports hubs with Pybricks version 3.2.0 or newer.
* @class PybricksHub
* @extends BaseHub
*/
export class PybricksHub extends BaseHub {
private _maxCharSize: number = 100; // overwritten by value from capabilities characteristic
private _maxUserProgramSize: number = 16000; // overwritten by value from capabilities characteristic

public static IsPybricksHub (peripheral: Peripheral) {
return (
peripheral.advertisement &&
peripheral.advertisement.serviceUuids &&
peripheral.advertisement.serviceUuids.indexOf(Consts.BLEService.PYBRICKS_HUB.replace(/-/g, "")) >= 0
);
}


constructor (device: IBLEAbstraction) {
super(device, PortMap, Consts.HubType.PYBRICKS_HUB);
debug("Discovered Pybricks Hub");
}


public connect () {
return new Promise<void>(async (resolve) => {
debug("Connecting to Pybricks Hub");
await super.connect();
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.PYBRICKS_HUB);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.PYBRICKS_NUS);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.STANDARD_DEVICE_INFORMATION);
await new Promise<void>(async (resolve) => this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.STANDARD_FIRMWARE_REVISION, (err, data) => {
if (data) {
this._firmwareVersion = data.toString();
this._checkFirmware(this._firmwareVersion);
debug("Firmware version ", this._firmwareVersion);
return resolve();
}
}));
await new Promise<void>(async (resolve) => this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.PYBRICKS_CAPABILITIES, (err, data) => {
if (data) {
this._maxCharSize = data.readUInt16LE(0);
this._maxUserProgramSize = data.readUInt32LE(6);
debug("Recieved capabilities ", data, " maxCharSize: ", this._maxCharSize, " maxUserProgramSize: ", this._maxUserProgramSize);
return resolve();
}
}));
await this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.PYBRICKS_NUS_TX, this._parseMessage.bind(this));
debug("Connect completed");
this.emit("connect");
resolve();
});
}

protected _checkFirmware (version: string) {
if (compareVersion.validate(version) && compareVersion('3.2.0', version) === 1) {
throw new Error(`Your Hub's (${this.name}) firmware is out of date and unsupported by this library. Please update it via the official Pybricks website.`);
}
}

private _parseMessage (data?: Buffer) {
debug("Received Message (PYBRICKS_NUS_TX)", data);
this.emit("recieve", data);
}

public send (message: Buffer, uuid: string = Consts.BLECharacteristic.PYBRICKS_NUS_RX) {
debug(`Send Message (${uuid})`, message);
return this._bleDevice.writeToCharacteristic(uuid, message);
}

public uploadUserProgram (pythonCode: string) {
debug("Compiling Python User Program", pythonCode);
return compile('userProgram.py', pythonCode).then(async (result) => {
if(result.mpy) {
const multiFileBlob = Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('__main__\0'), result.mpy]);
multiFileBlob.writeUInt32LE(result.mpy.length);
if(multiFileBlob.length > this._maxUserProgramSize) {
throw new Error(`User program size ${multiFileBlob.length} larger than maximum ${this._maxUserProgramSize}`);
}
debug("Uploading Python User Program", multiFileBlob);
await this.writeUserProgramMeta(0);
const chunkSize = this._maxCharSize - 5;
for (let i = 0; i < multiFileBlob.length; i += chunkSize) {
const chunk = multiFileBlob.slice(i, i + chunkSize);
await this.writeUserRam(i, Buffer.from(chunk));
}
await this.writeUserProgramMeta(multiFileBlob.length);
debug("Finished uploading");
}
else throw new Error(`Compiling Python User Program failed: ${result.err}`);
});
}

public stopUserProgram () {
debug("Stopping Python User Program");
return this.send(Buffer.from([0]), Consts.BLECharacteristic.PYBRICKS_COMMAND_EVENT);
}

public startUserProgram () {
debug("Starting Python User Program");
return this.send(Buffer.from([1]), Consts.BLECharacteristic.PYBRICKS_COMMAND_EVENT);
}

private writeUserProgramMeta (programLength: number) {
const message = Buffer.alloc(5);
message[0] = 3;
message.writeUInt32LE(programLength, 1);
return this.send(message, Consts.BLECharacteristic.PYBRICKS_COMMAND_EVENT);
}

private writeUserRam (offset: number, payload: Buffer) {
const message = Buffer.concat([Buffer.from([4, 0, 0, 0, 0]), payload]);
message.writeUInt32LE(offset, 1);
return this.send(message, Consts.BLECharacteristic.PYBRICKS_COMMAND_EVENT);
}
}

export const PortMap: {[portName: string]: number} = {
};
8 changes: 4 additions & 4 deletions src/hubs/wedo2smarthub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class WeDo2SmartHub extends BaseHub {
if (!isWebBluetooth) {
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_3);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_4);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.WEDO2_SMART_HUB_5);
await this._bleDevice.discoverCharacteristicsForService(Consts.BLEService.STANDARD_DEVICE_INFORMATION);
} else {
await this._bleDevice.discoverCharacteristicsForService("battery_service");
await this._bleDevice.discoverCharacteristicsForService("device_information");
Expand All @@ -61,8 +61,8 @@ export class WeDo2SmartHub extends BaseHub {
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_SENSOR_VALUE, this._parseSensorMessage.bind(this));
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_BUTTON, this._parseSensorMessage.bind(this));
if (!isWebBluetooth) {
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY, this._parseBatteryMessage.bind(this));
this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.WEDO2_BATTERY, (err, data) => {
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.STANDARD_BATTERY, this._parseBatteryMessage.bind(this));
this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.STANDARD_BATTERY, (err, data) => {
if (data) {
this._parseBatteryMessage(data);
}
Expand All @@ -77,7 +77,7 @@ export class WeDo2SmartHub extends BaseHub {
}
this._bleDevice.subscribeToCharacteristic(Consts.BLECharacteristic.WEDO2_HIGH_CURRENT_ALERT, this._parseHighCurrentAlert.bind(this));
if (!isWebBluetooth) {
this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.WEDO2_FIRMWARE_REVISION, (err, data) => {
this._bleDevice.readFromCharacteristic(Consts.BLECharacteristic.STANDARD_FIRMWARE_REVISION, (err, data) => {
if (data) {
this._parseFirmwareRevisionString(data);
}
Expand Down
2 changes: 2 additions & 0 deletions src/index-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MoveHub } from "./hubs/movehub";
import { RemoteControl } from "./hubs/remotecontrol";
import { TechnicMediumHub } from "./hubs/technicmediumhub";
import { WeDo2SmartHub } from "./hubs/wedo2smarthub";
import { PybricksHub } from "./hubs/pybrickshub";

import { ColorDistanceSensor } from "./devices/colordistancesensor";
import { CurrentSensor } from "./devices/currentsensor";
Expand Down Expand Up @@ -58,6 +59,7 @@ export {
Hub,
RemoteControl,
DuploTrainBase,
PybricksHub,
Consts,
Color,
ColorDistanceSensor,
Expand Down
5 changes: 5 additions & 0 deletions src/poweredup-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MoveHub } from "./hubs/movehub";
import { RemoteControl } from "./hubs/remotecontrol";
import { TechnicMediumHub } from "./hubs/technicmediumhub";
import { WeDo2SmartHub } from "./hubs/wedo2smarthub";
import { PybricksHub } from "./hubs/pybrickshub";

import * as Consts from "./consts";

Expand All @@ -25,6 +26,8 @@ let wantScan = false;

const startScanning = () => {
noble.startScanning([
Consts.BLEService.PYBRICKS_HUB,
Consts.BLEService.PYBRICKS_HUB.replace(/-/g, ""),
Consts.BLEService.LPF2_HUB,
Consts.BLEService.LPF2_HUB.replace(/-/g, ""),
Consts.BLEService.WEDO2_SMART_HUB,
Expand Down Expand Up @@ -172,6 +175,8 @@ export class PoweredUP extends EventEmitter {
hub = new TechnicMediumHub(device);
} else if (Mario.IsMario(peripheral)) {
hub = new Mario(device);
} else if (PybricksHub.IsPybricksHub(peripheral)) {
hub = new PybricksHub(device);
} else {
return;
}
Expand Down