2018-06-25 13:42:48 -04:00
|
|
|
const ArgumentType = require('../../extension-support/argument-type');
|
|
|
|
const BlockType = require('../../extension-support/block-type');
|
|
|
|
const Cast = require('../../util/cast');
|
2018-07-19 15:46:01 -04:00
|
|
|
const uid = require('../../util/uid');
|
2018-07-17 16:03:06 -04:00
|
|
|
// const log = require('../../util/log');
|
2018-06-25 13:42:48 -04:00
|
|
|
const Base64Util = require('../../util/base64-util');
|
2018-07-02 11:06:23 -04:00
|
|
|
const BTSession = require('../../io/btSession');
|
2018-07-17 16:03:06 -04:00
|
|
|
const MathUtil = require('../../util/math-util');
|
2018-06-25 13:42:48 -04:00
|
|
|
|
2018-06-28 13:40:16 -04:00
|
|
|
// TODO: Refactor/rename all these high level primitives to be clearer/match
|
|
|
|
|
2018-07-02 18:07:34 -04:00
|
|
|
/**
|
|
|
|
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
|
|
|
|
* @type {string}
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line max-len
|
|
|
|
const blockIconURI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSI0MHB4IiB2aWV3Qm94PSIwIDAgNDAgNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUwLjIgKDU1MDQ3KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5ldjMtYmxvY2staWNvbjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJldjMtYmxvY2staWNvbiIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9ImV2MyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNS41MDAwMDAsIDMuNTAwMDAwKSIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPHJlY3QgaWQ9IlJlY3RhbmdsZS1wYXRoIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNGRkZGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgeD0iMC41IiB5PSIzLjU5IiB3aWR0aD0iMjgiIGhlaWdodD0iMjUuODEiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMC41IiB3aWR0aD0iMjQiIGhlaWdodD0iMzIiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRkZGRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHg9IjIuNSIgeT0iMTQuNSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjEzIj48L3JlY3Q+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNC41LDEwLjUgTDE0LjUsMTQuNSIgaWQ9IlNoYXBlIiBzdHJva2U9IiM3Qzg3QTUiIGZpbGw9IiNFNkU3RTgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PC9wYXRoPgogICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlLXBhdGgiIGZpbGw9IiM0MTQ3NTciIHg9IjQuNSIgeT0iMi41IiB3aWR0aD0iMjAiIGhlaWdodD0iMTAiIHJ4PSIxIj48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUtcGF0aCIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB4PSIxMy41IiB5PSIyMC4xMyIgd2lkdGg9IjIiIGhlaWdodD0iMiIgcng9IjAuNSI+PC9yZWN0PgogICAgICAgICAgICA8cGF0aCBkPSJNOS4wNiwyMC4xMyBMMTAuNTYsMjAuMTMgQzEwLjgzNjE0MjQsMjAuMTMgMTEuMDYsMjAuMzUzODU3NiAxMS4wNiwyMC42MyBMMTEuMDYsMjEuNjMgQzExLjA2LDIxLjkwNjE0MjQgMTAuODM2MTQyNCwyMi4xMyAxMC41NiwyMi4xMyBMOS4wNiwyMi4xMyBDOC41MDc3MTUyNSwyMi4xMyA4LjA2LDIxLjY4MjI4NDcgOC4wNiwyMS4xMyBDOC4wNiwyMC41Nzc3MTUzIDguNTA3NzE1MjUsMjAuMTMgOS4wNiwyMC4xMyBaIiBpZD0iU2hhcGUiIGZpbGw9IiM3Qzg3QTUiIG9wYWNpdHk9IjAuNSI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBkPSJNMTguOTEsMjAuMTMgTDIwLjQyLDIwLjEzIEMyMC42OTYxNDI0LDIwLjEzIDIwLjkyLDIwLjM1Mzg1NzYgMjAuOTIsMjAuNjMgTDIwLjkyLDIxLjYzIEMyMC45MiwyMS45MDYxNDI0IDIwLjY5NjE0MjQsMjIuMTMgMjAuNDIsMjIuMTMgTDE4LjkyLDIyLjEzIEMxOC4zNjc3MTUzLDIyLjEzIDE3LjkyLDIxLjY4MjI4NDcgMTcuOTIsMjEuMTMgQzE3LjkxOTk3MjYsMjAuNTgxNTk3IDE4LjM2MTYyNDUsMjAuMTM1NDg0IDE4LjkxLDIwLjEzIFoiIGlkPSJTaGFwZSIgZmlsbD0iIzdDODdBNSIgb3BhY2l0eT0iMC41IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOS40MjAwMDAsIDIxLjEzMDAwMCkgcm90YXRlKC0xODAuMDAwMDAwKSB0cmFuc2xhdGUoLTE5LjQyMDAwMCwgLTIxLjEzMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik04LjIzLDE3LjUgTDUsMTcuNSBDNC43MjM4NTc2MywxNy41IDQuNSwxNy4yNzYxNDI0IDQuNSwxNyBMNC41LDE0LjUgTDEwLjUsMTQuNSBMOC42NSwxNy4yOCBDOC41NTQ2Njk2MSwxNy40MTc5MDgyIDguMzk3NjUwMDYsMTcuNTAwMTU2NiA4LjIzLDE3LjUgWiIgaWQ9IlNoYXBlIiBmaWxsPSIjN0M4N0E1IiBvcGFjaXR5PSIwLjUiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggZD0iTTE4LjE1LDE4Ljg1IEwxNy42NSwxOS4zNSBDMTcuNTUyMzQxNiwxOS40NDQwNzU2IDE3LjQ5ODAzMzksMTkuNTc0NDE0MiAxNy41LDE5LjcxIEwxNy41LDIwIEMxNy41LDIwLjI3NjE0MjQgMTcuMjc2MTQyNCwyMC41IDE3LDIwLjUgTDE2LjUsMjAuNSBDMTYuMjIzODU3NiwyMC41IDE2LDIwLjI3NjE0MjQgMTYsMjAgQzE2LDE5LjcyMzg1NzYgMTUuNzc2MTQyNCwxOS41IDE1LjUsMTkuNSBMMTMuNSwxOS41IEMxMy4yMjM4NTc2LDE5LjUgMTMsMTkuNzIzODU3NiAxMywyMCBDMTMsMjAuMjc2MTQyNCAxMi43NzYxNDI0LDIwLjUgMTIuNSwyMC41IEwxMiwyMC41IEMxMS43MjM4NTc2LDIwLjUgMTEuNSwyMC4yNzYxNDI0IDExLjUsMjAgTDExLjUsMTkuNzEgQzExLjUwMTk2NjEsMTkuNTc0NDE0MiAxMS40NDc2NTg0LDE5LjQ0NDA3NTYgMTEuMzUsMTkuMzUgTDEwLjg1LDE4Ljg1IEMxMC42NTgyMTY3LDE4LjY1MjE4NjMgMTAuNjU4MjE2NywxOC4zMzc4MTM3IDEwLjg1LDE4LjE0IEwxMi4zNiwxNi42NSBDMTIuNDUwMjgwMywxNi41NTI4NjE3IDEyLjU3NzM5NjEsMTYuNDk4MzgzNSAxMi43MSwxNi41IEwxNi4yOSwxNi41IEMxNi40MjI2MDM5LDE2LjQ5ODM4MzUgMTYuNTQ5NzE5NywxNi41NTI4NjE3IDE2LjY0LDE2LjY1IEwxOC4xNSwxOC4xNCBDMTguMzQ
|
|
|
|
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* High-level primitives / constants used by the extension.
|
|
|
|
* @type {object}
|
|
|
|
*/
|
|
|
|
const BTCommand = {
|
|
|
|
LAYER: 0x00,
|
|
|
|
NUM8: 0x81,
|
|
|
|
NUM16: 0x82,
|
|
|
|
NUM32: 0x83,
|
|
|
|
COAST: 0x0,
|
|
|
|
BRAKE: 0x1,
|
|
|
|
LONGRAMP: 50,
|
|
|
|
STEPSPEED: 0xAE,
|
|
|
|
TIMESPEED: 0xAF,
|
|
|
|
OUTPUTSTOP: 0xA3,
|
|
|
|
OUTPUTRESET: 0xA2,
|
|
|
|
STEPSPEEDSYNC: 0xB0,
|
|
|
|
TIMESPEEDSYNC: 0xB1
|
|
|
|
};
|
|
|
|
|
|
|
|
const MOTOR_PORTS = [
|
|
|
|
{
|
|
|
|
name: 'A',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 0
|
2018-06-25 13:42:48 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'B',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 1
|
2018-06-25 13:42:48 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'C',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 2
|
2018-06-25 13:42:48 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'D',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 3
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
];
|
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
const VALID_MOTOR_PORTS = [0, 1, 2, 3];
|
|
|
|
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* Array of accepted sensor ports.
|
|
|
|
* @note These should not be translated as they correspond to labels on
|
|
|
|
* the EV3 hub.
|
|
|
|
* @type {array}
|
|
|
|
*/
|
2018-06-25 14:11:26 -04:00
|
|
|
const SENSOR_PORTS = [
|
|
|
|
{
|
|
|
|
name: '1',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 0
|
2018-06-25 14:11:26 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: '2',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 1
|
2018-06-25 14:11:26 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: '3',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 2
|
2018-06-25 14:11:26 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: '4',
|
2018-07-13 13:14:53 -04:00
|
|
|
value: 3
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
];
|
2018-06-25 13:42:48 -04:00
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
const VALID_SENSOR_PORTS = [0, 1, 2, 3];
|
|
|
|
|
2018-06-28 13:40:16 -04:00
|
|
|
// firmware pdf page 100
|
|
|
|
const EV_DEVICE_TYPES = {
|
|
|
|
29: 'color',
|
|
|
|
30: 'ultrasonic',
|
|
|
|
32: 'gyro',
|
|
|
|
16: 'touch',
|
|
|
|
8: 'mediumMotor',
|
|
|
|
7: 'largeMotor',
|
2018-07-13 13:14:53 -04:00
|
|
|
126: 'none',
|
|
|
|
125: 'none'
|
2018-06-28 13:40:16 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
// firmware pdf page 100?
|
|
|
|
const EV_DEVICE_MODES = {
|
|
|
|
touch: 0,
|
|
|
|
color: 1,
|
2018-07-13 13:14:53 -04:00
|
|
|
ultrasonic: 1,
|
|
|
|
none: 0
|
2018-06-28 13:40:16 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const EV_DEVICE_LABELS = {
|
|
|
|
touch: 'button',
|
|
|
|
color: 'brightness',
|
|
|
|
ultrasonic: 'distance'
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2018-06-25 13:42:48 -04:00
|
|
|
class EV3 {
|
|
|
|
|
|
|
|
constructor (runtime, extensionId) {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Scratch 3.0 runtime used to trigger the green flag button.
|
|
|
|
* @type {Runtime}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
this._runtime = runtime;
|
2018-07-13 13:14:53 -04:00
|
|
|
this._runtime.on('PROJECT_STOP_ALL', this._stopAllMotors.bind(this));
|
2018-06-25 13:42:48 -04:00
|
|
|
|
2018-06-26 15:07:08 -04:00
|
|
|
/**
|
2018-07-13 13:14:53 -04:00
|
|
|
* State
|
2018-06-26 15:07:08 -04:00
|
|
|
*/
|
2018-07-13 13:14:53 -04:00
|
|
|
this._sensorPorts = [];
|
|
|
|
this._motorPorts = [];
|
2018-06-26 15:07:08 -04:00
|
|
|
this._sensors = {
|
2018-06-28 13:40:16 -04:00
|
|
|
distance: 0,
|
2018-07-03 13:58:40 -04:00
|
|
|
brightness: 0,
|
|
|
|
buttons: [0, 0, 0, 0]
|
2018-07-02 10:36:13 -04:00
|
|
|
};
|
2018-07-13 13:14:53 -04:00
|
|
|
this._motors = {
|
|
|
|
speeds: [50, 50, 50, 50],
|
|
|
|
positions: [0, 0, 0, 0],
|
2018-07-19 15:46:01 -04:00
|
|
|
busy: [0, 0, 0, 0],
|
|
|
|
commandId: [null, null, null, null]
|
2018-07-13 13:14:53 -04:00
|
|
|
};
|
2018-06-28 13:40:16 -04:00
|
|
|
this._pollingIntervalID = null;
|
2018-07-13 13:14:53 -04:00
|
|
|
this._pollingCounter = 0;
|
2018-06-25 13:42:48 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The Bluetooth connection session for reading/writing device data.
|
|
|
|
* @type {BTSession}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
this._bt = null;
|
|
|
|
this._runtime.registerExtensionDevice(extensionId, this);
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* Called by the runtime when user wants to scan for a device.
|
|
|
|
*/
|
|
|
|
startDeviceScan () {
|
|
|
|
this._bt = new BTSession(this._runtime, {
|
|
|
|
majorDeviceClass: 8,
|
|
|
|
minorDeviceClass: 1
|
2018-06-26 15:07:08 -04:00
|
|
|
}, this._onSessionConnect.bind(this), this._onSessionMessage.bind(this));
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* Called by the runtime when user wants to connect to a certain device.
|
|
|
|
* @param {number} id - the id of the device to connect to.
|
|
|
|
*/
|
|
|
|
connectDevice (id) {
|
|
|
|
this._bt.connectDevice(id);
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-28 13:40:16 -04:00
|
|
|
/**
|
|
|
|
* Called by the runtime when user wants to disconnect from the device.
|
|
|
|
*/
|
2018-06-27 15:26:57 -04:00
|
|
|
disconnectSession () {
|
|
|
|
this._bt.disconnectSession();
|
2018-06-28 13:40:16 -04:00
|
|
|
window.clearInterval(this._pollingIntervalID); // TODO: window?
|
2018-07-13 13:14:53 -04:00
|
|
|
this._sensorPorts = [];
|
|
|
|
this._motorPorts = [];
|
2018-07-03 14:38:17 -04:00
|
|
|
this._sensors = {
|
|
|
|
distance: 0,
|
|
|
|
brightness: 0,
|
|
|
|
buttons: [0, 0, 0, 0]
|
|
|
|
};
|
2018-07-13 13:14:53 -04:00
|
|
|
this._motors = {
|
|
|
|
speeds: [50, 50, 50, 50],
|
|
|
|
positions: [0, 0, 0, 0],
|
2018-07-19 15:46:01 -04:00
|
|
|
busy: [0, 0, 0, 0],
|
|
|
|
commandId: [null, null, null, null]
|
2018-07-13 13:14:53 -04:00
|
|
|
};
|
2018-07-03 14:38:17 -04:00
|
|
|
this._pollingIntervalID = null;
|
2018-06-27 15:26:57 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-28 13:40:16 -04:00
|
|
|
/**
|
|
|
|
* Called by the runtime to detect whether the device is connected.
|
|
|
|
* @return {boolean} - the connected state.
|
|
|
|
*/
|
2018-06-27 15:26:57 -04:00
|
|
|
getPeripheralIsConnected () {
|
|
|
|
let connected = false;
|
|
|
|
if (this._bt) {
|
|
|
|
connected = this._bt.getPeripheralIsConnected();
|
|
|
|
}
|
|
|
|
return connected;
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
get distance () {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return 0;
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-06-28 13:40:16 -04:00
|
|
|
// https://shop.lego.com/en-US/EV3-Ultrasonic-Sensor-45504
|
|
|
|
// Measures distances between one and 250 cm (one to 100 in.)
|
|
|
|
// Accurate to +/- 1 cm (+/- .394 in.)
|
|
|
|
let value = this._sensors.distance > 100 ? 100 : this._sensors.distance;
|
|
|
|
value = value < 0 ? 0 : value;
|
2018-07-03 13:58:40 -04:00
|
|
|
value = Math.round(100 * value) / 100;
|
2018-06-28 13:40:16 -04:00
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
return value;
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get brightness () {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return 0;
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-06-28 13:40:16 -04:00
|
|
|
return this._sensors.brightness;
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
2018-07-02 10:36:13 -04:00
|
|
|
getMotorPosition (port) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-07-02 11:03:19 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
let value = this._motors.positions[port];
|
2018-07-03 13:58:40 -04:00
|
|
|
value = value % 360;
|
|
|
|
value = value < 0 ? value * -1 : value;
|
|
|
|
|
|
|
|
return value;
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
isButtonPressed (port) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
return this._sensors.buttons[port];
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
beep (freq, time) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-07-13 13:14:53 -04:00
|
|
|
|
|
|
|
const cmd = [];
|
|
|
|
cmd[0] = 15; // length
|
|
|
|
cmd[1] = 0; // 0x00
|
|
|
|
cmd[2] = 0; // 0x00
|
|
|
|
cmd[3] = 0; // 0x00
|
|
|
|
cmd[4] = 128; // 0x80
|
|
|
|
cmd[5] = 0; // 0x00
|
|
|
|
cmd[6] = 0; // 0x00
|
|
|
|
cmd[7] = 148; // 0x94 op sound
|
|
|
|
cmd[8] = 1; // 0x01 tone
|
|
|
|
cmd[9] = 129; // 0x81 volume following in 1 byte
|
|
|
|
cmd[10] = 2; // volume byte 1
|
|
|
|
cmd[11] = 130; // 0x82 frequency following in 2 bytes
|
|
|
|
cmd[12] = freq; // frequency byte 1
|
|
|
|
cmd[13] = freq >> 8; // frequency byte 2
|
|
|
|
cmd[14] = 130; // 0x82 time following in 2 bytes
|
|
|
|
cmd[15] = time; // time byte 1
|
|
|
|
cmd[16] = time >> 8; // time byte 2
|
|
|
|
|
2018-06-25 13:42:48 -04:00
|
|
|
this._bt.sendMessage({
|
2018-07-13 13:14:53 -04:00
|
|
|
message: Base64Util.arrayBufferToBase64(cmd),
|
2018-06-25 13:42:48 -04:00
|
|
|
encoding: 'base64'
|
|
|
|
});
|
2018-07-13 13:14:53 -04:00
|
|
|
|
|
|
|
// Yield for sound duration
|
|
|
|
// TODO: does this work?
|
|
|
|
return new Promise(resolve => {
|
|
|
|
setTimeout(() => {
|
|
|
|
resolve();
|
|
|
|
}, time);
|
|
|
|
});
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
motorTurnClockwise (port, time) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-06-25 13:42:48 -04:00
|
|
|
|
|
|
|
// Build up motor command
|
|
|
|
const cmd = this._applyPrefix(0, this._motorCommand(
|
|
|
|
BTCommand.TIMESPEED,
|
2018-07-13 13:14:53 -04:00
|
|
|
this._portMask(port),
|
2018-06-25 13:42:48 -04:00
|
|
|
time,
|
2018-07-13 13:14:53 -04:00
|
|
|
this._motors.speeds[port],
|
2018-06-25 13:42:48 -04:00
|
|
|
BTCommand.LONGRAMP
|
|
|
|
));
|
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
// Send turn message
|
2018-06-25 13:42:48 -04:00
|
|
|
this._bt.sendMessage({
|
|
|
|
message: Base64Util.arrayBufferToBase64(cmd),
|
|
|
|
encoding: 'base64'
|
|
|
|
});
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// Set motor to busy
|
|
|
|
// this._motors.busy[port] = 1;
|
|
|
|
|
2018-07-19 15:46:01 -04:00
|
|
|
this.coastAfter(port, time);
|
2018-07-03 13:58:40 -04:00
|
|
|
|
|
|
|
// Yield for turn time + brake time
|
2018-07-13 13:14:53 -04:00
|
|
|
const coastTime = 100; // TODO: calculate coasting or set flag
|
2018-06-25 13:42:48 -04:00
|
|
|
return new Promise(resolve => {
|
|
|
|
setTimeout(() => {
|
|
|
|
resolve();
|
2018-07-03 13:58:40 -04:00
|
|
|
}, time + coastTime);
|
2018-06-25 13:42:48 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
motorTurnCounterClockwise (port, time) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-06-25 13:42:48 -04:00
|
|
|
|
|
|
|
// Build up motor command
|
|
|
|
const cmd = this._applyPrefix(0, this._motorCommand(
|
|
|
|
BTCommand.TIMESPEED,
|
2018-07-13 13:14:53 -04:00
|
|
|
this._portMask(port),
|
2018-06-25 13:42:48 -04:00
|
|
|
time,
|
2018-07-13 13:14:53 -04:00
|
|
|
this._motors.speeds[port] * -1,
|
2018-06-25 13:42:48 -04:00
|
|
|
BTCommand.LONGRAMP
|
|
|
|
));
|
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
// Send turn message
|
2018-06-25 13:42:48 -04:00
|
|
|
this._bt.sendMessage({
|
|
|
|
message: Base64Util.arrayBufferToBase64(cmd),
|
|
|
|
encoding: 'base64'
|
|
|
|
});
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// Set motor to busy
|
|
|
|
// this._motors.busy[port] = 1;
|
|
|
|
|
2018-07-19 15:46:01 -04:00
|
|
|
this.coastAfter(port, time);
|
2018-07-03 13:58:40 -04:00
|
|
|
|
2018-06-25 13:42:48 -04:00
|
|
|
// Yield for time
|
2018-07-13 13:14:53 -04:00
|
|
|
const coastTime = 100; // TODO: calculate coasting or set flag
|
2018-06-25 13:42:48 -04:00
|
|
|
return new Promise(resolve => {
|
|
|
|
setTimeout(() => {
|
|
|
|
resolve();
|
2018-07-03 13:58:40 -04:00
|
|
|
}, time + coastTime);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-19 16:41:07 -04:00
|
|
|
coastAfter (port, time) {
|
2018-07-19 15:46:01 -04:00
|
|
|
// Set the motor command id to check before starting coast
|
|
|
|
const commandId = uid();
|
|
|
|
this._motors.commandId[port] = commandId;
|
|
|
|
|
|
|
|
// Send coast message
|
|
|
|
setTimeout(() => {
|
|
|
|
// Do not send coast if another motor command changed the command id.
|
|
|
|
if (this._motors.commandId[port] === commandId) {
|
|
|
|
this.motorCoast(port);
|
|
|
|
this._motors.commandId[port] = null;
|
|
|
|
}
|
|
|
|
}, time);
|
|
|
|
}
|
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
motorCoast (port) {
|
2018-07-13 13:14:53 -04:00
|
|
|
|
2018-07-03 13:58:40 -04:00
|
|
|
const cmd = [];
|
2018-07-13 13:14:53 -04:00
|
|
|
cmd[0] = 9; // length
|
|
|
|
cmd[1] = 0; // length
|
|
|
|
cmd[2] = 1; // 0x01
|
|
|
|
cmd[3] = 0; // 0x00
|
|
|
|
cmd[4] = 0; // 0x00
|
|
|
|
cmd[5] = 0; // 0x00
|
|
|
|
cmd[6] = 0; // 0x00
|
|
|
|
cmd[7] = 163; // 0xA3 Motor brake/coast command
|
|
|
|
cmd[8] = 0; // layer
|
|
|
|
cmd[9] = this._portMask(port); // port output bit field
|
|
|
|
cmd[10] = 0; // float = coast = 0
|
2018-07-03 13:58:40 -04:00
|
|
|
|
|
|
|
this._bt.sendMessage({
|
|
|
|
message: Base64Util.uint8ArrayToBase64(cmd),
|
|
|
|
encoding: 'base64'
|
2018-06-25 13:42:48 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
motorRotate (port, degrees) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// Build up motor command
|
|
|
|
const cmd = this._applyPrefix(0, this._motorCommand(
|
|
|
|
BTCommand.STEPSPEED,
|
|
|
|
this._portMask(port),
|
|
|
|
degrees,
|
|
|
|
this._motors.speeds[port],
|
|
|
|
BTCommand.LONGRAMP
|
|
|
|
));
|
|
|
|
|
|
|
|
// Send rotate message
|
|
|
|
this._bt.sendMessage({
|
|
|
|
message: Base64Util.arrayBufferToBase64(cmd),
|
|
|
|
encoding: 'base64'
|
|
|
|
});
|
|
|
|
|
|
|
|
// Set motor to busy
|
|
|
|
// this._motors.busy[port] = 1;
|
|
|
|
|
|
|
|
/*
|
|
|
|
// Yield for time
|
|
|
|
// TODO: calculate time?
|
|
|
|
return new Promise(resolve => {
|
|
|
|
setTimeout(() => {
|
|
|
|
resolve();
|
|
|
|
}, time);
|
|
|
|
});
|
|
|
|
*/
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
motorSetPosition (port, degrees) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// Calculate degrees to turn
|
|
|
|
let previousPos = this._motors.positions[port];
|
|
|
|
previousPos = previousPos % 360;
|
|
|
|
previousPos = previousPos < 0 ? previousPos * -1 : previousPos;
|
|
|
|
const newPos = degrees % 360;
|
|
|
|
let degreesToTurn = 0;
|
|
|
|
let direction = 1;
|
|
|
|
if (previousPos <= newPos) {
|
|
|
|
degreesToTurn = newPos - previousPos;
|
|
|
|
} else {
|
|
|
|
degreesToTurn = previousPos - newPos;
|
|
|
|
direction = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build up motor command
|
|
|
|
const cmd = this._applyPrefix(0, this._motorCommand(
|
|
|
|
BTCommand.STEPSPEED,
|
|
|
|
this._portMask(port),
|
|
|
|
degreesToTurn,
|
|
|
|
this._motors.speeds[port] * direction,
|
|
|
|
BTCommand.LONGRAMP
|
|
|
|
));
|
|
|
|
|
|
|
|
// Send rotate message
|
|
|
|
this._bt.sendMessage({
|
|
|
|
message: Base64Util.arrayBufferToBase64(cmd),
|
|
|
|
encoding: 'base64'
|
|
|
|
});
|
|
|
|
|
|
|
|
// Set motor to busy
|
|
|
|
// this._motors.busy[port] = 1;
|
|
|
|
|
|
|
|
/*
|
|
|
|
// Yield for time
|
|
|
|
// TODO: calculate time?
|
|
|
|
return new Promise(resolve => {
|
|
|
|
setTimeout(() => {
|
|
|
|
resolve();
|
|
|
|
}, time);
|
|
|
|
});
|
|
|
|
*/
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
motorSetPower (port, power) {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) return;
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
this._motors.speeds[port] = power;
|
2018-06-25 14:39:38 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// *******
|
|
|
|
// PRIVATE
|
|
|
|
// *******
|
|
|
|
|
|
|
|
_stopAllMotors () {
|
|
|
|
for (let i = 0; i < this._motorPorts.length; i++) {
|
|
|
|
if (this._motorPorts[i] !== 'none') {
|
|
|
|
this.motorCoast(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: keep here? / refactor
|
2018-06-25 13:42:48 -04:00
|
|
|
_applyPrefix (n, cmd) {
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: document
|
2018-06-25 13:42:48 -04:00
|
|
|
const len = cmd.length + 5;
|
|
|
|
return [].concat(
|
|
|
|
len & 0xFF,
|
|
|
|
(len >> 8) & 0xFF,
|
|
|
|
0x1,
|
|
|
|
0x0,
|
|
|
|
0x0,
|
|
|
|
n,
|
|
|
|
0x0,
|
|
|
|
cmd
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* Generate a motor command in EV3 byte array format (CMD, LAYER, PORT,
|
|
|
|
* SPEED, RAMP UP, RUN, RAMP DOWN, BREAKING TYPE)
|
|
|
|
* @param {string} command Motor command primitive (i.e. "prefix")
|
|
|
|
* @param {string} port Port to address
|
|
|
|
* @param {number} n Value to be passed to motor command
|
|
|
|
* @param {number} speed Speed value
|
|
|
|
* @param {number} ramp Ramp value
|
|
|
|
* @return {array} Byte array
|
|
|
|
*/
|
|
|
|
_motorCommand (command, port, n, speed, ramp) {
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: document
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* Generate run values for a given input.
|
|
|
|
* @param {number} run Run input
|
|
|
|
* @return {array} Run values (byte array)
|
|
|
|
*/
|
|
|
|
const getRunValues = function (run) {
|
|
|
|
// If run duration is less than max 16-bit integer
|
|
|
|
if (run < 0x7fff) {
|
|
|
|
return [
|
|
|
|
BTCommand.NUM16,
|
|
|
|
run & 0xff,
|
|
|
|
(run >> 8) & 0xff
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run forever
|
|
|
|
return [
|
|
|
|
BTCommand.NUM32,
|
|
|
|
run & 0xff,
|
|
|
|
(run >> 8) & 0xff,
|
|
|
|
(run >> 16) & 0xff,
|
|
|
|
(run >> 24) & 0xff
|
|
|
|
];
|
|
|
|
};
|
|
|
|
|
|
|
|
// If speed is less than zero, make it positive and multiply the input
|
|
|
|
// value by -1
|
|
|
|
if (speed < 0) {
|
|
|
|
speed = -1 * speed;
|
|
|
|
n = -1 * n;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the input value is less than 0
|
|
|
|
const dir = (n < 0) ? 0x100 - speed : speed; // step negative or possitive
|
|
|
|
n = Math.abs(n);
|
|
|
|
|
|
|
|
// Setup motor run duration and ramping behavior
|
|
|
|
let rampup = ramp;
|
|
|
|
let rampdown = ramp;
|
2018-06-28 13:40:16 -04:00
|
|
|
let run = n - (ramp * 2);
|
2018-06-25 13:42:48 -04:00
|
|
|
if (run < 0) {
|
|
|
|
rampup = Math.floor(n / 2);
|
|
|
|
run = 0;
|
|
|
|
rampdown = n - rampup;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate motor command
|
|
|
|
const runcmd = getRunValues(run);
|
|
|
|
return [
|
|
|
|
command,
|
|
|
|
BTCommand.LAYER,
|
|
|
|
port,
|
|
|
|
BTCommand.NUM8,
|
|
|
|
dir & 0xff,
|
|
|
|
BTCommand.NUM8,
|
|
|
|
rampup
|
|
|
|
].concat(runcmd.concat([
|
|
|
|
BTCommand.NUM8,
|
|
|
|
rampdown,
|
|
|
|
BTCommand.BRAKE
|
|
|
|
]));
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-25 13:42:48 -04:00
|
|
|
_onSessionConnect () {
|
2018-07-13 13:14:53 -04:00
|
|
|
// start polling
|
|
|
|
// TODO: window?
|
|
|
|
this._pollingIntervalID = window.setInterval(this._getSessionData.bind(this), 150);
|
2018-06-26 15:07:08 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-26 15:07:08 -04:00
|
|
|
_getSessionData () {
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!this.getPeripheralIsConnected()) {
|
2018-06-28 13:40:16 -04:00
|
|
|
window.clearInterval(this._pollingIntervalID);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
const cmd = []; // a compound command
|
|
|
|
|
|
|
|
// HEADER
|
|
|
|
cmd[0] = null; // calculate length later
|
|
|
|
cmd[1] = 0; // ...
|
|
|
|
cmd[2] = 1; // message counter // TODO: ?????
|
|
|
|
cmd[3] = 0; // message counter // TODO: ?????
|
|
|
|
cmd[4] = 0; // command type: direct command
|
|
|
|
cmd[5] = null; // calculate vars length later
|
|
|
|
cmd[6] = 0; // ...
|
|
|
|
|
|
|
|
let sensorCount = 0;
|
|
|
|
// Either request device list or request sensor data ??
|
|
|
|
if (this._pollingCounter % 20 === 0) {
|
|
|
|
// GET DEVICE LIST
|
|
|
|
cmd[7] = 152; // 0x98 op: get device list
|
|
|
|
cmd[8] = 129; // 0x81 LENGTH // TODO: ?????
|
|
|
|
cmd[9] = 33; // 0x21 ARRAY // TODO: ?????
|
|
|
|
cmd[10] = 96; // 0x60 CHANGED // TODO: ?????
|
|
|
|
cmd[11] = 225; // 0xE1 size of global var - 1 byte to follow
|
|
|
|
cmd[12] = 32; // 0x20 global var index "0" 0b00100000
|
|
|
|
|
|
|
|
// Command and payload lengths
|
|
|
|
cmd[0] = cmd.length - 2;
|
|
|
|
cmd[5] = 33;
|
|
|
|
|
|
|
|
// Clear sensor data
|
|
|
|
this._updateDevices = true;
|
|
|
|
this._sensorPorts = [];
|
|
|
|
this._motorPorts = [];
|
|
|
|
// TODO: figure out when/how to clear out sensor data
|
2018-06-26 15:07:08 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
} else {
|
2018-06-28 13:40:16 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
let index = 7;
|
|
|
|
|
|
|
|
// GET SENSOR VALUES
|
|
|
|
// eslint-disable-next-line no-undefined
|
|
|
|
if (!this._sensorPorts.includes(undefined)) {
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
if (this._sensorPorts[i] !== 'none') {
|
|
|
|
cmd[index + 0] = 157; // 0x9D op: get sensor value
|
|
|
|
cmd[index + 1] = 0; // layer
|
|
|
|
cmd[index + 2] = i; // port
|
|
|
|
cmd[index + 3] = 0; // do not change type
|
|
|
|
cmd[index + 4] = EV_DEVICE_MODES[this._sensorPorts[i]]; // mode
|
|
|
|
cmd[index + 5] = 225; // 0xE1 one byte to follow
|
|
|
|
cmd[index + 6] = sensorCount * 4; // global index
|
|
|
|
index += 7;
|
|
|
|
}
|
|
|
|
sensorCount++;
|
|
|
|
}
|
|
|
|
}
|
2018-06-28 13:40:16 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// GET MOTOR POSITION VALUES
|
|
|
|
// eslint-disable-next-line no-undefined
|
|
|
|
if (!this._motorPorts.includes(undefined)) {
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
cmd[index + 0] = 179; // 0XB3 op: get motor position value
|
|
|
|
cmd[index + 1] = 0; // layer
|
|
|
|
cmd[index + 2] = i; // port
|
|
|
|
cmd[index + 3] = 225; // 0xE1 byte following
|
|
|
|
cmd[index + 4] = sensorCount * 4; // global index
|
|
|
|
index += 5;
|
|
|
|
sensorCount++;
|
|
|
|
}
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
|
|
|
|
// Command and payload lengths
|
|
|
|
cmd[0] = cmd.length - 2;
|
|
|
|
cmd[5] = sensorCount * 4;
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
|
|
|
|
// GET MOTOR BUSY STATES
|
|
|
|
/*
|
2018-06-28 13:40:16 -04:00
|
|
|
for (let i = 0; i < this._motorPorts.length; i++) {
|
|
|
|
if (this._motorPorts[i] !== 'none') {
|
|
|
|
sensorCount++;
|
2018-07-13 13:14:53 -04:00
|
|
|
compoundCommand[compoundCommandIndex + 0] = 169; // 0xA9 op: test if output port is busy
|
|
|
|
compoundCommand[compoundCommandIndex + 1] = 0; // layer
|
|
|
|
compoundCommand[compoundCommandIndex + 2] = this._portMask(i); // output bit field
|
|
|
|
compoundCommand[compoundCommandIndex + 3] = 225; // 0xE1 1 byte following
|
|
|
|
compoundCommand[compoundCommandIndex + 4] = sensorCount * 4; // global index
|
2018-06-28 13:40:16 -04:00
|
|
|
compoundCommandIndex += 5;
|
|
|
|
}
|
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
*/
|
2018-06-28 13:40:16 -04:00
|
|
|
|
|
|
|
this._bt.sendMessage({
|
2018-07-13 13:14:53 -04:00
|
|
|
message: Base64Util.uint8ArrayToBase64(cmd),
|
2018-06-28 13:40:16 -04:00
|
|
|
encoding: 'base64'
|
|
|
|
});
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
this._pollingCounter++;
|
2018-06-26 15:07:08 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: rename and document better
|
2018-06-26 15:07:08 -04:00
|
|
|
_onSessionMessage (params) {
|
|
|
|
const message = params.message;
|
|
|
|
const array = Base64Util.base64ToUint8Array(message);
|
2018-07-13 13:14:53 -04:00
|
|
|
// log.info(`received array: ${array}`);
|
|
|
|
|
2018-07-24 11:08:48 -04:00
|
|
|
if (array.length < 35) { // TODO: find safer solution
|
|
|
|
return; // don't parse results that aren't sensor data list or device list
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
if (this._updateDevices) {
|
|
|
|
// READ DEVICE LIST
|
|
|
|
this._sensorPorts[0] = EV_DEVICE_TYPES[array[5]] ? EV_DEVICE_TYPES[array[5]] : 'none';
|
|
|
|
this._sensorPorts[1] = EV_DEVICE_TYPES[array[6]] ? EV_DEVICE_TYPES[array[6]] : 'none';
|
|
|
|
this._sensorPorts[2] = EV_DEVICE_TYPES[array[7]] ? EV_DEVICE_TYPES[array[7]] : 'none';
|
|
|
|
this._sensorPorts[3] = EV_DEVICE_TYPES[array[8]] ? EV_DEVICE_TYPES[array[8]] : 'none';
|
|
|
|
this._motorPorts[0] = EV_DEVICE_TYPES[array[21]] ? EV_DEVICE_TYPES[array[21]] : '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[3] = EV_DEVICE_TYPES[array[24]] ? EV_DEVICE_TYPES[array[24]] : 'none';
|
2018-07-17 16:03:06 -04:00
|
|
|
// log.info(`sensor ports: ${this._sensorPorts}`);
|
|
|
|
// log.info(`motor ports: ${this._motorPorts}`);
|
2018-07-13 13:14:53 -04:00
|
|
|
this._updateDevices = false;
|
|
|
|
// eslint-disable-next-line no-undefined
|
|
|
|
} else if (!this._sensorPorts.includes(undefined) && !this._motorPorts.includes(undefined)) {
|
|
|
|
// READ SENSOR VALUES
|
|
|
|
let offset = 5; // start reading sensor values at byte 5
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
const value = this._array2float([
|
|
|
|
array[offset],
|
|
|
|
array[offset + 1],
|
|
|
|
array[offset + 2],
|
|
|
|
array[offset + 3]
|
|
|
|
]);
|
|
|
|
if (EV_DEVICE_LABELS[this._sensorPorts[i]] === 'button') {
|
|
|
|
// Read a button value per port
|
|
|
|
this._sensors.buttons[i] = value ? value : 0;
|
|
|
|
} else if (EV_DEVICE_LABELS[this._sensorPorts[i]]) { // if valid
|
|
|
|
// Read brightness / distance values and set to 0 if null
|
|
|
|
this._sensors[EV_DEVICE_LABELS[this._sensorPorts[i]]] = value ? value : 0;
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
2018-07-17 16:03:06 -04:00
|
|
|
// log.info(`${JSON.stringify(this._sensors)}`);
|
2018-07-13 13:14:53 -04:00
|
|
|
offset += 4;
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
// READ MOTOR POSITION VALUES
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
let value = this._tachoValue([ // from Paula
|
|
|
|
array[offset],
|
|
|
|
array[offset + 1],
|
|
|
|
array[offset + 2],
|
|
|
|
array[offset + 3]
|
|
|
|
]);
|
|
|
|
if (value > 0x7fffffff) { // from Paula
|
|
|
|
value = value - 0x100000000;
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
if (value) {
|
|
|
|
this._motors.positions[i] = value;
|
|
|
|
}
|
2018-07-17 16:03:06 -04:00
|
|
|
// log.info(`motor positions: ${this._motors.positions}`);
|
2018-07-13 13:14:53 -04:00
|
|
|
offset += 4;
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
/*
|
|
|
|
// READ MOTOR BUSY STATES
|
|
|
|
/*
|
|
|
|
for (let i = 0; i < this._motorPorts.length; i++) {
|
|
|
|
if (this._motorPorts[i] !== 'none') {
|
|
|
|
const busy = array[offset];
|
|
|
|
if (busy === 0 && this._motors.busy[i]) {
|
|
|
|
this.motorCoast(i); // always set to coast for now, but really should only do for recently moved
|
|
|
|
this._motors.busy[i] = 0; // reset busy
|
|
|
|
}
|
|
|
|
// this._motors.positions[i] = value;
|
|
|
|
log.info(`motor ${i} busy: ${busy}`);
|
|
|
|
offset += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: keep here? / refactor
|
|
|
|
_portMask (port) {
|
|
|
|
// TODO: convert to enum or lookup
|
|
|
|
let p = null;
|
|
|
|
if (port === 0) {
|
|
|
|
p = 1;
|
|
|
|
} else if (port === 1) {
|
|
|
|
p = 2;
|
|
|
|
} else if (port === 2) {
|
|
|
|
p = 4;
|
|
|
|
} else if (port === 3) {
|
|
|
|
p = 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
return p;
|
2018-06-28 13:40:16 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-28 13:40:16 -04:00
|
|
|
_tachoValue (list) {
|
|
|
|
const value = list[0] + (list[1] * 256) + (list[2] * 256 * 256) + (list[3] * 256 * 256 * 256);
|
|
|
|
return value;
|
2018-06-26 15:07:08 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: keep here? / refactor
|
2018-06-26 15:07:08 -04:00
|
|
|
_array2float (list) {
|
|
|
|
const buffer = new Uint8Array(list).buffer;
|
|
|
|
const view = new DataView(buffer);
|
|
|
|
return view.getFloat32(0, true);
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
class Scratch3Ev3Blocks {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The ID of the extension.
|
|
|
|
* @return {string} the id
|
|
|
|
*/
|
|
|
|
static get EXTENSION_ID () {
|
|
|
|
return 'ev3';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new instance of the EV3 extension.
|
|
|
|
* @param {object} runtime VM runtime
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
constructor (runtime) {
|
|
|
|
/**
|
|
|
|
* The Scratch 3.0 runtime.
|
|
|
|
* @type {Runtime}
|
|
|
|
*/
|
|
|
|
this.runtime = runtime;
|
|
|
|
|
2018-06-25 18:24:23 -04:00
|
|
|
// Create a new EV3 device instance
|
2018-06-25 13:42:48 -04:00
|
|
|
this._device = new EV3(this.runtime, Scratch3Ev3Blocks.EXTENSION_ID);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Define the EV3 extension.
|
|
|
|
* @return {object} Extension description.
|
|
|
|
*/
|
|
|
|
getInfo () {
|
|
|
|
return {
|
|
|
|
id: Scratch3Ev3Blocks.EXTENSION_ID,
|
2018-07-13 13:14:53 -04:00
|
|
|
name: 'LEGO EV3',
|
2018-07-02 18:07:34 -04:00
|
|
|
blockIconURI: blockIconURI,
|
2018-06-25 18:24:23 -04:00
|
|
|
showStatusButton: true,
|
2018-06-25 13:42:48 -04:00
|
|
|
blocks: [
|
|
|
|
{
|
|
|
|
opcode: 'motorTurnClockwise',
|
2018-07-13 13:14:53 -04:00
|
|
|
text: 'motor [PORT] turn this way for [TIME] seconds',
|
2018-06-25 13:42:48 -04:00
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'motorPorts',
|
2018-07-13 13:14:53 -04:00
|
|
|
defaultValue: 'A'
|
2018-06-25 13:42:48 -04:00
|
|
|
},
|
|
|
|
TIME: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'motorTurnCounterClockwise',
|
2018-07-13 13:14:53 -04:00
|
|
|
text: 'motor [PORT] turn that way for [TIME] seconds',
|
2018-06-25 13:42:48 -04:00
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'motorPorts',
|
2018-07-13 13:14:53 -04:00
|
|
|
defaultValue: 'A'
|
2018-06-25 13:42:48 -04:00
|
|
|
},
|
|
|
|
TIME: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2018-07-13 13:14:53 -04:00
|
|
|
/* {
|
2018-06-25 14:11:26 -04:00
|
|
|
opcode: 'motorRotate',
|
|
|
|
text: 'motor [PORT] rotate [DEGREES] degrees',
|
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'motorPorts',
|
2018-07-13 13:14:53 -04:00
|
|
|
defaultValue: 'A'
|
2018-06-25 14:11:26 -04:00
|
|
|
},
|
|
|
|
DEGREES: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 90
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'motorSetPosition',
|
|
|
|
text: 'motor [PORT] set position [DEGREES] degrees',
|
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'motorPorts',
|
2018-07-13 13:14:53 -04:00
|
|
|
defaultValue: 'A'
|
2018-06-25 14:11:26 -04:00
|
|
|
},
|
|
|
|
DEGREES: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 90
|
|
|
|
}
|
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
}, */
|
2018-06-25 14:11:26 -04:00
|
|
|
{
|
|
|
|
opcode: 'motorSetPower',
|
|
|
|
text: 'motor [PORT] set power [POWER] %',
|
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'motorPorts',
|
2018-07-13 13:14:53 -04:00
|
|
|
defaultValue: 'A'
|
2018-06-25 14:11:26 -04:00
|
|
|
},
|
|
|
|
POWER: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 50
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'getMotorPosition',
|
|
|
|
text: 'motor [PORT] position',
|
|
|
|
blockType: BlockType.REPORTER,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'motorPorts',
|
2018-07-13 13:14:53 -04:00
|
|
|
defaultValue: 'A'
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'whenButtonPressed',
|
|
|
|
text: 'when button [PORT] pressed',
|
|
|
|
blockType: BlockType.HAT,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'sensorPorts',
|
|
|
|
defaultValue: SENSOR_PORTS[0].value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'whenDistanceLessThan',
|
|
|
|
text: 'when distance < [DISTANCE]',
|
|
|
|
blockType: BlockType.HAT,
|
|
|
|
arguments: {
|
|
|
|
DISTANCE: {
|
|
|
|
type: ArgumentType.NUMBER,
|
2018-06-26 15:07:08 -04:00
|
|
|
defaultValue: 5
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'whenBrightnessLessThan',
|
|
|
|
text: 'when brightness < [DISTANCE]',
|
|
|
|
blockType: BlockType.HAT,
|
|
|
|
arguments: {
|
|
|
|
DISTANCE: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 50
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'buttonPressed',
|
|
|
|
text: 'button [PORT] pressed?',
|
|
|
|
blockType: BlockType.BOOLEAN,
|
|
|
|
arguments: {
|
|
|
|
PORT: {
|
|
|
|
type: ArgumentType.STRING,
|
|
|
|
menu: 'sensorPorts',
|
|
|
|
defaultValue: SENSOR_PORTS[0].value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'getDistance',
|
|
|
|
text: 'distance',
|
|
|
|
blockType: BlockType.REPORTER
|
|
|
|
},
|
|
|
|
{
|
|
|
|
opcode: 'getBrightness',
|
|
|
|
text: 'brightness',
|
|
|
|
blockType: BlockType.REPORTER
|
|
|
|
},
|
2018-06-25 13:42:48 -04:00
|
|
|
{
|
|
|
|
opcode: 'beep',
|
2018-07-13 13:14:53 -04:00
|
|
|
text: 'beep note [NOTE] for [TIME] secs',
|
|
|
|
blockType: BlockType.COMMAND,
|
|
|
|
arguments: {
|
|
|
|
NOTE: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 60
|
|
|
|
},
|
|
|
|
TIME: {
|
|
|
|
type: ArgumentType.NUMBER,
|
|
|
|
defaultValue: 0.5
|
|
|
|
}
|
|
|
|
}
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
],
|
|
|
|
menus: {
|
2018-06-25 14:11:26 -04:00
|
|
|
motorPorts: this._buildMenu(MOTOR_PORTS),
|
|
|
|
sensorPorts: this._buildMenu(SENSOR_PORTS)
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
// TODO: redo?
|
2018-06-25 13:42:48 -04:00
|
|
|
/**
|
|
|
|
* Create data for a menu in scratch-blocks format, consisting of an array of objects with text and
|
|
|
|
* value properties. The text is a translated string, and the value is one-indexed.
|
|
|
|
* @param {object[]} info - An array of info objects each having a name property.
|
|
|
|
* @return {array} - An array of objects with text and value properties.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_buildMenu (info) {
|
|
|
|
return info.map((entry, index) => {
|
|
|
|
const obj = {};
|
|
|
|
obj.text = entry.name;
|
2018-07-13 13:14:53 -04:00
|
|
|
obj.value = String(index);
|
2018-06-25 13:42:48 -04:00
|
|
|
return obj;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
motorTurnClockwise (args) {
|
2018-07-13 13:14:53 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
2018-07-17 16:03:06 -04:00
|
|
|
let time = Cast.toNumber(args.TIME) * 1000;
|
|
|
|
time = MathUtil.clamp(time, 0, 15000);
|
|
|
|
|
|
|
|
if (!VALID_MOTOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-06-25 13:42:48 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
return this._device.motorTurnClockwise(port, time);
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
motorTurnCounterClockwise (args) {
|
2018-07-13 13:14:53 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
2018-07-17 16:03:06 -04:00
|
|
|
let time = Cast.toNumber(args.TIME) * 1000;
|
|
|
|
time = MathUtil.clamp(time, 0, 15000);
|
|
|
|
|
|
|
|
if (!VALID_MOTOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-06-25 13:42:48 -04:00
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
return this._device.motorTurnCounterClockwise(port, time);
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
/*
|
2018-06-25 14:11:26 -04:00
|
|
|
motorRotate (args) {
|
2018-06-25 14:39:38 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
|
|
|
const degrees = Cast.toNumber(args.DEGREES);
|
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!VALID_MOTOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
this._device.motorRotate(port, degrees);
|
|
|
|
return;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
motorSetPosition (args) {
|
2018-06-25 14:39:38 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
|
|
|
const degrees = Cast.toNumber(args.DEGREES);
|
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!VALID_MOTOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
this._device.motorSetPosition(port, degrees);
|
|
|
|
return;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
2018-07-17 16:03:06 -04:00
|
|
|
*/
|
2018-06-25 14:11:26 -04:00
|
|
|
|
|
|
|
motorSetPower (args) {
|
2018-06-25 14:39:38 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
2018-07-17 16:03:06 -04:00
|
|
|
const power = MathUtil.clamp(Cast.toNumber(args.POWER), 0, 100);
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!VALID_MOTOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
this._device.motorSetPower(port, power);
|
2018-06-25 14:39:38 -04:00
|
|
|
return;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getMotorPosition (args) {
|
2018-07-13 13:14:53 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!VALID_MOTOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
return this._device.getMotorPosition(port);
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
whenButtonPressed (args) {
|
2018-07-13 13:14:53 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!VALID_SENSOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
return this._device.isButtonPressed(port);
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
whenDistanceLessThan (args) {
|
2018-07-17 16:03:06 -04:00
|
|
|
const distance = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100);
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-06-26 15:07:08 -04:00
|
|
|
return this._device.distance < distance;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
whenBrightnessLessThan (args) {
|
2018-07-17 16:03:06 -04:00
|
|
|
const brightness = MathUtil.clamp(Cast.toNumber(args.DISTANCE), 0, 100);
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-06-28 13:40:16 -04:00
|
|
|
return this._device.brightness < brightness;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
buttonPressed (args) {
|
2018-07-13 13:14:53 -04:00
|
|
|
const port = Cast.toNumber(args.PORT);
|
2018-06-25 14:39:38 -04:00
|
|
|
|
2018-07-17 16:03:06 -04:00
|
|
|
if (!VALID_SENSOR_PORTS.includes(port)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-25 14:39:38 -04:00
|
|
|
return this._device.isButtonPressed(port);
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getDistance () {
|
2018-06-25 14:39:38 -04:00
|
|
|
return this._device.distance;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
getBrightness () {
|
2018-06-25 14:39:38 -04:00
|
|
|
return this._device.brightness;
|
2018-06-25 14:11:26 -04:00
|
|
|
}
|
|
|
|
|
2018-07-13 13:14:53 -04:00
|
|
|
beep (args) {
|
2018-07-17 16:03:06 -04:00
|
|
|
const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 47, 99); // valid EV3 sounds
|
|
|
|
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
|
|
|
|
}
|
2018-07-13 13:14:53 -04:00
|
|
|
|
|
|
|
// https://en.wikipedia.org/wiki/MIDI_tuning_standard#Frequency_values
|
|
|
|
const freq = Math.pow(2, ((note - 69 + 12) / 12)) * 440;
|
|
|
|
|
|
|
|
return this._device.beep(freq, time);
|
2018-06-25 13:42:48 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Scratch3Ev3Blocks;
|