EV3/Microbit critical fixes for code freeze (#1354)

* Resolves - BLESession and BTSession should emit PERIPHERAL_SCAN_TIMEOUT #1348.

* Resolves - BLESession should handle 'could not find service' error #1350.

* Resolves - BTSession should handle 'no peripheral connected' error #1351.

* Fixing a typo that caused device scan timeout bugs.

* Resolves - Add casting and clamping throughout the EV3 extension #1352.

* Fixing a linting error.

* Further fixes for issue #1351.
This commit is contained in:
Evelyn Eastmond 2018-07-17 16:03:06 -04:00 committed by Eric Rosenbaum
parent 1dcdfc9548
commit c4ee7065a2
6 changed files with 117 additions and 46 deletions

View file

@ -428,6 +428,14 @@ class Runtime extends EventEmitter {
return 'PERIPHERAL_ERROR'; return 'PERIPHERAL_ERROR';
} }
/**
* Event name for reporting that a peripheral has not been discovered.
* @const {string}
*/
static get PERIPHERAL_SCAN_TIMEOUT () {
return 'PERIPHERAL_SCAN_TIMEOUT';
}
/** /**
* Event name for reporting that blocksInfo was updated. * Event name for reporting that blocksInfo was updated.
* @const {string} * @const {string}

View file

@ -1,9 +1,10 @@
const ArgumentType = require('../../extension-support/argument-type'); const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type'); const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast'); const Cast = require('../../util/cast');
const log = require('../../util/log'); // const log = require('../../util/log');
const Base64Util = require('../../util/base64-util'); const Base64Util = require('../../util/base64-util');
const BTSession = require('../../io/btSession'); const BTSession = require('../../io/btSession');
const MathUtil = require('../../util/math-util');
// TODO: Refactor/rename all these high level primitives to be clearer/match // TODO: Refactor/rename all these high level primitives to be clearer/match
@ -53,6 +54,8 @@ const MOTOR_PORTS = [
} }
]; ];
const VALID_MOTOR_PORTS = [0, 1, 2, 3];
/** /**
* Array of accepted sensor ports. * Array of accepted sensor ports.
* @note These should not be translated as they correspond to labels on * @note These should not be translated as they correspond to labels on
@ -78,6 +81,8 @@ const SENSOR_PORTS = [
} }
]; ];
const VALID_SENSOR_PORTS = [0, 1, 2, 3];
// firmware pdf page 100 // firmware pdf page 100
const EV_DEVICE_TYPES = { const EV_DEVICE_TYPES = {
29: 'color', 29: 'color',
@ -120,7 +125,6 @@ class EV3 {
/** /**
* State * State
*/ */
this.connected = false;
this._sensorPorts = []; this._sensorPorts = [];
this._motorPorts = []; this._motorPorts = [];
this._sensors = { this._sensors = {
@ -201,7 +205,7 @@ class EV3 {
} }
get distance () { get distance () {
if (!this.connected) return 0; if (!this.getPeripheralIsConnected()) return 0;
// https://shop.lego.com/en-US/EV3-Ultrasonic-Sensor-45504 // https://shop.lego.com/en-US/EV3-Ultrasonic-Sensor-45504
// Measures distances between one and 250 cm (one to 100 in.) // Measures distances between one and 250 cm (one to 100 in.)
@ -214,13 +218,13 @@ class EV3 {
} }
get brightness () { get brightness () {
if (!this.connected) return 0; if (!this.getPeripheralIsConnected()) return 0;
return this._sensors.brightness; return this._sensors.brightness;
} }
getMotorPosition (port) { getMotorPosition (port) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
let value = this._motors.positions[port]; let value = this._motors.positions[port];
value = value % 360; value = value % 360;
@ -230,15 +234,13 @@ class EV3 {
} }
isButtonPressed (port) { isButtonPressed (port) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
return this._sensors.buttons[port]; return this._sensors.buttons[port];
} }
beep (freq, time) { beep (freq, time) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
log.info('should be beeping');
const cmd = []; const cmd = [];
cmd[0] = 15; // length cmd[0] = 15; // length
@ -274,7 +276,7 @@ class EV3 {
} }
motorTurnClockwise (port, time) { motorTurnClockwise (port, time) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
// Build up motor command // Build up motor command
const cmd = this._applyPrefix(0, this._motorCommand( const cmd = this._applyPrefix(0, this._motorCommand(
@ -309,7 +311,7 @@ class EV3 {
} }
motorTurnCounterClockwise (port, time) { motorTurnCounterClockwise (port, time) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
// Build up motor command // Build up motor command
const cmd = this._applyPrefix(0, this._motorCommand( const cmd = this._applyPrefix(0, this._motorCommand(
@ -365,7 +367,7 @@ class EV3 {
} }
motorRotate (port, degrees) { motorRotate (port, degrees) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
// Build up motor command // Build up motor command
const cmd = this._applyPrefix(0, this._motorCommand( const cmd = this._applyPrefix(0, this._motorCommand(
@ -397,7 +399,7 @@ class EV3 {
} }
motorSetPosition (port, degrees) { motorSetPosition (port, degrees) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
// Calculate degrees to turn // Calculate degrees to turn
let previousPos = this._motors.positions[port]; let previousPos = this._motors.positions[port];
@ -443,7 +445,7 @@ class EV3 {
} }
motorSetPower (port, power) { motorSetPower (port, power) {
if (!this.connected) return; if (!this.getPeripheralIsConnected()) return;
this._motors.speeds[port] = power; this._motors.speeds[port] = power;
} }
@ -453,7 +455,6 @@ class EV3 {
// ******* // *******
_stopAllMotors () { _stopAllMotors () {
log.info('stop all motors');
for (let i = 0; i < this._motorPorts.length; i++) { for (let i = 0; i < this._motorPorts.length; i++) {
if (this._motorPorts[i] !== 'none') { if (this._motorPorts[i] !== 'none') {
this.motorCoast(i); this.motorCoast(i);
@ -555,8 +556,6 @@ class EV3 {
// TODO: keep here? / refactor // TODO: keep here? / refactor
_onSessionConnect () { _onSessionConnect () {
this.connected = true;
// start polling // start polling
// TODO: window? // TODO: window?
this._pollingIntervalID = window.setInterval(this._getSessionData.bind(this), 150); this._pollingIntervalID = window.setInterval(this._getSessionData.bind(this), 150);
@ -564,7 +563,7 @@ class EV3 {
// TODO: keep here? / refactor // TODO: keep here? / refactor
_getSessionData () { _getSessionData () {
if (!this.connected) { if (!this.getPeripheralIsConnected()) {
window.clearInterval(this._pollingIntervalID); window.clearInterval(this._pollingIntervalID);
return; return;
} }
@ -595,7 +594,6 @@ class EV3 {
cmd[0] = cmd.length - 2; cmd[0] = cmd.length - 2;
cmd[5] = 33; cmd[5] = 33;
// log.info(`REQUEST DEVICE LIST: ${compoundCommand}`);
// Clear sensor data // Clear sensor data
this._updateDevices = true; this._updateDevices = true;
this._sensorPorts = []; this._sensorPorts = [];
@ -682,8 +680,8 @@ class EV3 {
this._motorPorts[1] = EV_DEVICE_TYPES[array[22]] ? EV_DEVICE_TYPES[array[22]] : 'none'; this._motorPorts[1] = EV_DEVICE_TYPES[array[22]] ? EV_DEVICE_TYPES[array[22]] : 'none';
this._motorPorts[2] = EV_DEVICE_TYPES[array[23]] ? EV_DEVICE_TYPES[array[23]] : 'none'; this._motorPorts[2] = EV_DEVICE_TYPES[array[23]] ? EV_DEVICE_TYPES[array[23]] : 'none';
this._motorPorts[3] = EV_DEVICE_TYPES[array[24]] ? EV_DEVICE_TYPES[array[24]] : 'none'; this._motorPorts[3] = EV_DEVICE_TYPES[array[24]] ? EV_DEVICE_TYPES[array[24]] : 'none';
log.info(`sensor ports: ${this._sensorPorts}`); // log.info(`sensor ports: ${this._sensorPorts}`);
log.info(`motor ports: ${this._motorPorts}`); // log.info(`motor ports: ${this._motorPorts}`);
this._updateDevices = false; this._updateDevices = false;
// eslint-disable-next-line no-undefined // eslint-disable-next-line no-undefined
} else if (!this._sensorPorts.includes(undefined) && !this._motorPorts.includes(undefined)) { } else if (!this._sensorPorts.includes(undefined) && !this._motorPorts.includes(undefined)) {
@ -703,7 +701,7 @@ class EV3 {
// Read brightness / distance values and set to 0 if null // Read brightness / distance values and set to 0 if null
this._sensors[EV_DEVICE_LABELS[this._sensorPorts[i]]] = value ? value : 0; this._sensors[EV_DEVICE_LABELS[this._sensorPorts[i]]] = value ? value : 0;
} }
log.info(`${JSON.stringify(this._sensors)}`); // log.info(`${JSON.stringify(this._sensors)}`);
offset += 4; offset += 4;
} }
// READ MOTOR POSITION VALUES // READ MOTOR POSITION VALUES
@ -720,7 +718,7 @@ class EV3 {
if (value) { if (value) {
this._motors.positions[i] = value; this._motors.positions[i] = value;
} }
log.info(`motor positions: ${this._motors.positions}`); // log.info(`motor positions: ${this._motors.positions}`);
offset += 4; offset += 4;
} }
} }
@ -1002,22 +1000,37 @@ class Scratch3Ev3Blocks {
motorTurnClockwise (args) { motorTurnClockwise (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
const time = Cast.toNumber(args.TIME) * 1000; let time = Cast.toNumber(args.TIME) * 1000;
time = MathUtil.clamp(time, 0, 15000);
if (!VALID_MOTOR_PORTS.includes(port)) {
return;
}
return this._device.motorTurnClockwise(port, time); return this._device.motorTurnClockwise(port, time);
} }
motorTurnCounterClockwise (args) { motorTurnCounterClockwise (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
const time = Cast.toNumber(args.TIME) * 1000; let time = Cast.toNumber(args.TIME) * 1000;
time = MathUtil.clamp(time, 0, 15000);
if (!VALID_MOTOR_PORTS.includes(port)) {
return;
}
return this._device.motorTurnCounterClockwise(port, time); return this._device.motorTurnCounterClockwise(port, time);
} }
/*
motorRotate (args) { motorRotate (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
const degrees = Cast.toNumber(args.DEGREES); const degrees = Cast.toNumber(args.DEGREES);
if (!VALID_MOTOR_PORTS.includes(port)) {
return;
}
this._device.motorRotate(port, degrees); this._device.motorRotate(port, degrees);
return; return;
} }
@ -1026,40 +1039,55 @@ class Scratch3Ev3Blocks {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
const degrees = Cast.toNumber(args.DEGREES); const degrees = Cast.toNumber(args.DEGREES);
if (!VALID_MOTOR_PORTS.includes(port)) {
return;
}
this._device.motorSetPosition(port, degrees); this._device.motorSetPosition(port, degrees);
return; return;
} }
*/
motorSetPower (args) { motorSetPower (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
const power = Cast.toNumber(args.POWER); const power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
const value = Math.max(-100, Math.min(power, 100)); if (!VALID_MOTOR_PORTS.includes(port)) {
return;
}
this._device.motorSetPower(port, value); this._device.motorSetPower(port, power);
return; return;
} }
getMotorPosition (args) { getMotorPosition (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
if (!VALID_MOTOR_PORTS.includes(port)) {
return;
}
return this._device.getMotorPosition(port); return this._device.getMotorPosition(port);
} }
whenButtonPressed (args) { whenButtonPressed (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
if (!VALID_SENSOR_PORTS.includes(port)) {
return;
}
return this._device.isButtonPressed(port); return this._device.isButtonPressed(port);
} }
whenDistanceLessThan (args) { whenDistanceLessThan (args) {
const distance = Cast.toNumber(args.DISTANCE); const distance = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100);
return this._device.distance < distance; return this._device.distance < distance;
} }
whenBrightnessLessThan (args) { whenBrightnessLessThan (args) {
const brightness = Cast.toNumber(args.DISTANCE); const brightness = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100);
return this._device.brightness < brightness; return this._device.brightness < brightness;
} }
@ -1067,6 +1095,10 @@ class Scratch3Ev3Blocks {
buttonPressed (args) { buttonPressed (args) {
const port = Cast.toNumber(args.PORT); const port = Cast.toNumber(args.PORT);
if (!VALID_SENSOR_PORTS.includes(port)) {
return;
}
return this._device.isButtonPressed(port); return this._device.isButtonPressed(port);
} }
@ -1079,8 +1111,13 @@ class Scratch3Ev3Blocks {
} }
beep (args) { beep (args) {
const note = Cast.toNumber(args.NOTE); const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 47, 99); // valid EV3 sounds
const time = Cast.toNumber(args.TIME * 1000); let time = Cast.toNumber(args.TIME) * 1000;
time = MathUtil.clamp(time, 0, 3000);
if (time === 0) {
return; // don't send a beep time of 0
}
// https://en.wikipedia.org/wiki/MIDI_tuning_standard#Frequency_values // https://en.wikipedia.org/wiki/MIDI_tuning_standard#Frequency_values
const freq = Math.pow(2, ((note - 69 + 12) / 12)) * 440; const freq = Math.pow(2, ((note - 69 + 12) / 12)) * 440;

View file

@ -105,7 +105,6 @@ class MicroBit {
* Called by the runtime when user wants to scan for a device. * Called by the runtime when user wants to scan for a device.
*/ */
startDeviceScan () { startDeviceScan () {
log.info('making a new BLE session');
this._ble = new BLESession(this._runtime, { this._ble = new BLESession(this._runtime, {
filters: [ filters: [
{services: [BLEUUID.service]} {services: [BLEUUID.service]}

View file

@ -22,11 +22,11 @@ class BLESession extends JSONRPCWebSocket {
this._availablePeripherals = {}; this._availablePeripherals = {};
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
this._connected = false;
this._characteristicDidChangeCallback = null; this._characteristicDidChangeCallback = null;
this._deviceOptions = deviceOptions; this._deviceOptions = deviceOptions;
this._discoverTimeoutID = null;
this._runtime = runtime; this._runtime = runtime;
this._connected = false;
} }
/** /**
@ -35,7 +35,8 @@ class BLESession extends JSONRPCWebSocket {
*/ */
requestDevice () { requestDevice () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
// TODO: start a 'discover' timeout this._availablePeripherals = {};
this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._deviceOptions) this.sendRemoteRequest('discover', this._deviceOptions)
.catch(e => this._sendError(e)); // never reached? .catch(e => this._sendError(e)); // never reached?
} }
@ -88,7 +89,10 @@ class BLESession extends JSONRPCWebSocket {
this._runtime.constructor.PERIPHERAL_LIST_UPDATE, this._runtime.constructor.PERIPHERAL_LIST_UPDATE,
this._availablePeripherals this._availablePeripherals
); );
// TODO: cancel a discover timeout if one is active if (this._discoverTimeoutID) {
// TODO: window?
window.clearTimeout(this._discoverTimeoutID);
}
break; break;
case 'characteristicDidChange': case 'characteristicDidChange':
this._characteristicDidChangeCallback(params.message); this._characteristicDidChangeCallback(params.message);
@ -115,8 +119,10 @@ class BLESession extends JSONRPCWebSocket {
params.startNotifications = true; params.startNotifications = true;
} }
this._characteristicDidChangeCallback = onCharacteristicChanged; this._characteristicDidChangeCallback = onCharacteristicChanged;
return this.sendRemoteRequest('read', params); return this.sendRemoteRequest('read', params)
// TODO: handle error here .catch(e => {
this._sendError(e);
});
} }
/** /**
@ -132,7 +138,10 @@ class BLESession extends JSONRPCWebSocket {
if (encoding) { if (encoding) {
params.encoding = encoding; params.encoding = encoding;
} }
return this.sendRemoteRequest('write', params); return this.sendRemoteRequest('write', params)
.catch(e => {
this._sendError(e);
});
} }
_sendError (e) { _sendError (e) {
@ -140,6 +149,10 @@ class BLESession extends JSONRPCWebSocket {
log.error(`BLESession error: ${JSON.stringify(e)}`); log.error(`BLESession error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR);
} }
_sendDiscoverTimeout () {
this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT);
}
} }
module.exports = BLESession; module.exports = BLESession;

View file

@ -23,12 +23,12 @@ class BTSession extends JSONRPCWebSocket {
this._availablePeripherals = {}; this._availablePeripherals = {};
this._connectCallback = connectCallback; this._connectCallback = connectCallback;
this._connected = false;
this._characteristicDidChangeCallback = null; this._characteristicDidChangeCallback = null;
this._deviceOptions = deviceOptions; this._deviceOptions = deviceOptions;
this._discoverTimeoutID = null;
this._messageCallback = messageCallback; this._messageCallback = messageCallback;
this._runtime = runtime; this._runtime = runtime;
this._connected = false;
} }
/** /**
@ -37,7 +37,8 @@ class BTSession extends JSONRPCWebSocket {
*/ */
requestDevice () { requestDevice () {
if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen? if (this._ws.readyState === 1) { // is this needed since it's only called on ws.onopen?
// TODO: start a 'discover' timeout this._availablePeripherals = {};
this._discoverTimeoutID = window.setTimeout(this._sendDiscoverTimeout.bind(this), 15000);
this.sendRemoteRequest('discover', this._deviceOptions) this.sendRemoteRequest('discover', this._deviceOptions)
.catch(e => this._sendError(e)); // never reached? .catch(e => this._sendError(e)); // never reached?
} }
@ -76,9 +77,11 @@ class BTSession extends JSONRPCWebSocket {
return this._connected; return this._connected;
} }
sendMessage (options) { sendMessage (options) {
return this.sendRemoteRequest('send', options); return this.sendRemoteRequest('send', options)
.catch(e => {
this._sendError(e);
});
} }
/** /**
@ -96,7 +99,10 @@ class BTSession extends JSONRPCWebSocket {
this._runtime.constructor.PERIPHERAL_LIST_UPDATE, this._runtime.constructor.PERIPHERAL_LIST_UPDATE,
this._availablePeripherals this._availablePeripherals
); );
// TODO: cancel a discover timeout if one is active if (this._discoverTimeoutID) {
// TODO: window?
window.clearTimeout(this._discoverTimeoutID);
}
break; break;
case 'didReceiveMessage': case 'didReceiveMessage':
this._messageCallback(params); // TODO: refine? this._messageCallback(params); // TODO: refine?
@ -107,9 +113,14 @@ class BTSession extends JSONRPCWebSocket {
} }
_sendError (e) { _sendError (e) {
this._connected = false;
log.error(`BTSession error: ${JSON.stringify(e)}`); log.error(`BTSession error: ${JSON.stringify(e)}`);
this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR); this._runtime.emit(this._runtime.constructor.PERIPHERAL_ERROR);
} }
_sendDiscoverTimeout () {
this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT);
}
} }
module.exports = BTSession; module.exports = BTSession;

View file

@ -115,6 +115,9 @@ class VirtualMachine extends EventEmitter {
this.runtime.on(Runtime.PERIPHERAL_ERROR, () => this.runtime.on(Runtime.PERIPHERAL_ERROR, () =>
this.emit(Runtime.PERIPHERAL_ERROR) this.emit(Runtime.PERIPHERAL_ERROR)
); );
this.runtime.on(Runtime.PERIPHERAL_SCAN_TIMEOUT, () =>
this.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT)
);
this.extensionManager = new ExtensionManager(this.runtime); this.extensionManager = new ExtensionManager(this.runtime);