Skip to content

Commit

Permalink
Merge pull request #37815 from code-dot-org/chromeserialport-microbit
Browse files Browse the repository at this point in the history
micro:bit on Chromebooks
  • Loading branch information
Erin Peach committed Dec 1, 2020
2 parents c0867b3 + 3ff8135 commit 72f732e
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 30 deletions.
@@ -1,8 +1,6 @@
/** @file Board controller for Adafruit Circuit Playground */
/* global SerialPort */ // Maybe provided by the Code.org Browser
import _ from 'lodash';
import {EventEmitter} from 'events'; // provided by webpack's node-libs-browser
import ChromeSerialPort from 'chrome-serialport';
import five from '@code-dot-org/johnny-five';
import Playground from 'playground-io';
import experiments from '@cdo/apps/util/experiments';
Expand All @@ -25,6 +23,7 @@ import Led from './Led';
import {isNodeSerialAvailable} from '../../portScanning';
import PlaygroundButton from './Button';
import {detectBoardTypeFromPort, BOARD_TYPE} from '../../util/boardUtils';
import {serialPortType} from '../../util/browserChecks';

// Polyfill node's process.hrtime for the browser, gets used by johnny-five.
process.hrtime = require('browser-process-hrtime');
Expand Down Expand Up @@ -339,17 +338,7 @@ export default class CircuitPlaygroundBoard extends EventEmitter {
* @return {SerialPort}
*/
static openSerialPort(portName) {
// A gotcha here: These two types of SerialPort provide similar, but not
// exactly equivalent, interfaces. When making changes to construction
// here maker sure to test both paths:
//
// Code.org Browser case: Native Node SerialPort 6 is available on window.
//
// Code.org connector app case: ChromeSerialPort bridges through the Chrome
// app, implements SerialPort 3's interface.
const SerialPortType = isNodeSerialAvailable()
? SerialPort
: ChromeSerialPort.SerialPort;
const SerialPortType = serialPortType();

const port = new SerialPortType(portName, {
baudRate: SERIAL_BAUD
Expand Down
38 changes: 36 additions & 2 deletions apps/src/lib/kits/maker/boards/microBit/MBFirmataWrapper.js
@@ -1,5 +1,6 @@
import MBFirmataClient from '../../../../../third-party/maker/MBFirmataClient';
import {SAMPLE_INTERVAL} from './MicroBitConstants';
import {isNodeSerialAvailable} from '../../portScanning';

export const ACCEL_EVENT_ID = 27;

Expand All @@ -9,14 +10,47 @@ export default class MicrobitFirmataWrapper extends MBFirmataClient {
this.digitalCallbacks = [];
}

connectBoard() {
connectBoard(port) {
return Promise.resolve()
.then(() => this.connect())
.then(() => this.setSerialPort(port))
.then(() => {
return this.setAnalogSamplingInterval(SAMPLE_INTERVAL);
});
}

// Used in setSerialPort, which is copied from setSerialPort in MBFirmataClient,
// as a wrapper. Lifted into its own function because of linting.
dataReceived(data) {
if (this.inbufCount + data.length < this.inbuf.length) {
this.inbuf.set(data, this.inbufCount);
this.inbufCount += data.length;
this.processFirmatMessages();
}
}

setSerialPort(port) {
if (isNodeSerialAvailable()) {
return super.setSerialPort(port);
} else {
// Use the given port. Assume the port has been opened by the caller.

this.myPort = port;
this.myPort.on('data', this.dataReceived.bind(this));
this.requestFirmataVersion();
this.requestFirmwareVersion();

// get the board serial number (used to determine board version)
this.boardVersion = '';

// Above code is directly from setSerialPort in MBFirmataClient.
// Returning an empty promise below because Chrome Serial Port doesn't return
// .list() as a promise, as expected in MBFirmataClient. Because of the empty
// promise we don't set this.boardVersion. As of this edit, we do not use the
// boardVersion in our MB integration so no impact.
return Promise.resolve();
}
}

setPinMode(pin, mode) {
// If setting a pin to input, start tracking it immediately
if (mode === 0) {
Expand Down
51 changes: 47 additions & 4 deletions apps/src/lib/kits/maker/boards/microBit/MicroBitBoard.js
@@ -1,5 +1,4 @@
/** @file Board controller for BBC micro:bit */
/* global SerialPort */ // Maybe provided by the Code.org Browser
import {EventEmitter} from 'events'; // provided by webpack's node-libs-browser
import {
createMicroBitComponents,
Expand All @@ -11,6 +10,8 @@ import MBFirmataWrapper from './MBFirmataWrapper';
import ExternalLed from './ExternalLed';
import ExternalButton from './ExternalButton';
import CapacitiveTouchSensor from './CapacitiveTouchSensor';
import {serialPortType} from '../../util/browserChecks';
import {isNodeSerialAvailable} from '../../portScanning';

/**
* Controller interface for BBC micro:bit board using
Expand All @@ -19,14 +20,20 @@ import CapacitiveTouchSensor from './CapacitiveTouchSensor';
* @implements MakerBoard
*/
export default class MicroBitBoard extends EventEmitter {
constructor() {
constructor(port) {
super();

this.port = port;

/** @private {Object} Map of component controllers */
this.prewiredComponents_ = null;

this.nodeSerialAvailable = isNodeSerialAvailable();

let portType = serialPortType(true);

/** @private {MicrobitFirmataClient} serial port controller */
this.boardClient_ = new MBFirmataWrapper(SerialPort);
this.boardClient_ = new MBFirmataWrapper(portType);

/** @private {Array} List of dynamically-created component controllers. */
this.dynamicComponents_ = [];
Expand All @@ -44,6 +51,40 @@ export default class MicroBitBoard extends EventEmitter {
.then(() => this.initializeComponents());
}

/**
* Create a serial port controller and open the serial port immediately.
* @return {SerialPort}
*/
openSerialPort() {
const portName = this.port ? this.port.comName : undefined;
const SerialPortType = serialPortType(false);

/** @const {number} serial port transfer rate */
const SERIAL_BAUD = 57600;

let serialPort;
if (this.nodeSerialAvailable) {
serialPort = new SerialPortType(portName, {baudRate: SERIAL_BAUD});
return Promise.resolve(serialPort);
} else {
// Chrome-serialport uses callback to relay when serialport initialization is complete.
// Wrapping construction function to call promise resolution as callback.
let constructorFunction = callback => {
serialPort = new SerialPortType(
portName,
{
baudRate: SERIAL_BAUD
},
true,
callback
);
};
return new Promise(resolve => constructorFunction(resolve)).then(() =>
Promise.resolve(serialPort)
);
}
}

/**
* Connect to the micro:bit firmata client. After connecting, check the firmata
* version and firmware version response on the boardClient. If not connected
Expand All @@ -53,7 +94,9 @@ export default class MicroBitBoard extends EventEmitter {
* @returns {Promise<void>}
*/
checkExpectedFirmware() {
return Promise.resolve().then(() => this.boardClient_.connectBoard());
return Promise.resolve()
.then(() => this.openSerialPort())
.then(serialPort => this.boardClient_.connectBoard(serialPort));
}

/**
Expand Down
3 changes: 1 addition & 2 deletions apps/src/lib/kits/maker/toolkit.js
Expand Up @@ -163,8 +163,7 @@ function getBoard() {
return Promise.resolve(new FakeBoard());
} else {
if (project.getMakerAPIs() === MB_API) {
//TODO - break out the applicable parts of findPortWithViableDevice
return findPortWithViableDevice().then(() => new MicroBitBoard());
return findPortWithViableDevice().then(port => new MicroBitBoard(port));
} else {
return findPortWithViableDevice().then(
port => new CircuitPlaygroundBoard(port)
Expand Down
28 changes: 28 additions & 0 deletions apps/src/lib/kits/maker/util/browserChecks.js
@@ -1,4 +1,7 @@
/** @file Some misc. browser check methods for maker */
/* global SerialPort */ // Maybe provided by the Code.org Browser
import {isNodeSerialAvailable} from '../portScanning';
import ChromeSerialPort from 'chrome-serialport';

export function gtChrome33() {
return getChromeVersion() >= 33;
Expand Down Expand Up @@ -32,3 +35,28 @@ export function isChromeOS() {
export function isLinux() {
return /^Linux/.test(navigator.platform) && !isChromeOS();
}

/*
A gotcha here: These two types of SerialPort provide similar, but not
exactly equivalent, interfaces. When making changes to construction
here maker sure to test both paths:
Code.org Browser case: Native Node SerialPort 6 is available on window.
Code.org connector app case: ChromeSerialPort bridges through the Chrome
app, implements SerialPort 3's interface.
@param {boolean} getFactory - optional - ChromeSerialPort is a factory.
Parameter determines whether to return the factory of the SerialPort
*/
export function serialPortType(getFactory = null) {
if (isNodeSerialAvailable()) {
return SerialPort;
} else {
if (getFactory) {
return ChromeSerialPort;
} else {
return ChromeSerialPort.SerialPort;
}
}
}
26 changes: 17 additions & 9 deletions apps/test/unit/lib/kits/maker/boards/microBit/MicroBitBoardTest.js
Expand Up @@ -13,6 +13,20 @@ import ExternalLed from '@cdo/apps/lib/kits/maker/boards/microBit/ExternalLed';
import ExternalButton from '@cdo/apps/lib/kits/maker/boards/microBit/ExternalButton';
import CapacitiveTouchSensor from '@cdo/apps/lib/kits/maker/boards/microBit/CapacitiveTouchSensor';

function boardSetupAndStub(board) {
stubOpenSerialPort(board);
sinon.stub(board.boardClient_, 'connectBoard').callsFake(() => {
board.boardClient_.myPort = {write: () => {}};
sinon.stub(board.boardClient_.myPort, 'write');
});
}

function stubOpenSerialPort(board) {
sinon.stub(board, 'openSerialPort').callsFake(() => {
return Promise.resolve();
});
}

describe('MicroBitBoard', () => {
let board;

Expand All @@ -21,6 +35,7 @@ describe('MicroBitBoard', () => {
window.SerialPort = {};
board = new MicroBitBoard();
board.boardClient_ = new MicrobitStubBoard();
boardSetupAndStub(board);
});

afterEach(() => {
Expand All @@ -29,11 +44,7 @@ describe('MicroBitBoard', () => {

describe('Maker Board Interface', () => {
itImplementsTheMakerBoardInterface(MicroBitBoard, board => {
sinon.stub(board.boardClient_, 'connect').callsFake(() => {
board.boardClient_.myPort = {write: () => {}};
sinon.stub(board.boardClient_.myPort, 'write');
});

boardSetupAndStub(board);
sinon.stub(board.boardClient_, 'analogRead').callsArgWith(1, 0);
sinon.stub(board.boardClient_, 'digitalRead').callsArgWith(1, 0);
});
Expand All @@ -48,10 +59,7 @@ describe('MicroBitBoard', () => {

beforeEach(() => {
board = new MicroBitBoard();
sinon.stub(board.boardClient_, 'connect').callsFake(() => {
board.boardClient_.myPort = {write: () => {}};
sinon.stub(board.boardClient_.myPort, 'write');
});
boardSetupAndStub(board);

jsInterpreter = {
globalProperties: {},
Expand Down

0 comments on commit 72f732e

Please sign in to comment.